From 7d5b7a310a52372dac0a8bac90fd2a6a2ab342dc Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 1 Nov 2024 17:33:57 +0200 Subject: [PATCH 1/3] Update bug-reports.yml --- .github/DISCUSSION_TEMPLATE/bug-reports.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index 04cf427403..fcf95819ca 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -74,6 +74,7 @@ body: description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - v1.6.19.1 (Unto the Breach Update Hotfix 2) + - v1.7.0.1 (Unstable) - Other validations: required: true From f6349b2175258d19105e51c4f852648649b3869e Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Wed, 11 Dec 2024 13:26:13 +0200 Subject: [PATCH 2/3] v1.7.7.0 (Winter Update 2024) --- .../BarotraumaClient/ClientSource/Camera.cs | 15 +- .../Characters/AI/EnemyAIController.cs | 2 +- .../Characters/AI/HumanAIController.cs | 2 +- .../Characters/Animation/Ragdoll.cs | 79 +++-- .../ClientSource/Characters/Character.cs | 17 +- .../ClientSource/Characters/CharacterHUD.cs | 52 ++- .../ClientSource/Characters/CharacterInfo.cs | 11 + .../Characters/CharacterNetworking.cs | 18 +- .../Characters/Health/AfflictionPsychosis.cs | 2 +- .../Characters/Health/CharacterHealth.cs | 4 +- .../ClientSource/Characters/Limb.cs | 97 ++++-- .../ClientSource/DebugConsole.cs | 14 +- .../Events/EventActions/ConversationAction.cs | 29 +- .../ClientSource/Events/EventLog.cs | 1 + .../ClientSource/Events/Missions/Mission.cs | 10 + .../Events/Missions/MissionPrefab.cs | 4 +- .../Events/Missions/ScanMission.cs | 17 +- .../ClientSource/Fonts/ScalableFont.cs | 75 +++-- .../ClientSource/GUI/GUIComponent.cs | 51 +++ .../ClientSource/GUI/Store.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 3 +- .../ClientSource/GUI/TalentMenu.cs | 13 +- .../BarotraumaClient/ClientSource/GameMain.cs | 15 +- .../ClientSource/GameSession/CrewManager.cs | 15 +- .../ClientSource/GameSession/RoundSummary.cs | 11 +- .../ClientSource/Items/Components/Door.cs | 7 + .../Items/Components/GeneticMaterial.cs | 7 +- .../Items/Components/Holdable/Holdable.cs | 8 +- .../Items/Components/ItemComponent.cs | 10 +- .../Items/Components/Machines/Fabricator.cs | 6 +- .../Items/Components/Machines/Sonar.cs | 19 -- .../Items/Components/Projectile.cs | 2 +- .../Components/Signal/CustomInterface.cs | 104 ++++-- .../Items/Components/Signal/Terminal.cs | 62 ++-- .../Items/Components/Signal/Wire.cs | 2 + .../Items/Components/StatusHUD.cs | 2 +- .../ClientSource/Items/Item.cs | 39 ++- .../BarotraumaClient/ClientSource/Map/Gap.cs | 8 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 7 +- .../ClientSource/Map/Levels/CaveGenerator.cs | 124 +++++-- .../Levels/LevelObjects/LevelObjectManager.cs | 28 +- .../ClientSource/Map/Levels/LevelRenderer.cs | 90 +++-- .../ClientSource/Map/Levels/LevelWall.cs | 26 +- .../ClientSource/Map/Lights/ConvexHull.cs | 4 +- .../ClientSource/Map/Lights/LightManager.cs | 44 ++- .../ClientSource/Map/Lights/LightSource.cs | 12 +- .../ClientSource/Map/Structure.cs | 26 +- .../ClientSource/Map/Submarine.cs | 54 +-- .../ClientSource/Map/WayPoint.cs | 4 + .../ClientSource/Networking/GameClient.cs | 76 +++-- .../ClientEntityEventManager.cs | 38 ++- .../P2PSocket/SteamConnectSocket.cs | 11 +- .../Networking/Voip/VoipCapture.cs | 61 ++++ .../ClientSource/Particles/ParticlePrefab.cs | 1 + .../ClientSource/Physics/PhysicsBody.cs | 4 +- .../CampaignSetupUI/CampaignSetupUI.cs | 17 + .../MultiPlayerCampaignSetupUI.cs | 32 +- .../ClientSource/Screens/CampaignUI.cs | 10 +- .../CharacterEditor/CharacterEditorScreen.cs | 2 +- .../ClientSource/Screens/GameScreen.cs | 61 ++-- .../ClientSource/Screens/LevelEditorScreen.cs | 29 +- .../ClientSource/Screens/NetLobbyScreen.cs | 15 +- .../ServerListScreen/ServerListScreen.cs | 2 + .../ClientSource/Screens/SubEditorScreen.cs | 9 +- .../ClientSource/Settings/SettingsMenu.cs | 74 ++++- .../ClientSource/Sounds/OpenAL/Alc.cs | 6 +- .../ClientSource/Sounds/SoundManager.cs | 48 +++ .../ClientSource/SpamServerFilter.cs | 2 + .../ClientSource/Sprite/Sprite.cs | 17 +- .../WorkshopMenu/Mutable/InstalledTab.cs | 4 +- .../ClientSource/Utils/MathUtils.cs | 19 ++ .../ClientSource/Utils/WikiImage.cs | 11 +- .../Content/Effects/damageshader.xnb | Bin 2340 -> 2341 bytes .../Content/Effects/damageshader_opengl.xnb | Bin 2274 -> 2274 bytes .../BarotraumaClient/LinuxClient.csproj | 4 +- Barotrauma/BarotraumaClient/MacClient.csproj | 4 +- .../BarotraumaClient/Shaders/damageshader.fx | 6 +- .../Shaders/damageshader_opengl.fx | 8 +- .../BarotraumaClient/WindowsClient.csproj | 4 +- .../BarotraumaServer/LinuxServer.csproj | 4 +- Barotrauma/BarotraumaServer/MacServer.csproj | 4 +- .../ServerSource/Characters/Character.cs | 12 + .../Characters/CharacterNetworking.cs | 20 +- .../ServerSource/Characters/Limb.cs | 70 ++++ .../ServerSource/DebugConsole.cs | 5 + .../Events/EventActions/ConversationAction.cs | 40 ++- .../Events/Missions/CombatMission.cs | 21 ++ .../Events/Missions/ScanMission.cs | 9 +- .../Items/Components/Holdable/Holdable.cs | 11 +- .../ServerSource/Items/Item.cs | 19 +- .../BarotraumaServer/ServerSource/Map/Hull.cs | 6 + .../ServerSource/Networking/Client.cs | 6 +- .../ServerSource/Networking/GameServer.cs | 21 +- .../ServerSource/Networking/KarmaManager.cs | 6 +- .../ServerEntityEventManager.cs | 47 ++- .../Peers/Server/LidgrenServerPeer.cs | 14 +- .../ServerSource/Networking/ServerSettings.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 4 +- .../Data/campaignsettings.xml | 6 +- .../Crawler/Animations/CrawlerRun.xml | 2 + .../Crawler/Animations/CrawlerSwimFast.xml | 2 + .../Crawler/Animations/CrawlerSwimSlow.xml | 2 + .../Crawler/Animations/CrawlerWalk.xml | 2 + .../Characters/Crawler/Crawler.xml | 77 +++++ .../Ragdolls/CrawlerDefaultRagdoll.xml | 127 +++++++ .../Characters/Crawler/crawler.png | Bin 0 -> 220731 bytes .../Human.xml | 312 ++++++++++++++++++ .../Mudraptor.xml | 68 ++++ .../README.txt | 27 ++ .../Spineling_morbusine_m.xml | 1 + .../Testcyborgworm_m.xml | 3 + .../filelist.xml | 8 + .../SharedSource/AchievementManager.cs | 105 +++++- .../Characters/AI/EnemyAIController.cs | 35 +- .../Characters/AI/HumanAIController.cs | 180 ++-------- .../Characters/AI/IndoorsSteeringManager.cs | 58 ++-- .../Objectives/AIObjectiveFightIntruders.cs | 2 +- .../AI/Objectives/AIObjectiveIdle.cs | 10 +- .../AI/Objectives/AIObjectiveOperateItem.cs | 86 ++++- .../AI/Objectives/AIObjectiveRepairItem.cs | 2 +- .../AI/Objectives/AIObjectiveRepairItems.cs | 54 ++- .../AI/Objectives/AIObjectiveRescue.cs | 15 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 4 +- .../AI/ShipCommand/ShipIssueWorker.cs | 12 +- .../Characters/AI/Wreck/WreckAI.cs | 177 +++++----- .../Characters/Animation/AnimController.cs | 14 +- .../Animation/FishAnimController.cs | 21 +- .../Animation/HumanoidAnimController.cs | 28 +- .../Characters/Animation/Ragdoll.cs | 49 ++- .../SharedSource/Characters/Attack.cs | 19 +- .../SharedSource/Characters/Character.cs | 281 +++++++--------- .../SharedSource/Characters/CharacterInfo.cs | 8 +- .../Characters/CharacterNetworking.cs | 17 +- .../Health/Afflictions/AfflictionPrefab.cs | 9 +- .../Characters/Health/CharacterHealth.cs | 15 +- .../SharedSource/Characters/HumanPrefab.cs | 8 +- .../Characters/Jobs/SkillPrefab.cs | 2 +- .../SharedSource/Characters/Limb.cs | 22 +- .../Params/Animation/AnimationParams.cs | 6 +- .../Characters/Params/CharacterParams.cs | 33 +- .../Params/Ragdoll/RagdollParams.cs | 48 ++- .../Talents/Abilities/CharacterAbility.cs | 2 +- .../CharacterAbilityApplyStatusEffects.cs | 18 +- .../CharacterAbilityReduceAffliction.cs | 26 +- .../AbilityGroups/CharacterAbilityGroup.cs | 2 +- .../Characters/Talents/TalentPrefab.cs | 7 + .../SharedSource/DebugConsole.cs | 18 +- .../DisembarkPerks/PerkBehaviors/PerkBase.cs | 2 +- .../BarotraumaShared/SharedSource/Enums.cs | 5 + .../Events/EventActions/AddScoreAction.cs | 84 +++++ .../EventActions/CheckVisibilityAction.cs | 2 +- .../Events/EventActions/ConversationAction.cs | 4 +- .../EventActions/WaitForItemUsedAction.cs | 10 +- .../SharedSource/Events/EventManager.cs | 4 +- .../Events/Missions/BeaconMission.cs | 4 +- .../Events/Missions/CargoMission.cs | 2 +- .../Events/Missions/EscortMission.cs | 2 +- .../SharedSource/Events/Missions/Mission.cs | 32 +- .../Events/Missions/MissionPrefab.cs | 3 + .../Events/Missions/PirateMission.cs | 2 +- .../Events/Missions/ScanMission.cs | 35 +- .../SharedSource/GameSession/CargoManager.cs | 31 +- .../SharedSource/GameSession/CrewManager.cs | 24 +- .../GameSession/GameModes/CampaignMode.cs | 16 + .../GameSession/GameModes/CampaignSettings.cs | 18 +- .../SharedSource/GameSession/GameSession.cs | 7 +- .../SharedSource/InputType.cs | 4 +- .../SharedSource/Items/Components/Door.cs | 36 +- .../Items/Components/Holdable/Holdable.cs | 78 ++++- .../Items/Components/Holdable/MeleeWeapon.cs | 5 +- .../Items/Components/Holdable/Pickable.cs | 16 +- .../Items/Components/Holdable/RangedWeapon.cs | 7 +- .../Items/Components/Holdable/RepairTool.cs | 5 +- .../Items/Components/ItemComponent.cs | 6 +- .../Items/Components/ItemContainer.cs | 7 +- .../Items/Components/Machines/Controller.cs | 26 +- .../Items/Components/Machines/Fabricator.cs | 25 +- .../Items/Components/Machines/Reactor.cs | 8 + .../Items/Components/Machines/Sonar.cs | 26 ++ .../Items/Components/Power/PowerTransfer.cs | 2 +- .../Items/Components/Power/Powered.cs | 7 + .../Items/Components/Projectile.cs | 31 +- .../Items/Components/Signal/MotionSensor.cs | 32 +- .../Items/Components/Signal/RelayComponent.cs | 1 - .../Items/Components/Signal/Terminal.cs | 16 +- .../Items/Components/Signal/WifiComponent.cs | 14 +- .../SharedSource/Items/Components/Turret.cs | 17 +- .../SharedSource/Items/Components/Wearable.cs | 3 +- .../SharedSource/Items/Inventory.cs | 45 ++- .../SharedSource/Items/Item.cs | 158 +++++---- .../SharedSource/Items/ItemPrefab.cs | 4 +- .../SharedSource/Map/Entity.cs | 7 +- .../SharedSource/Map/Explosion.cs | 4 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 99 +++++- .../BarotraumaShared/SharedSource/Map/Hull.cs | 6 +- .../SharedSource/Map/ISpatialEntity.cs | 112 ++++++- .../SharedSource/Map/Levels/Biome.cs | 8 +- .../SharedSource/Map/Levels/CaveGenerator.cs | 112 +++++-- .../SharedSource/Map/Levels/Level.cs | 96 +++--- .../Map/Levels/LevelGenerationParams.cs | 21 +- .../Levels/LevelObjects/LevelObjectPrefab.cs | 8 +- .../Map/Levels/LevelObjects/LevelTrigger.cs | 15 + .../SharedSource/Map/Map/Location.cs | 16 +- .../SharedSource/Map/Map/LocationType.cs | 2 + .../SharedSource/Map/MapEntityPrefab.cs | 10 +- .../Map/Outposts/OutpostGenerator.cs | 18 +- .../SharedSource/Map/Structure.cs | 2 + .../SharedSource/Map/Submarine.cs | 14 +- .../SharedSource/Map/SubmarineBody.cs | 10 +- .../SharedSource/Networking/NetConfig.cs | 14 +- .../NetEntityEvent/NetEntityEventManager.cs | 16 +- .../Networking/OrderChatMessage.cs | 4 +- .../SharedSource/Networking/ServerSettings.cs | 14 + .../SharedSource/Physics/Physics.cs | 1 + .../SharedSource/Physics/PhysicsBody.cs | 5 +- .../SharedSource/ProcGen/VoronoiElements.cs | 54 ++- .../SharedSource/Screens/GameScreen.cs | 5 +- .../SerializableProperty.cs | 40 +++ .../Serialization/XMLExtensions.cs | 4 +- .../SharedSource/Settings/GameSettings.cs | 6 +- .../StatusEffects/StatusEffect.cs | 89 ++++- .../SharedSource/Steam/SteamManager.cs | 14 +- .../SharedSource/Text/TextManager.cs | 56 +++- .../SharedSource/Utils/SaveUtil.cs | 31 +- .../SharedSource/Utils/ToolBox.cs | 7 + Barotrauma/BarotraumaShared/changelog.txt | 160 +++++++++ .../Extensions/EnumExtensions.cs | 33 +- .../BarotraumaCore/Utils/MathUtils.cs | 3 +- .../BarotraumaCore/Utils/ReflectionUtils.cs | 5 +- .../Collision/DynamicTree.cs | 34 +- .../Collision/DynamicTreeBroadPhase.cs | 2 +- .../Collision/Shapes/CircleShape.cs | 6 +- .../Decomposition/Seidel/MonotoneMountain.cs | 2 +- .../Farseer Physics Engine 3.5/Common/Math.cs | 2 +- .../Common/Maths/Complex.cs | 12 +- .../Farseer Physics Engine 3.5/Common/Path.cs | 2 +- .../Common/PhysicsLogic/RealExplosion.cs | 14 +- .../Common/PhysicsLogic/SimpleExplosion.cs | 4 +- .../PolygonManipulation/SimplifyTools.cs | 4 +- .../Common/PolygonTools.cs | 36 +- .../Common/Vertices.cs | 4 +- .../Controllers/GravityController.cs | 4 +- .../Controllers/VelocityLimitController.cs | 2 +- .../Dynamics/Body.cs | 69 +++- .../Dynamics/ContactManager.cs | 1 - .../Dynamics/Fixture.cs | 12 +- .../Dynamics/Joints/Joint.cs | 4 +- .../Fluids/1/FluidSystem1.cs | 12 +- .../Fluids/2/FluidSystem2.cs | 6 +- .../Farseer Physics Engine 3.5/Settings.cs | 2 +- .../Src/MonoGame.Framework/Display.cs | 11 + .../Src/MonoGame.Framework/GameWindow.cs | 2 + ...onoGame.Framework.Linux.NetStandard.csproj | 1 + ...onoGame.Framework.MacOS.NetStandard.csproj | 1 + ...oGame.Framework.Windows.NetStandard.csproj | 1 + .../MonoGame.Framework/SDL/SDLGameWindow.cs | 32 +- 256 files changed, 4795 insertions(+), 1654 deletions(-) create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Characters/Limb.cs create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerRun.xml create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerSwimFast.xml create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerSwimSlow.xml create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerWalk.xml create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Ragdolls/CrawlerDefaultRagdoll.xml create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/crawler.png create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Human.xml create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Mudraptor.xml create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/README.txt create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Spineling_morbusine_m.xml create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Testcyborgworm_m.xml create mode 100644 Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/filelist.xml create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AddScoreAction.cs create mode 100644 Libraries/MonoGame.Framework/Src/MonoGame.Framework/Display.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index c497c157f1..66429655b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -63,6 +63,12 @@ public float MaxZoom private float prevZoom; public float Shake; + + /// + /// Should the camera's transform matrices be automatically updated to match the screen resolution? + /// + public bool AutoUpdateToScreenResolution = true; + public Vector2 ShakePosition { get; private set; } private float shakeTimer; @@ -198,10 +204,13 @@ public void SetResolution(Point res) public void UpdateTransform(bool interpolate = true, bool updateListener = true) { - if (GameMain.GraphicsWidth != Resolution.X || - GameMain.GraphicsHeight != Resolution.Y) + if (AutoUpdateToScreenResolution) { - CreateMatrices(); + if (GameMain.GraphicsWidth != Resolution.X || + GameMain.GraphicsHeight != Resolution.Y) + { + CreateMatrices(); + } } Vector2 interpolatedPosition = interpolate ? Timing.Interpolate(prevPosition, position) : position; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index 55998217a3..bdc1abf4b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -42,7 +42,7 @@ public override void DebugDraw(SpriteBatch spriteBatch) if (wallTarget != null && !IsCoolDownRunning) { Vector2 wallTargetPos = wallTarget.Position; - if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.Position; } + if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.DrawPosition; } wallTargetPos.Y = -wallTargetPos.Y; GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false); GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 672fffbb72..3b097532db 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -11,7 +11,7 @@ public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spri { if (Character == Character.Controlled) { return; } if (!DebugAI) { return; } - Vector2 pos = Character.WorldPosition; + Vector2 pos = Character.DrawPosition; pos.Y = -pos.Y; Vector2 textOffset = new Vector2(-40, -160); textOffset.Y -= Math.Max(ObjectiveManager.CurrentOrders.Count - 1, 0) * 20; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index e6b91667c6..b1e635d967 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -93,6 +93,9 @@ partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSu character.AnimController.Anim = AnimController.Animation.None; } + character.AnimController.IgnorePlatforms = character.MemState[0].IgnorePlatforms; + character.AnimController.overrideTargetMovement = character.MemState[0].TargetMovement; + Vector2 newVelocity = Collider.LinearVelocity; Vector2 newPosition = Collider.SimPosition; float newRotation = Collider.Rotation; @@ -103,16 +106,17 @@ partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSu { newVelocity = newVelocity.ClampLength(100.0f); if (!MathUtils.IsValid(newVelocity)) { newVelocity = Vector2.Zero; } - overrideTargetMovement = newVelocity.LengthSquared() > 0.01f ? newVelocity : Vector2.Zero; Collider.LinearVelocity = newVelocity; Collider.AngularVelocity = newAngularVelocity; } float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); - float errorTolerance = character.CanMove && (!character.IsRagdolled || character.AnimController.IsHangingWithRope) ? 0.01f : 0.2f; + float errorTolerance = + ColliderControlsMovement && (!character.IsRagdolled || character.AnimController.IsHangingWithRope) ? 0.01f : 0.2f; if (distSqrd > errorTolerance) { - if (distSqrd > 10.0f || !character.CanMove) + character.AnimController.BodyInRest = false; + if (distSqrd > 10.0f) { Collider.TargetRotation = newRotation; if (distSqrd > 10.0f) @@ -126,28 +130,31 @@ partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSu } } SetPosition(newPosition, lerp: distSqrd < 5.0f, ignorePlatforms: false); + //make sure ragdoll isn't stuck at the wrong side of a platform if the movement is controlled by the ragdoll, and the ragdoll has come to rest server-side + if (!ColliderControlsMovement && newVelocity.LengthSquared() < 0.01f) { TryPlatformCorrection(newPosition); } } - else + else if (ColliderControlsMovement) { Collider.TargetRotation = newRotation; Collider.TargetPosition = newPosition; Collider.MoveToTargetPosition(true); } - } - - //immobilized characters can't correct their position using AnimController movement - // -> we need to correct it manually - if (!character.CanMove) - { - float mainLimbDistSqrd = Vector2.DistanceSquared(MainLimb.PullJointWorldAnchorA, Collider.SimPosition); - float mainLimbErrorTolerance = 0.1f; - //if the main limb is roughly at the correct position and the collider isn't moving (much at least), - //don't attempt to correct the position. - if (mainLimbDistSqrd > mainLimbErrorTolerance || Collider.LinearVelocity.LengthSquared() > 0.05f) + else { - MainLimb.PullJointWorldAnchorB = Collider.SimPosition; - MainLimb.PullJointEnabled = true; - MainLimb.body.LinearVelocity = newVelocity; + float mainLimbDistSqrd = Vector2.DistanceSquared(MainLimb.PullJointWorldAnchorA, newPosition); + float mainLimbErrorTolerance = 0.1f; + //if the main limb is roughly at the correct position and the collider isn't moving (much at least), + //don't attempt to correct the position. + if (mainLimbDistSqrd > mainLimbErrorTolerance) + { + MainLimb.PullJointWorldAnchorB = newPosition; + MainLimb.PullJointEnabled = true; + if (!ColliderControlsMovement && newVelocity.LengthSquared() < 0.01f) { TryPlatformCorrection(newPosition); } + } + else + { + MainLimb.body.LinearVelocity = newVelocity; + } } } } @@ -179,9 +186,9 @@ partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSu } } - if (character.MemState.Count < 1) return; + if (character.MemState.Count < 1) { return; } - overrideTargetMovement = Vector2.Zero; + overrideTargetMovement = null; CharacterStateInfo serverPos = character.MemState.Last(); @@ -294,6 +301,38 @@ partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSu character.MemState.Clear(); } } + + /// + /// Attempts to correct the ragdoll to the correct side of a platform if the server position is above the platform and some of the ragdoll's limbs below it client-side, or vice versa. + /// + private void TryPlatformCorrection(Vector2 serverPos) + { + float highestPos = limbs.Where(static l => !l.IsSevered).Max(static l => l.SimPosition.Y); + highestPos = Math.Max(serverPos.Y, highestPos); + float lowestPos = limbs.Where(static l => !l.IsSevered).Min(static l => l.SimPosition.Y); + lowestPos = Math.Min(serverPos.Y, lowestPos); + + var platform = Submarine.PickBody(new Vector2(serverPos.X, highestPos), new Vector2(serverPos.X, lowestPos), collisionCategory: Physics.CollisionPlatform, allowInsideFixture: true); + if (platform == null) { return; } + + int serverDir = Math.Sign(serverPos.Y - platform.Position.Y); + foreach (var limb in limbs) + { + if (limb.IsSevered) { continue; } + int limbDir = Math.Sign(limb.SimPosition.Y - platform.Position.Y); + + const float Margin = 0.01f; + + if (limbDir != serverDir) + { + limb.body.SetTransformIgnoreContacts( + new Vector2( + limb.SimPosition.X, + serverDir > 0 ? Math.Max(serverPos.Y + Margin + limb.body.GetMaxExtent(), limb.SimPosition.Y) : Math.Min(serverPos.Y - Margin - limb.body.GetMaxExtent(), limb.SimPosition.Y)), + limb.Rotation); + } + } + } partial void ImpactProjSpecific(float impact, Body body) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 23cd60f7f4..fee47ca70f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -250,7 +250,7 @@ sealed class SpeechBubble public Vector2 Position; public Vector2 DrawPosition; public float MoveUpAmount; - public readonly string Text; + public readonly RichString Text; public readonly Character Character; public readonly Submarine Submarine; public readonly Vector2 TextSize; @@ -260,7 +260,7 @@ sealed class SpeechBubble public SpeechBubble(Character character, float lifeTime, Color color, string text = "") { - Text = ToolBox.WrapText(text, GUI.IntScale(300), GUIStyle.SmallFont.GetFontForStr(text)); + Text = RichString.Rich(ToolBox.WrapText(text, GUI.IntScale(300), GUIStyle.SmallFont.GetFontForStr(text))); TextSize = GUIStyle.SmallFont.MeasureString(Text); Character = character; @@ -322,7 +322,6 @@ partial void UpdateLimbLightSource(Limb limb) /// public void ControlLocalPlayer(float deltaTime, Camera cam, bool moveCam = true) { - if (DisableControls || GUI.InputBlockingMenuOpen) { foreach (Key key in keys) @@ -417,6 +416,11 @@ void ResetInputIfPrimaryMouse(InputType inputType) UpdateLocalCursor(cam); + if (IsKeyHit(InputType.ToggleRun)) + { + ToggleRun = !ToggleRun; + } + Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition); if (GUI.PauseMenuOpen) { @@ -1189,7 +1193,7 @@ public static void DrawSpeechBubbles(SpriteBatch spriteBatch, Camera cam) Vector2 bubbleSize = bubble.TextSize + Vector2.One * GUI.IntScale(15); speechBubbleIconSliced.Draw(spriteBatch, new RectangleF(iconPos - bubbleSize / 2, bubbleSize), bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha); } - GUI.DrawString(spriteBatch, iconPos - bubble.TextSize / 2, bubble.Text, bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha, font: GUIStyle.SmallFont); + GUI.DrawStringWithColors(spriteBatch, iconPos - bubble.TextSize / 2, bubble.Text.SanitizedValue, bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha, bubble.Text.RichTextData, font: GUIStyle.SmallFont); } spriteBatch.End(); } @@ -1435,7 +1439,10 @@ partial void OnMoneyChanged(int prevAmount, int newAmount) { } partial void OnTalentGiven(TalentPrefab talentPrefab) { - AddMessage(TextManager.Get("talentname." + talentPrefab.Identifier).Value, GUIStyle.Yellow, playSound: this == Controlled); + if (!talentPrefab.IsHiddenExtraTalent) + { + AddMessage(TextManager.Get("talentname." + talentPrefab.Identifier).Value, GUIStyle.Yellow, playSound: this == Controlled); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 4068aba1a2..7eb98838d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -11,17 +11,15 @@ namespace Barotrauma { partial class CharacterHUD { - const float BossHealthBarDuration = 120.0f; - - abstract class BossProgressBar + abstract class ProgressBar { public float FadeTimer; public readonly GUIComponent TopContainer; public readonly GUIComponent SideContainer; - public readonly GUIProgressBar TopHealthBar; - public readonly GUIProgressBar SideHealthBar; + public readonly GUIProgressBar TopBar; + public readonly GUIProgressBar SideBar; public abstract bool Completed { get; } @@ -33,9 +31,9 @@ abstract class BossProgressBar public abstract Color Color { get; } - public BossProgressBar(LocalizedString label) + public ProgressBar(LocalizedString label, float fadeTimer = 120.0f) { - FadeTimer = BossHealthBarDuration; + FadeTimer = fadeTimer; TopContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.18f, 0.03f), HUDFrame.RectTransform, Anchor.TopCenter) { @@ -43,25 +41,25 @@ public BossProgressBar(LocalizedString label) RelativeOffset = new Vector2(0.0f, 0.01f) }, isHorizontal: false, childAnchor: Anchor.TopCenter); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), label, textAlignment: Alignment.Center, textColor: GUIStyle.Red); - TopHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.6f), TopContainer.RectTransform) + TopBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.6f), TopContainer.RectTransform) { MinSize = new Point(100, HUDLayoutSettings.HealthBarArea.Size.Y) }, barSize: 0.0f, style: "CharacterHealthBarCentered") { Color = GUIStyle.Red }; - CreateNumberText(TopHealthBar); + CreateNumberText(TopBar); SideContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), bossHealthContainer.RectTransform) { MinSize = new Point(80, 60) }, isHorizontal: false, childAnchor: Anchor.TopRight); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), label, textAlignment: Alignment.CenterRight, textColor: GUIStyle.Red); - SideHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.7f), SideContainer.RectTransform), barSize: 0.0f, style: "CharacterHealthBar") + SideBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.7f), SideContainer.RectTransform), barSize: 0.0f, style: "CharacterHealthBar") { Color = GUIStyle.Red }; - CreateNumberText(SideHealthBar); + CreateNumberText(SideBar); TopContainer.Visible = SideContainer.Visible = false; TopContainer.CanBeFocused = false; @@ -88,7 +86,7 @@ void CreateNumberText(GUIComponent parent) public abstract bool IsDuplicate(object targetObject); } - class BossHealthBar : BossProgressBar + class HealthBar : ProgressBar { public readonly Character Character; @@ -104,7 +102,7 @@ class BossHealthBar : BossProgressBar public override string NumberToDisplay => string.Empty; - public BossHealthBar(Character character) : base(character.DisplayName) + public HealthBar(Character character) : base(character.DisplayName) { Character = character; } @@ -115,7 +113,7 @@ public override bool IsDuplicate(object targetObject) } } - class MissionProgressBar : BossProgressBar + class MissionProgressBar : ProgressBar { public readonly Mission Mission; @@ -125,13 +123,13 @@ class MissionProgressBar : BossProgressBar public override bool Interrupted => Mission.Failed || GameMain.GameSession?.Missions == null || !GameMain.GameSession.Missions.Contains(Mission); - public override Color Color => GUIStyle.Red; + public override Color Color => Mission.Prefab.ProgressBarColor; public override string NumberToDisplay => Mission.Prefab.ShowProgressInNumbers ? $"{Mission.State}/{Mission.Prefab.MaxProgressState}" : string.Empty; - public MissionProgressBar(Mission mission) : base(mission.Prefab.ProgressBarLabel) + public MissionProgressBar(Mission mission) : base(mission.Prefab.ProgressBarLabel, fadeTimer: float.PositiveInfinity) { Mission = mission; } @@ -150,7 +148,7 @@ public override bool IsDuplicate(object targetObject) private static readonly List brokenItems = new List(); private static float brokenItemsCheckTimer; - private static readonly List bossProgressBars = new List(); + private static readonly List bossProgressBars = new List(); private static readonly Dictionary cachedHudTexts = new Dictionary(); private static LanguageIdentifier cachedHudTextLanguage = LanguageIdentifier.None; @@ -564,7 +562,7 @@ void DrawInteractionIcon(Entity entity, Identifier iconStyle) float alpha = MathHelper.Lerp(0.3f, 1.0f, distFactor); GUI.DrawIndicator( spriteBatch, - entity.WorldPosition, + entity.DrawPosition, cam, visibleRange, style.GetDefaultSprite(), @@ -592,7 +590,7 @@ void DrawInteractionIcon(Entity entity, Identifier iconStyle) if (Vector2.DistanceSquared(character.Position, item.Position) > 500f * 500f) { continue; } var body = Submarine.CheckVisibility(character.SimPosition, item.SimPosition, ignoreLevel: true); if (body != null && body.UserData as Item != item) { continue; } - GUI.DrawIndicator(spriteBatch, item.WorldPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Range(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color, createOffset: false); + GUI.DrawIndicator(spriteBatch, item.DrawPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Range(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color, createOffset: false); } } @@ -773,7 +771,7 @@ private static void DrawCharacterHoverTexts(SpriteBatch spriteBatch, Camera cam, GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); textPos.Y += textSize.Y; } - if (!character.FocusedCharacter.CustomInteractHUDText.IsNullOrEmpty() && character.FocusedCharacter.AllowCustomInteract) + if (character.FocusedCharacter.ShouldShowCustomInteractText) { GUI.DrawString(spriteBatch, textPos, character.FocusedCharacter.CustomInteractHUDText, GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); textPos.Y += textSize.Y; @@ -784,7 +782,7 @@ public static void ShowBossHealthBar(Character character, float damage) { if (character == null || character.IsDead || character.Removed) { return; } if (bossProgressBars.Any(b => b.IsDuplicate(character))) { return; } - AddBossProgressBar(new BossHealthBar(character)); + AddBossProgressBar(new HealthBar(character)); } public static void ShowMissionProgressBar(Mission mission) @@ -803,14 +801,14 @@ public static void ClearBossProgressBars() bossProgressBars.Clear(); } - private static void RemoveBossProgressBar(BossProgressBar progressBar) + private static void RemoveBossProgressBar(ProgressBar progressBar) { progressBar.SideContainer.Parent?.RemoveChild(progressBar.SideContainer); progressBar.TopContainer.Parent?.RemoveChild(progressBar.TopContainer); bossProgressBars.Remove(progressBar); } - private static void AddBossProgressBar(BossProgressBar progressBar) + private static void AddBossProgressBar(ProgressBar progressBar) { var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; if (healthBarMode == EnemyHealthBarMode.HideAll) @@ -819,10 +817,10 @@ private static void AddBossProgressBar(BossProgressBar progressBar) } if (bossProgressBars.Count > 5) { - BossProgressBar oldestHealthBar = bossProgressBars.First(); + ProgressBar oldestHealthBar = bossProgressBars.First(); foreach (var bar in bossProgressBars) { - if (bar.TopHealthBar.BarSize < oldestHealthBar.TopHealthBar.BarSize) + if (bar.TopBar.BarSize < oldestHealthBar.TopBar.BarSize) { oldestHealthBar = bar; } @@ -850,7 +848,7 @@ public static void UpdateBossProgressBars(float deltaTime) bossHealthBar.TopContainer.Visible = showTopBar; bossHealthBar.SideContainer.Visible = !bossHealthBar.TopContainer.Visible; - bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = bossHealthBar.State; + bossHealthBar.TopBar.BarSize = bossHealthBar.SideBar.BarSize = bossHealthBar.State; float alpha = Math.Min(bossHealthBar.FadeTimer, 1.0f); if (bossHealthBar.TopContainer.Visible) @@ -862,7 +860,7 @@ public static void UpdateBossProgressBars(float deltaTime) SetColor(bossHealthBar, bossHealthBar.SideContainer, alpha); } - static void SetColor(BossProgressBar bossHealthBar, GUIComponent container, float alpha) + static void SetColor(ProgressBar bossHealthBar, GUIComponent container, float alpha) { foreach (var component in container.GetAllChildren()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 1276312d0e..8e9c5d6025 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -526,6 +526,17 @@ private void DrawAttachmentSprite(SpriteBatch spriteBatch, WearableSprite attach else { origin = attachment.Sprite.Origin; + if (spriteEffects.HasFlag(SpriteEffects.FlipHorizontally)) + { + origin.X = attachment.Sprite.size.X - origin.X; + } + if (spriteEffects.HasFlag(SpriteEffects.FlipVertically)) + { + origin.Y = attachment.Sprite.size.Y - origin.Y; + } + //the portrait's origin is forced to 0,0 (presumably for easier drawing on the UI?), see LoadHeadElement + //we need to take that into account here and draw the attachment at where the origin of the "actual" head sprite would be + drawPos += HeadSprite.Origin * scale; } float depth = attachment.Sprite.Depth; if (attachment.InheritLimbDepth) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 5ef1cebff5..dee206a965 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -47,6 +47,7 @@ partial void UpdateNetInput() SelectedCharacter, SelectedItem, SelectedSecondaryItem, + AnimController.TargetMovement, AnimController.Anim); memLocalState.Add(posInfo); @@ -56,7 +57,7 @@ partial void UpdateNetInput() if (IsKeyDown(InputType.Right)) newInput |= InputNetFlags.Right; if (IsKeyDown(InputType.Up)) newInput |= InputNetFlags.Up; if (IsKeyDown(InputType.Down)) newInput |= InputNetFlags.Down; - if (IsKeyDown(InputType.Run)) newInput |= InputNetFlags.Run; + if (IsKeyDown(InputType.Run) || ToggleRun) newInput |= InputNetFlags.Run; if (IsKeyDown(InputType.Crouch)) newInput |= InputNetFlags.Crouch; if (IsKeyHit(InputType.Select)) newInput |= InputNetFlags.Select; //TODO: clean up the way this input is registered if (IsKeyHit(InputType.Deselect)) newInput |= InputNetFlags.Deselect; @@ -68,7 +69,7 @@ partial void UpdateNetInput() if (IsKeyDown(InputType.Attack)) newInput |= InputNetFlags.Attack; if (IsKeyDown(InputType.Ragdoll)) newInput |= InputNetFlags.Ragdoll; - if (AnimController.TargetDir == Direction.Left) newInput |= InputNetFlags.FacingLeft; + if (AnimController.Dir < 0) newInput |= InputNetFlags.FacingLeft; Vector2 relativeCursorPos = cursorPosition - AimRefPosition; relativeCursorPos.Normalize(); @@ -262,6 +263,11 @@ public void ClientReadPosition(IReadMessage msg, float sendingTime) msg.ReadRangedSingle(-MaxVel, MaxVel, 12)); linearVelocity = NetConfig.Quantize(linearVelocity, -MaxVel, MaxVel, 12); + Vector2 targetMovement = new Vector2( + msg.ReadRangedSingle(-Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12), + msg.ReadRangedSingle(-Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12)); + targetMovement = NetConfig.Quantize(targetMovement, -Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12); + bool fixedRotation = msg.ReadBoolean(); float? rotation = null; float? angularVelocity = null; @@ -273,6 +279,8 @@ public void ClientReadPosition(IReadMessage msg, float sendingTime) angularVelocity = NetConfig.Quantize(angularVelocity.Value, -MaxAngularVel, MaxAngularVel, 8); } + bool ignorePlatforms = msg.ReadBoolean(); + bool readStatus = msg.ReadBoolean(); if (readStatus) { @@ -294,7 +302,7 @@ public void ClientReadPosition(IReadMessage msg, float sendingTime) { byte happiness = msg.ReadByte(); byte hunger = msg.ReadByte(); - if ((AIController as EnemyAIController)?.PetBehavior is PetBehavior petBehavior) + if (AIController is EnemyAIController { PetBehavior: PetBehavior petBehavior }) { petBehavior.Happiness = (float)happiness / byte.MaxValue * petBehavior.MaxHappiness; petBehavior.Hunger = (float)hunger / byte.MaxValue * petBehavior.MaxHunger; @@ -316,7 +324,7 @@ public void ClientReadPosition(IReadMessage msg, float sendingTime) pos, rotation, networkUpdateID, facingRight ? Direction.Right : Direction.Left, - selectedCharacter, selectedItem, selectedSecondaryItem, animation); + selectedCharacter, selectedItem, selectedSecondaryItem, targetMovement, animation, ignorePlatforms); while (index < memState.Count && NetIdUtils.IdMoreRecent(posInfo.ID, memState[index].ID)) index++; @@ -328,7 +336,7 @@ public void ClientReadPosition(IReadMessage msg, float sendingTime) pos, rotation, linearVelocity, angularVelocity, sendingTime, facingRight ? Direction.Right : Direction.Left, - selectedCharacter, selectedItem, selectedSecondaryItem, animation); + selectedCharacter, selectedItem, selectedSecondaryItem, targetMovement, animation, ignorePlatforms); while (index < memState.Count && posInfo.Timestamp > memState[index].Timestamp) index++; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs index 77672ac7e3..c353f07be5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs @@ -180,7 +180,7 @@ private void UpdateFakeBroken(float deltaTime) fakeBrokenTimer -= deltaTime; if (fakeBrokenTimer > 0.0f) { return; } - foreach (Item item in Item.ItemList) + foreach (Item item in Item.RepairableItems) { var repairable = item.GetComponent(); if (repairable == null) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 02af592fea..2500b7eded 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -1401,7 +1401,9 @@ private void CreateRecommendedTreatments() GetSuitableTreatments(treatmentSuitability, user: Character.Controlled, ignoreHiddenAfflictions: true, - limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex)); + limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex), + checkTreatmentSuggestionThreshold: true, + checkTreatmentThreshold: false); foreach (Identifier treatment in treatmentSuitability.Keys.ToList()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 2542cdb116..cea46ff0f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -137,7 +137,16 @@ public DeformableSprite DeformSprite { get { - var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.Exclusive && c.IsActive && c.DeformableSprite != null); + // Performance-sensitive, hence implemented without Linq. + ConditionalSprite conditionalSprite = null; + foreach (ConditionalSprite cs in ConditionalSprites) + { + if (cs.Exclusive && cs.IsActive && cs.DeformableSprite != null) + { + conditionalSprite = cs; + break; + } + } if (conditionalSprite != null) { return conditionalSprite.DeformableSprite; @@ -155,7 +164,16 @@ public Sprite ActiveSprite { get { - var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.Exclusive && c.IsActive && c.ActiveSprite != null); + // Performance-sensitive, hence implemented without Linq. + ConditionalSprite conditionalSprite = null; + foreach (ConditionalSprite cs in ConditionalSprites) + { + if (cs.Exclusive && cs.IsActive && cs.ActiveSprite != null) + { + conditionalSprite = cs; + break; + } + } if (conditionalSprite != null) { return conditionalSprite.ActiveSprite; @@ -483,9 +501,20 @@ private string GetSpritePath(ContentXElement element, SpriteParams spriteParams, { if (spriteParams != null) { - //1. check if the variant file redefines the texture - ContentPath texturePath = character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage); - //2. check if the base prefab defines the texture + ContentPath texturePath; + //1. check if the limb defines the texture directly + var definedTexturePath = element?.GetAttributeContentPath("texture"); + if (!definedTexturePath.IsNullOrEmpty()) + { + texturePath = definedTexturePath; + } + else + { + //2. check if the character file defines the texture directly + texturePath = character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage); + } + + //3. check if the base prefab defines the texture if (texturePath.IsNullOrEmpty() && !character.Prefab.VariantOf.IsEmpty) { Identifier speciesName = character.GetBaseCharacterSpeciesName(); @@ -495,7 +524,7 @@ private string GetSpritePath(ContentXElement element, SpriteParams spriteParams, texturePath = parentRagdollParams.OriginalElement?.GetAttributeContentPath("texture"); } - //3. "default case", get the texture from this character's XML + //4. "default case", get the texture from this character's XML texturePath ??= ContentPath.FromRaw(spriteParams.Element.ContentPackage ?? character.Prefab.ContentPackage, spriteParams.GetTexturePath()); path = GetSpritePath(texturePath); } @@ -753,9 +782,29 @@ public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = nul float herpesStrength = character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.SpaceHerpesType); - bool hideLimb = Hide || - OtherWearables.Any(w => w.HideLimb) || - WearingItems.Any(w => w.HideLimb); + bool hideLimb = ShouldHideLimb(this); + if (!hideLimb && Params.InheritHiding != LimbType.None) + { + if (character.AnimController.GetLimb(Params.InheritHiding) is Limb otherLimb) + { + hideLimb = ShouldHideLimb(otherLimb); + } + } + + static bool ShouldHideLimb(Limb limb) + { + if (limb.Hide) { return true; } + // Performance-sensitive code -> implemented without Linq + foreach (var wearable in limb.OtherWearables) + { + if (wearable.HideLimb) { return true; } + } + foreach (var wearable in limb.WearingItems) + { + if (wearable.HideLimb) { return true; } + } + return false; + } bool drawHuskSprite = HuskSprite != null && !wearableTypesToHide.Contains(WearableType.Husk); @@ -887,28 +936,28 @@ public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = nul } depthStep += step; } - foreach (WearableSprite wearable in OtherWearables) + if (!hideLimb) { - if (wearable.Type == WearableType.Husk) { continue; } - if (wearableTypesToHide.Contains(wearable.Type)) + foreach (WearableSprite wearable in OtherWearables) { - if (wearable.Type == WearableType.Hair) + if (wearable.Type == WearableType.Husk) { continue; } + if (wearableTypesToHide.Contains(wearable.Type)) { - if (HairWithHatSprite != null && !hideLimb) + // Draws the short hair + if (wearable.Type == WearableType.Hair) { - DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); - depthStep += step; - continue; + if (HairWithHatSprite != null) + { + DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); + depthStep += step; + } } - } - else - { continue; } + DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); + //if there are multiple sprites on this limb, make the successive ones be drawn in front + depthStep += step; } - DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); - //if there are multiple sprites on this limb, make the successive ones be drawn in front - depthStep += step; } } foreach (WearableSprite wearable in WearingItems) @@ -967,7 +1016,7 @@ public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = nul new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), colorWithoutTint * damageOverlayStrength, activeSprite.Origin, -body.DrawRotation, - Scale, spriteEffect, activeSprite.Depth - depthStep * Math.Max(1, WearingItems.Count * 2)); // Multiply by 2 to get rid of z-fighting with some clothing combos + Scale * TextureScale, spriteEffect, activeSprite.Depth - depthStep * Math.Max(1, WearingItems.Count * 2)); // Multiply by 2 to get rid of z-fighting with some clothing combos } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 002104b6ae..f26a0fd627 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -684,7 +684,7 @@ async Task gameOwnershipTokenTest() if (args.Length > 1) { string forceThalamusArg = args[1]; - if (Enum.TryParse(forceThalamusArg, out LevelData.ThalamusSpawn result)) + if (Enum.TryParse(forceThalamusArg, ignoreCase: true, out LevelData.ThalamusSpawn result)) { NewMessage($"Setting ThalamusSpawn to: {result}", color: Color.Yellow); LevelData.ForceThalamus = result; @@ -2530,6 +2530,18 @@ static Dictionary getContent(XElement element) })); #if DEBUG + + commands.Add(new Command("unlockachievement", "unlockachievement [identifier]: Unlocks the specified achievement.", (string[] args) => + { + if (args.Length < 1) + { + ThrowError("Please specify the achievement to unlock."); + return; + } + NewMessage($"Unlocked \"{args[0]}\"."); + AchievementManager.UnlockAchievement(args[0].ToIdentifier()); + }, isCheat: true)); + commands.Add(new Command("deathprompt", "Shows the death prompt for testing purposes.", (string[] args) => { DeathPrompt.Create(delay: 1.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index efbdb6712e..3a7988c212 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -36,7 +36,8 @@ private bool IsBlockedByAnotherConversation(IEnumerable _, float duratio { return lastActiveAction != null && - lastActiveAction.ParentEvent != ParentEvent && + !lastActiveAction.ParentEvent.IsFinished && + lastActiveAction.ParentEvent != ParentEvent && Timing.TotalTime < lastActiveAction.lastActiveTime + duration; } @@ -101,6 +102,7 @@ private static void CreateDialog(LocalizedString text, Character speaker, IEnume conversationList.BarScroll = (prevSize - conversationList.Content.Rect.Height) / (conversationList.TotalSize - conversationList.Content.Rect.Height); conversationList.ScrollToEnd(duration: 0.5f); lastMessageBox.SetBackgroundIcon(eventSprite); + MarkMessageBoxAsLastAction(lastMessageBox); return; } } @@ -123,16 +125,7 @@ private static void CreateDialog(LocalizedString text, Character speaker, IEnume messageBox.AutoClose = false; GUIStyle.Apply(messageBox.InnerFrame, "DialogBox"); - if (actionInstance != null) - { - lastActiveAction = actionInstance; - actionInstance.lastActiveTime = Timing.TotalTime; - actionInstance.dialogBox = messageBox; - } - else - { - messageBox.UserData = new Pair("ConversationAction", actionId.Value); - } + MarkMessageBoxAsLastAction(messageBox); int padding = GUI.IntScale(16); @@ -155,6 +148,20 @@ private static void CreateDialog(LocalizedString text, Character speaker, IEnume }; shadow.SetAsFirstChild(); + void MarkMessageBoxAsLastAction(GUIMessageBox messageBox) + { + if (actionInstance != null) + { + lastActiveAction = actionInstance; + actionInstance.lastActiveTime = Timing.TotalTime; + actionInstance.dialogBox = messageBox; + } + else + { + messageBox.UserData = new Pair("ConversationAction", actionId.Value); + } + } + static void RecalculateLastMessage(GUIListBox conversationList, bool append) { if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs index 0db59ddea0..ce75680fb1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventLog.cs @@ -48,6 +48,7 @@ public void CreateEventLogUI(GUIComponent parent, TraitorManager.TraitorResults? textContent, difficultyIconCount, icon, GUIStyle.Red, + difficultyTooltipText: null, out GUIImage missionIcon); if (traitorResults != null && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 9081d1751c..0d9e499cf1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -61,6 +61,16 @@ public virtual RichString GetMissionRewardText(Submarine sub) return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖" + rewardText + "‖end‖")); } + public RichString GetDifficultyToolTipText() + { + // 2 skulls give +10% XP, 3 skulls +20% XP and 4 skulls give +30% XP. + float xpBonusMultiplier = CalculateDifficultyXPMultiplier(); + float xpBonusPercentage = (xpBonusMultiplier - 1f) * 100f; + int bonusRounded = (int)Math.Round(xpBonusPercentage); + LocalizedString tooltipText = TextManager.GetWithVariable(tag: "missiondifficultyxpbonustooltip", varName: "[bonus]", value: bonusRounded.ToString()); + return RichString.Rich(tooltipText); + } + public RichString GetReputationRewardText() { List reputationRewardTexts = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index 0bddcc04f3..e69cbb8879 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -49,7 +49,8 @@ public Color HudIconColor { return hudIconColor ?? IconColor; } - } + } + public Color ProgressBarColor { get; private set; } private Sprite hudIcon; private Color? hudIconColor; @@ -90,6 +91,7 @@ partial void InitProjSpecific(ContentXElement element) } this.portraits = portraits.ToImmutableArray(); overrideMusicOnState = overrideMusic.ToImmutableDictionary(); + ProgressBarColor = element.GetAttributeColor(nameof(ProgressBarColor), GUIStyle.Blue); } public Identifier GetOverrideMusicType(int state) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs index 291de47598..7eb69a5e66 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs @@ -7,20 +7,7 @@ namespace Barotrauma { partial class ScanMission : Mission { - public override IEnumerable HudIconTargets - { - get - { - if (State == 0) - { - return scanTargets.Where(kvp => !kvp.Value).Select(kvp => kvp.Key); - } - else - { - return Enumerable.Empty(); - } - } - } + public override IEnumerable HudIconTargets => scanTargets.Where(kvp => !kvp.Value).Select(kvp => kvp.Key); public override bool DisplayAsCompleted => false; public override bool DisplayAsFailed => false; @@ -62,7 +49,7 @@ private void ClientReadScanTargetStatus(IReadMessage msg) ushort id = msg.ReadUInt16(); bool scanned = msg.ReadBoolean(); Entity entity = Entity.FindEntityByID(id); - if (!(entity is WayPoint wayPoint)) + if (entity is not WayPoint wayPoint) { string errorMsg = $"Failed to find a waypoint in ScanMission.ClientReadScanTargetStatus. Entity {id} was {(entity?.ToString() ?? null)}"; DebugConsole.ThrowError(errorMsg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index 5a41e279f8..40532c06a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -419,8 +419,7 @@ private void DynamicRenderAtlas(GraphicsDevice gd, IEnumerable characters, if (anyChanges) { textures[^1].SetData(currentDynamicPixelBuffer); } } } - - // TODO: refactor this further + private void HandleNewLineAndAlignment( string text, in Vector2 advanceUnit, @@ -435,23 +434,29 @@ private void HandleNewLineAndAlignment( out uint charIndex, out bool shouldContinue) { - if ((alignment.HasFlag(Alignment.CenterX) || alignment.HasFlag(Alignment.Right)) && (lineWidth < 0.0f || text[i] == '\n')) + if (lineWidth < 0.0f || text[i] == '\n') { - int startIndex = lineWidth < 0.0f ? i : (i + 1); - lineWidth = 0.0f; - for (int j = startIndex; j < text.Length; j++) + // Use bitwise operations instead of HasFlag or HasAnyFlag to avoid boxing, as this is performance-sensitive code. + bool isHorizontallyCentered = (alignment & Alignment.CenterX) == Alignment.CenterX; + bool isAlignedToRight = (alignment & Alignment.Right) == Alignment.Right; + if (isHorizontallyCentered || isAlignedToRight) { - if (text[j] == '\n') { break; } - uint chrIndex = text[j]; + int startIndex = lineWidth < 0.0f ? i : (i + 1); + lineWidth = 0.0f; + for (int j = startIndex; j < text.Length; j++) + { + if (text[j] == '\n') { break; } + uint chrIndex = text[j]; - var gd2 = GetGlyphData(chrIndex); - lineWidth += gd2.Advance; - } - currentLineOffset = -lineWidth * advanceUnit * scale.X; - if (alignment.HasFlag(Alignment.CenterX)) { currentLineOffset *= 0.5f; } + var gd2 = GetGlyphData(chrIndex); + lineWidth += gd2.Advance; + } + currentLineOffset = -lineWidth * advanceUnit * scale.X; + if (isHorizontallyCentered) { currentLineOffset *= 0.5f; } - currentLineOffset.X = MathF.Round(currentLineOffset.X); - currentLineOffset.Y = MathF.Round(currentLineOffset.Y); + currentLineOffset.X = MathF.Round(currentLineOffset.X); + currentLineOffset.Y = MathF.Round(currentLineOffset.Y); + } } if (text[i] == '\n') { @@ -493,7 +498,7 @@ public void DrawString(SpriteBatch sb, string text, Vector2 position, Color colo int lineNum = 0; Vector2 currentPos = position; - Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)); + Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2(MathF.Cos(rotation), MathF.Sin(rotation)); for (int i = 0; i < text.Length; i++) { HandleNewLineAndAlignment(text, advanceUnit, position, scale, alignment, i, @@ -504,7 +509,7 @@ public void DrawString(SpriteBatch sb, string text, Vector2 position, Color colo GlyphData gd = GetGlyphData(charIndex); if (gd.TexIndex >= 0) { - if (gd.TexIndex < 0 || gd.TexIndex >= textures.Count) + if (gd.TexIndex >= textures.Count) { throw new ArgumentOutOfRangeException($"Error while rendering text. Texture index was out of range. Text: {text}, char: {charIndex} index: {gd.TexIndex}, texture count: {textures.Count}"); } @@ -542,6 +547,11 @@ public void DrawString(SpriteBatch sb, string text, Vector2 position, Color colo DynamicRenderAtlas(graphicsDevice, text); } + quadVertices[0].Color = color; + quadVertices[1].Color = color; + quadVertices[2].Color = color; + quadVertices[3].Color = color; + Vector2 currentPos = position; for (int i = 0; i < text.Length; i++) { @@ -558,26 +568,33 @@ public void DrawString(SpriteBatch sb, string text, Vector2 position, Color colo if (gd.TexIndex >= 0) { float halfCharHeight = gd.TexCoords.Height * 0.5f; - float slantStrength = 0.35f; - float topItalicOffset = italics ? ((halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f : 0.0f; - float bottomItalicOffset = italics ? ((-halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f : 0.0f; - + const float slantStrength = 0.35f; + float topItalicOffset = 0.0f; + float bottomItalicOffset = 0.0f; + if (italics) + { + topItalicOffset = ((halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f; + bottomItalicOffset = ((-halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f; + } + Texture2D tex = textures[gd.TexIndex]; + + float left = (float)gd.TexCoords.Left / tex.Width; + float bottom = (float)gd.TexCoords.Bottom / tex.Height; + float top = (float)gd.TexCoords.Top / tex.Height; + float right = (float)gd.TexCoords.Right / tex.Width; + quadVertices[0].Position = new Vector3(currentPos + gd.DrawOffset + (bottomItalicOffset, gd.TexCoords.Height), 0.0f); - quadVertices[0].TextureCoordinate = ((float)gd.TexCoords.Left / tex.Width, (float)gd.TexCoords.Bottom / tex.Height); - quadVertices[0].Color = color; + quadVertices[0].TextureCoordinate = new Vector2(left, bottom); quadVertices[1].Position = new Vector3(currentPos + gd.DrawOffset + (topItalicOffset, 0.0f), 0.0f); - quadVertices[1].TextureCoordinate = ((float)gd.TexCoords.Left / tex.Width, (float)gd.TexCoords.Top / tex.Height); - quadVertices[1].Color = color; + quadVertices[1].TextureCoordinate = new Vector2(left, top); quadVertices[2].Position = new Vector3(currentPos + gd.DrawOffset + (gd.TexCoords.Width + bottomItalicOffset, gd.TexCoords.Height), 0.0f); - quadVertices[2].TextureCoordinate = ((float)gd.TexCoords.Right / tex.Width, (float)gd.TexCoords.Bottom / tex.Height); - quadVertices[2].Color = color; + quadVertices[2].TextureCoordinate = new Vector2(right, bottom); quadVertices[3].Position = new Vector3(currentPos + gd.DrawOffset + (gd.TexCoords.Width + topItalicOffset, 0.0f), 0.0f); - quadVertices[3].TextureCoordinate = ((float)gd.TexCoords.Right / tex.Width, (float)gd.TexCoords.Top / tex.Height); - quadVertices[3].Color = color; + quadVertices[3].TextureCoordinate = new Vector2(right, top); sb.Draw(tex, quadVertices, 0.0f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 8f58c3c113..25150f8a98 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -246,6 +246,57 @@ public Vector2 Center { get { return new Vector2(Rect.Center.X, Rect.Center.Y); } } + + /// + /// Clamps the component's rect position to the specified area. Does not resize the component. + /// + /// Area to contain the Rect of this component to + public void ClampToArea(Rectangle clampArea) + { + Rectangle componentRect = Rect; + + int x = componentRect.X; + int y = componentRect.Y; + + // Adjust the X position + if (componentRect.Width <= clampArea.Width) + { + if (componentRect.Left < clampArea.Left) + { + x = clampArea.Left; + } + else if (componentRect.Right > clampArea.Right) + { + x = clampArea.Right - componentRect.Width; + } + } + else + { + // Component is wider than clamp area, osition it to overlap as much as possible + x = clampArea.Left - (componentRect.Width - clampArea.Width) / 2; + } + + // Adjust the Y position + if (componentRect.Height <= clampArea.Height) + { + if (componentRect.Top < clampArea.Top) + { + y = clampArea.Top; + } + else if (componentRect.Bottom > clampArea.Bottom) + { + y = clampArea.Bottom - componentRect.Height; + } + } + else + { + // Component is taller than clamp area, osition it to overlap as much as possible + y = clampArea.Top - (componentRect.Height - clampArea.Height) / 2; + } + + Point moveAmount = new Point(x - componentRect.X, y - componentRect.Y); + RectTransform.ScreenSpaceOffset += moveAmount; + } protected Rectangle ClampRect(Rectangle r) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 372f1d4991..202d44da68 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -1562,7 +1562,7 @@ private GUIComponent CreateItemFrame(PurchasedItem pi, GUIComponent parentCompon bool locationHasDealOnItem = isSellingRelatedList ? ActiveStore.RequestedGoods.Contains(pi.ItemPrefab) : ActiveStore.DailySpecials.Contains(pi.ItemPrefab); GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), nameAndQuantityGroup.RectTransform), - pi.ItemPrefab.Name, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft) + RichString.Rich(pi.ItemPrefab.Name), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft) { CanBeFocused = false, Shadow = locationHasDealOnItem, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 8d7625d3c3..01ffb44d3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -902,7 +902,7 @@ private int GetTeamIndex(Client client) } } - return 0; + return teamIDs.IndexOf(client.TeamID); } private void CreateWalletCrewFrame(Character character, GUILayoutGroup paddedFrame) @@ -1694,6 +1694,7 @@ private void CreateMissionInfo(GUIFrame infoFrame) textContent, mission.Difficulty ?? 0, mission.Prefab.Icon, mission.Prefab.IconColor, + mission.GetDifficultyToolTipText(), out GUIImage missionIcon); if (missionIcon != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index b7b9109650..017a60625d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -299,7 +299,7 @@ private void CreateStatPanel(GUIComponent parent, CharacterInfo info) } ImmutableHashSet talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(static e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)).ToImmutableHashSet(); - if (talentsOutsideTree.Any()) + if (talentsOutsideTree.Any(static t => t != null && !t.IsHiddenExtraTalent)) { //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), nameLayout.RectTransform), style: null); @@ -324,6 +324,7 @@ private void CreateStatPanel(GUIComponent parent, CharacterInfo info) foreach (var extraTalent in talentsOutsideTree) { if (extraTalent is null) { continue; } + if (extraTalent.IsHiddenExtraTalent) { continue; } GUIImage talentImg = new GUIImage(new RectTransform(Vector2.One, extraTalentList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite: extraTalent.Icon, scaleToFit: true) { ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(extraTalent.Description.Value)), @@ -440,7 +441,12 @@ GUIListBox GetSpecializationList() private void CreateTalentResetPopup(GUIComponent parent) { - bool hasResetTalentsBefore = character?.Info.TalentResetCount > 0; + int talentResetCount = 0; + if (character?.Info != null) + { + talentResetCount = Math.Min(character.Info.TalentResetCount, character.Info.GetCurrentLevel()); + } + bool hasResetTalentsBefore = talentResetCount > 0; var bgBlocker = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, anchor: Anchor.Center), style: "GUIBackgroundBlocker") { IgnoreLayoutGroups = true @@ -455,7 +461,8 @@ private void CreateTalentResetPopup(GUIComponent parent) if (hasResetTalentsBefore) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), popupLayout.RectTransform), TextManager.Get("talentresetpromptwarning"), wrap: true) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), popupLayout.RectTransform), + TextManager.GetWithVariable("talentresetpromptwarning", "[count]", talentResetCount.ToString()), wrap: true) { TextColor = GUIStyle.Red }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 23b6ff533b..85c5d10d70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -313,6 +313,8 @@ static void updateConfig() GameSettings.SetCurrentConfig(config); } + int display = GameSettings.CurrentConfig.Graphics.Display; + GraphicsWidth = GameSettings.CurrentConfig.Graphics.Width; GraphicsHeight = GameSettings.CurrentConfig.Graphics.Height; @@ -340,7 +342,7 @@ static void updateConfig() GraphicsDeviceManager.PreferredBackBufferFormat = SurfaceFormat.Color; GraphicsDeviceManager.PreferMultiSampling = false; GraphicsDeviceManager.SynchronizeWithVerticalRetrace = GameSettings.CurrentConfig.Graphics.VSync; - SetWindowMode(GameSettings.CurrentConfig.Graphics.DisplayMode); + SetWindowMode(GameSettings.CurrentConfig.Graphics.DisplayMode, display); defaultViewport = new Viewport(0, 0, GraphicsWidth, GraphicsHeight); @@ -353,8 +355,17 @@ static void updateConfig() ResolutionChanged?.Invoke(); } - public void SetWindowMode(WindowMode windowMode) + public void SetWindowMode(WindowMode windowMode, int display) { + // We can't move the monitor while the window is fullscreen because of a restriction in SDL2, so as a workaround we switch to windowed mode first + var prevDisplayMode = WindowMode; + if (Window.TargetDisplay != display && prevDisplayMode != WindowMode.Windowed) + { + GraphicsDeviceManager.IsFullScreen = false; + GraphicsDeviceManager.ApplyChanges(); + } + Window.TargetDisplay = display; + WindowMode = windowMode; GraphicsDeviceManager.HardwareModeSwitch = windowMode != WindowMode.BorderlessWindowed; GraphicsDeviceManager.IsFullScreen = windowMode == WindowMode.Fullscreen || windowMode == WindowMode.BorderlessWindowed; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index cbfbba80e8..44b8f8234b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -1345,6 +1345,7 @@ private void SelectCharacter(Character character) { if (ConversationAction.IsDialogOpen) { return; } if (!AllowCharacterSwitch) { return; } + if (character == null || character.Removed) { return; } //make the previously selected character wait in place for some time //(so they don't immediately start idling and walking away from their station) var aiController = Character.Controlled?.AIController; @@ -1373,7 +1374,7 @@ private int TryAdjustIndex(int amount) if (index > lastIndex) { index = 0; } if (index < 0) { index = lastIndex; } - if ((crewList.Content.GetChild(index)?.UserData as Character)?.IsOnPlayerTeam ?? false) + if (crewList.Content.GetChild(index)?.UserData is Character { IsOnPlayerTeam: true, Removed: false }) { return index; } @@ -1668,7 +1669,7 @@ void CheckIfClosest(GUIComponent comp) { crewArea.Visible = characters.Count > 0 && CharacterHealth.OpenHealthWindow == null; - var myTeam = Character.Controlled?.TeamID ?? GameMain.Client?.MyClient?.TeamID; + CharacterTeamType myTeam = Character.Controlled?.TeamID ?? GameMain.Client?.MyClient?.TeamID ?? CharacterTeamType.Team1; if (GameMain.GameSession?.GameMode is PvPMode) { var team1Text = crewArea.GetChildByUserData(CharacterTeamType.Team1); @@ -1690,7 +1691,7 @@ void CheckIfClosest(GUIComponent comp) continue; } - characterComponent.Visible = Character.Controlled == null || myTeam == character.TeamID; + characterComponent.Visible = myTeam == character.TeamID; if (character.TeamID == CharacterTeamType.FriendlyNPC && Character.Controlled != null && (character.CurrentHull == Character.Controlled.CurrentHull || Vector2.DistanceSquared(Character.Controlled.WorldPosition, character.WorldPosition) < 500.0f * 500.0f)) { @@ -2055,7 +2056,7 @@ private void CreateCommandUI(Entity entityContext = null, bool forceContextual = // Character context works differently to others as we still use the "basic" command interface, // but the order will be automatically assigned to this character isContextual = forceContextual; - if (entityContext is Character character) + if (entityContext is Character { Info: not null } character) { characterContext = character; itemContext = null; @@ -2670,9 +2671,9 @@ static bool ShouldDelegateOrderId(Identifier orderIdentifier) bool IsNonDuplicateOrder(Order order) => IsNonDuplicateOrderPrefab(order.Prefab, order.Option); bool IsNonDuplicateOrderPrefab(OrderPrefab orderPrefab, Identifier option = default) { - return characterContext == null || (option.IsEmpty ? + return characterContext == null || (characterContext.CurrentOrders != null && (option.IsEmpty ? characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier) : - characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier && oi.Option == option)); + characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier && oi?.Option == option))); } void AddOrderNodeWithIdentifier(string identifier) { @@ -3045,7 +3046,7 @@ private void CreateMinimapNodes(Order order, Submarine submarine, List mat { optionElement.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); } - var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o != null && + var colorMultiplier = characters.Any(c => c.CurrentOrders != null && c.CurrentOrders.Any(o => o != null && o.Identifier == userData.Order.Identifier && o.TargetEntity == userData.Order.TargetEntity)) ? 0.5f : 1f; CreateNodeIcon(Vector2.One, optionElement.RectTransform, item.Prefab.MinimapIcon ?? order.SymbolSprite, order.Color * colorMultiplier, tooltip: item.Name); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 0ab745b7b6..e672460da2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -251,6 +251,7 @@ public GUIFrame CreateSummaryFrame(GameSession gameSession, string endMessage, C textContent, mission.Difficulty ?? 0, mission.Prefab.Icon, mission.Prefab.IconColor, + mission.GetDifficultyToolTipText(), out GUIImage missionIcon); if (selectedMissions.Contains(mission)) @@ -442,13 +443,14 @@ private static GUIComponent CreateTraitorInfoPanel(GUIComponent parent, TraitorM textContent, difficultyIconCount: 0, icon, GUIStyle.Red, + difficultyTooltipText: null, out GUIImage missionIcon); UpdateMissionStateIcon(traitorResults.VotedCorrectTraitor, missionIcon, iconAnimDelay); return content; } public static GUIComponent CreateMissionEntry(GUIComponent parent, LocalizedString header, List textContent, int difficultyIconCount, - Sprite icon, Color iconColor, out GUIImage missionIcon) + Sprite icon, Color iconColor, RichString difficultyTooltipText, out GUIImage missionIcon) { int spacing = GUI.IntScale(5); @@ -499,7 +501,8 @@ public static GUIComponent CreateMissionEntry(GUIComponent parent, LocalizedStri { difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(new Point(missionTextGroup.Rect.Width, defaultLineHeight), parent: missionTextGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { - AbsoluteSpacing = 1 + AbsoluteSpacing = 1, + CanBeFocused = true }; difficultyIndicatorGroup.RectTransform.MinSize = new Point(0, defaultLineHeight); var difficultyColor = Mission.GetDifficultyColor(difficultyIconCount); @@ -507,8 +510,8 @@ public static GUIComponent CreateMissionEntry(GUIComponent parent, LocalizedStri { new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), "DifficultyIndicator", scaleToFit: true) { - CanBeFocused = false, - Color = difficultyColor + Color = difficultyColor, + ToolTip = difficultyTooltipText }; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 5ea8a52656..d492bb83d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -184,6 +184,13 @@ partial void UpdateProjSpecific(float deltaTime) { shakePos = Vector2.Zero; } + if (Character.Controlled is Character character && character.FocusedItem == item) + { + if ((IsFullyOpen || IsFullyClosed) && MathF.Abs(openState - lastOpenState) > 0) + { + CharacterHUD.RecreateHudTexts = true; + } + } } public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs index d078480d50..ac03954d2b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs @@ -14,6 +14,7 @@ partial class GeneticMaterial : ItemComponent public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { + bool mergedMaterialTainted = false; if (!materialName.IsNullOrEmpty() && item.ContainedItems.Count() > 0) { LocalizedString mergedMaterialName = materialName; @@ -22,11 +23,15 @@ public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedStrin var containedMaterial = containedItem.GetComponent(); if (containedMaterial == null) { continue; } mergedMaterialName += ", " + containedMaterial.materialName; + if (containedMaterial.Tainted) + { + mergedMaterialTainted = true; + } } name = name.Replace(materialName, mergedMaterialName); } - if (Tainted) + if (Tainted || mergedMaterialTainted) { name = TextManager.GetWithVariable("entityname.taintedgeneticmaterial", "[geneticmaterialname]", name); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index b24344c848..0d532b2c2e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -71,13 +71,13 @@ public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Co } public override bool ValidateEventData(NetEntityEvent.IData data) - => TryExtractEventData(data, out _); + => TryExtractEventData(data, out _); public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { if (!attachable || body == null) { return; } - var eventData = ExtractEventData(extraData); + var eventData = ExtractEventData(extraData); Vector2 attachPos = eventData.AttachPos; msg.WriteSingle(attachPos.X); @@ -94,7 +94,9 @@ public override void ClientEventRead(IReadMessage msg, float sendingTime) bool shouldBeAttached = msg.ReadBoolean(); Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); UInt16 submarineID = msg.ReadUInt16(); + UInt16 attacherID = msg.ReadUInt16(); Submarine sub = Entity.FindEntityByID(submarineID) as Submarine; + Character attacher = Entity.FindEntityByID(attacherID) as Character; if (shouldBeAttached) { @@ -104,6 +106,8 @@ public override void ClientEventRead(IReadMessage msg, float sendingTime) item.SetTransform(simPosition, 0.0f); item.Submarine = sub; AttachToWall(); + PlaySound(ActionType.OnUse, attacher); + ApplyStatusEffects(ActionType.OnUse, (float)Timing.Step, character: attacher, user: attacher); } } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 6b18db9ba7..a80b76688d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -62,6 +62,10 @@ public bool HasSounds private readonly Dictionary> sounds; private Dictionary soundSelectionModes; + /// + /// Starts the timer for delayed client-side corrections () - in other words, + /// the client will not attempt to read server updates for this component until the timer elapses. + /// protected float correctionTimer; public float IsActiveTimer; @@ -760,7 +764,11 @@ protected void TryCreateDragHandle() /// protected virtual void CreateGUI() { } - //Starts a coroutine that will read the correct state of the component from the NetBuffer when correctionTimer reaches zero. + /// + /// Starts a coroutine that will read the correct state of the component from the NetBuffer when correctionTimer reaches zero. + /// Useful in cases where we a client is constantly adjusting some value, and we don't want state updates from the server to interfere with it + /// (e.g. setting the value back to what a client just set it to, when the client has already modified the value further). + /// protected void StartDelayedCorrection(IReadMessage buffer, float sendingTime, bool waitForMidRoundSync = false) { if (delayedCorrectionCoroutine != null) { CoroutineManager.StopCoroutines(delayedCorrectionCoroutine); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 1d5066db20..a8cb83664a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -372,17 +372,17 @@ private void InitInventoryUIs() outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform; } - private static LocalizedString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe) + private static RichString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe) { if (fabricationRecipe == null) { return ""; } if (fabricationRecipe.Amount > 1) { return TextManager.GetWithVariables("fabricationrecipenamewithamount", - ("[name]", fabricationRecipe.DisplayName), ("[amount]", fabricationRecipe.Amount.ToString())); + ("[name]", RichString.Rich(fabricationRecipe.DisplayName)), ("[amount]", fabricationRecipe.Amount.ToString())); } else { - return fabricationRecipe.DisplayName; + return RichString.Rich(fabricationRecipe.DisplayName); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index d5b8c5224a..17f13953f4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1978,25 +1978,6 @@ void CalculateDistance() 2, GUIStyle.SmallFont); } - protected override void RemoveComponentSpecific() - { - base.RemoveComponentSpecific(); - sonarBlip?.Remove(); - pingCircle?.Remove(); - directionalPingCircle?.Remove(); - screenOverlay?.Remove(); - screenBackground?.Remove(); - lineSprite?.Remove(); - - foreach (var t in targetIcons.Values) - { - t.Item1.Remove(); - } - targetIcons.Clear(); - - MineralClusters = null; - } - public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.WriteBoolean(currentMode == Mode.Active); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 0b551f6bd1..2fe30e4735 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -50,7 +50,7 @@ public void ClientEventRead(IReadMessage msg, float sendingTime) Hull hull = Entity.FindEntityByID(hullID) as Hull; item.Submarine = submarine; item.CurrentHull = hull; - item.body.SetTransform(simPosition, item.body.Rotation); + item.body.SetTransformIgnoreContacts(simPosition, item.body.Rotation); switch (targetType) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index de54014d38..2370fc843c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -12,7 +12,7 @@ partial class CustomInterface private readonly List uiElements = new List(); private GUILayoutGroup uiElementContainer; - private bool readingNetworkEvent; + private bool suppressNetworkEvents; private GUIComponent insufficientPowerWarning; @@ -70,7 +70,7 @@ protected override void CreateGUI() } else { - item.CreateClientEvent(this); + CreateClientEventWithCorrectionDelay(); } }; @@ -100,13 +100,10 @@ protected override void CreateGUI() ValueStep = numberInputStep, OnValueChanged = (ni) => { - if (GameMain.Client == null) + ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue); + if (!suppressNetworkEvents && GameMain.Client != null) { - ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue); - } - else if (!readingNetworkEvent) - { - item.CreateClientEvent(this); + CreateClientEventWithCorrectionDelay(); } } }; @@ -126,13 +123,10 @@ protected override void CreateGUI() ValueStep = numberInputStep, OnValueChanged = (ni) => { - if (GameMain.Client == null) + ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue); + if (!suppressNetworkEvents && GameMain.Client != null) { - ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue); - } - else if (!readingNetworkEvent) - { - item.CreateClientEvent(this); + CreateClientEventWithCorrectionDelay(); } } }; @@ -162,13 +156,10 @@ protected override void CreateGUI() }; tickBox.OnSelected += (tBox) => { - if (GameMain.Client == null) + TickBoxToggled(tBox.UserData as CustomInterfaceElement, tBox.Selected); + if (!suppressNetworkEvents && GameMain.Client != null) { - TickBoxToggled(tBox.UserData as CustomInterfaceElement, tBox.Selected); - } - else if (!readingNetworkEvent) - { - item.CreateClientEvent(this); + CreateClientEventWithCorrectionDelay(); } return true; }; @@ -191,8 +182,10 @@ protected override void CreateGUI() { ButtonClicked(btnElement); } - else if (!readingNetworkEvent) + else if (!suppressNetworkEvents && GameMain.Client != null) { + //don't use CreateClientEventWithCorrectionDelay here, because buttons have no state, + //which means we don't need to worry about server updates interfering with client-side changes to the values in the interface item.CreateClientEvent(this, new EventData(btnElement)); } return true; @@ -215,6 +208,12 @@ protected override void CreateGUI() Visible = false }; } + + void CreateClientEventWithCorrectionDelay() + { + item.CreateClientEvent(this); + correctionTimer = CorrectionDelay; + } } public override void CreateEditingHUD(SerializableEntityEditor editor) @@ -268,7 +267,7 @@ public override void UpdateHUDComponentSpecific(Character character, float delta if (visible) { visibleElementCount++; - if (element.GetValueInterval > 0.0f) + if (element.GetValueInterval > 0.0f && correctionTimer <= 0.0f) { element.GetValueTimer -= deltaTime; if (element.GetValueTimer <= 0.0f) @@ -330,7 +329,7 @@ partial void UpdateLabelsProjSpecific() LocalizedString CreateLabelText(int elementIndex) { - var label = customInterfaceElementList[elementIndex].Label; + string label = customInterfaceElementList[elementIndex].Label; return string.IsNullOrWhiteSpace(label) ? TextManager.GetWithVariable("connection.signaloutx", "[num]", (elementIndex + 1).ToString()) : TextManager.Get(label).Fallback(label); @@ -373,6 +372,9 @@ partial void UpdateSignalsProjSpecific() private void UpdateSignalProjSpecific(GUIComponent uiElement) { if (uiElement.UserData is not CustomInterfaceElement element) { return; } + + suppressNetworkEvents = true; + string signal = element.Signal; if (uiElement is GUITextBox tb) { @@ -384,14 +386,22 @@ private void UpdateSignalProjSpecific(GUIComponent uiElement) { if (ni.InputType == NumberType.Int) { - int.TryParse(signal, out int value); - ni.IntValue = value; + if (int.TryParse(signal, out int value)) + { + ni.IntValue = value; + } + else if (float.TryParse(signal, out float floatValue)) + { + ni.IntValue = (int)MathF.Round(floatValue); + } } } else if (uiElement is GUITickBox tickBox) { tickBox.Selected = signal.Equals("true", StringComparison.OrdinalIgnoreCase); } + + suppressNetworkEvents = false; } public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) @@ -429,38 +439,62 @@ public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = public void ClientEventRead(IReadMessage msg, float sendingTime) { - readingNetworkEvent = true; + int msgStartPos = msg.BitPosition; + suppressNetworkEvents = true; try { + string[] stringValues = new string[customInterfaceElementList.Count]; + bool[] boolValues = new bool[customInterfaceElementList.Count]; + for (int i = 0; i < customInterfaceElementList.Count; i++) + { + var element = customInterfaceElementList[i]; + switch (element.InputType) + { + case CustomInterfaceElement.InputTypeOption.Number: + case CustomInterfaceElement.InputTypeOption.Text: + stringValues[i] = msg.ReadString(); + break; + case CustomInterfaceElement.InputTypeOption.TickBox: + case CustomInterfaceElement.InputTypeOption.Button: + boolValues[i] = msg.ReadBoolean(); + break; + } + } + + if (correctionTimer > 0.0f) + { + int msgLength = msg.BitPosition - msgStartPos; + msg.BitPosition = msgStartPos; + StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime); + return; + } + for (int i = 0; i < customInterfaceElementList.Count; i++) { var element = customInterfaceElementList[i]; switch (element.InputType) { case CustomInterfaceElement.InputTypeOption.Number: - string newValue = msg.ReadString(); switch (element.NumberType) { - case NumberType.Int when int.TryParse(newValue, out int value): + case NumberType.Int when int.TryParse(stringValues[i], out int value): ValueChanged(element, value); break; - case NumberType.Float when TryParseFloatInvariantCulture(newValue, out float value): + case NumberType.Float when TryParseFloatInvariantCulture(stringValues[i], out float value): ValueChanged(element, value); break; } break; case CustomInterfaceElement.InputTypeOption.Text: - string newTextValue = msg.ReadString(); - TextChanged(element, newTextValue); + TextChanged(element, stringValues[i]); break; case CustomInterfaceElement.InputTypeOption.TickBox: - bool tickBoxState = msg.ReadBoolean(); + bool tickBoxState = boolValues[i]; ((GUITickBox)uiElements[i]).Selected = tickBoxState; TickBoxToggled(element, tickBoxState); break; case CustomInterfaceElement.InputTypeOption.Button: - bool buttonState = msg.ReadBoolean(); - if (buttonState) + if (boolValues[i]) { ButtonClicked(element); } @@ -472,7 +506,7 @@ public void ClientEventRead(IReadMessage msg, float sendingTime) } finally { - readingNetworkEvent = false; + suppressNetworkEvents = false; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 21cad1ace7..f890605ae6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -1,6 +1,7 @@ using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -21,13 +22,16 @@ public ClientEventData(string text) private GUIListBox historyBox; private GUITextBlock fillerBlock; private GUITextBox inputBox; + private GUILayoutGroup layoutGroup; private bool shouldSelectInputBox; + private readonly List inputElements = new List(); + partial void InitProjSpecific(XElement element) { float marginMultiplier = element.GetAttributeFloat("marginmultiplier", 1.0f); - var layoutGroup = new GUILayoutGroup(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin.Multiply(marginMultiplier), GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset.Multiply(marginMultiplier) }) + layoutGroup = new GUILayoutGroup(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin.Multiply(marginMultiplier), GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset.Multiply(marginMultiplier) }) { ChildAnchor = Anchor.TopCenter, RelativeSpacing = 0.02f, @@ -39,43 +43,53 @@ partial void InitProjSpecific(XElement element) AutoHideScrollBar = this.AutoHideScrollbar }; - if (!Readonly) - { - CreateFillerBlock(); - - new GUIFrame(new RectTransform(new Vector2(0.9f, 0.01f), layoutGroup.RectTransform), style: "HorizontalLine"); + inputElements.Add(CreateFillerBlock()); + inputElements.Add(new GUIFrame(new RectTransform(new Vector2(0.9f, 0.01f), layoutGroup.RectTransform), style: "HorizontalLine")); - inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: TextColor) + inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: TextColor) + { + MaxTextLength = MaxMessageLength, + OverflowClip = true, + OnEnterPressed = (GUITextBox textBox, string text) => { - MaxTextLength = MaxMessageLength, - OverflowClip = true, - OnEnterPressed = (GUITextBox textBox, string text) => + if (GameMain.NetworkMember == null) { - if (GameMain.NetworkMember == null) - { - SendOutput(text); - } - else - { - item.CreateClientEvent(this, new ClientEventData(text)); - } - textBox.Text = string.Empty; - return true; + SendOutput(text); } - }; - } + else + { + item.CreateClientEvent(this, new ClientEventData(text)); + } + textBox.Text = string.Empty; + return true; + } + }; + inputElements.Add(inputBox); + RefreshInputElements(); + } - layoutGroup.Recalculate(); + /// + /// Refreshes the visibility of the input box and the layout of the UI depending on whether the terminal is readonly or not. + /// + private void RefreshInputElements() + { + foreach (var inputElement in inputElements) + { + inputElement.Visible = !_readonly; + inputElement.IgnoreLayoutGroups = !inputElement.Visible; + } + layoutGroup?.Recalculate(); } // Create fillerBlock to cover historyBox so new values appear at the bottom of historyBox // This could be removed if GUIListBox supported aligning its children - public void CreateFillerBlock() + public GUIComponent CreateFillerBlock() { fillerBlock = new GUITextBlock(new RectTransform(new Vector2(1, 1), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), string.Empty) { CanBeFocused = false }; + return fillerBlock; } private void SendOutput(string input) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index a7c52a99fd..50707aa34d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -199,6 +199,8 @@ public void Draw(SpriteBatch spriteBatch, bool editing, Vector2 offset, float it return; } + if (Width * wireSprite.size.Y * Screen.Selected.Cam.Zoom < 1.0f) { return; } + Vector2 drawOffset = GetDrawOffset() + offset; float baseDepth = UseSpriteDepth ? item.SpriteDepth : wireSprite.Depth; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index f37ca46574..078f49ebd5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -282,7 +282,7 @@ private void DrawCharacterInfo(SpriteBatch spriteBatch, Character target, float } else { - if (!target.CustomInteractHUDText.IsNullOrEmpty() && target.AllowCustomInteract) + if (target.ShouldShowCustomInteractText) { texts.Add(target.CustomInteractHUDText); textColors.Add(GUIStyle.Green); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 57ae410338..0759cc6034 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1552,10 +1552,19 @@ private void SetHUDLayout(bool ignoreLocking = false) debugInitialHudPositions.Clear(); foreach (ItemComponent ic in activeHUDs) { - if (ic.GuiFrame == null || ic.AllowUIOverlap || ic.GetLinkUIToComponent() != null) { continue; } - if (!ignoreLocking && ic.LockGuiFramePosition) { continue; } - //if the frame covers nearly all of the screen, don't trying to prevent overlaps because it'd fail anyway - if (ic.GuiFrame.Rect.Width >= GameMain.GraphicsWidth * 0.9f && ic.GuiFrame.Rect.Height >= GameMain.GraphicsHeight * 0.9f) { continue; } + if (ic.GuiFrame == null || ic.GetLinkUIToComponent() != null) { continue; } + + bool nearlyCoversScreen = ic.GuiFrame.Rect.Width >= GameMain.GraphicsWidth * 0.9f && + ic.GuiFrame.Rect.Height >= GameMain.GraphicsHeight * 0.9f; + + // when we are not using overlap prevention, we still need to clamp the frame to the screen area to + // prevent frames becoming inaccessible outside the screen for example after a resolution change + if (ic.AllowUIOverlap || (!ignoreLocking && ic.LockGuiFramePosition) || nearlyCoversScreen) + { + ic.GuiFrame.ClampToArea(new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight)); + continue; + } + ic.GuiFrame.RectTransform.ScreenSpaceOffset = ic.GuiFrameOffset; elementsToMove.Add(ic.GuiFrame); debugInitialHudPositions.Add(ic.GuiFrame.Rect); @@ -2281,7 +2290,13 @@ public void CreateClientEvent(T ic, ItemComponent.IEventData extraData) where 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)) { + string errorMsg = + $"Client-side component event creation for the item \"{Prefab.Identifier}\" failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false. " + + $"Data: {extraData?.GetType().ToString() ?? "null"}"; + GameAnalyticsManager.AddErrorEventOnce($"Item.CreateClientEvent:ValidateEventData:{Prefab.Identifier}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); + } GameMain.Client.CreateEntityEvent(this, eventData); } @@ -2487,12 +2502,18 @@ public static Item ReadSpawnData(IReadMessage msg, bool spawn = true) if (inventory != null) { - if (inventorySlotIndex >= 0 && inventorySlotIndex < 255 && - inventory.TryPutItem(item, inventorySlotIndex, false, false, null, false)) + if (inventorySlotIndex is >= 0 and < 255) { - return item; + if (!inventory.TryPutItem(item, inventorySlotIndex, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false, ignoreCondition: true) && + inventory.IsSlotEmpty(inventorySlotIndex)) + { + //If the item won't go nicely, force it to the slot. If the server says the item is in the slot, it should go in the slot. + //May happen e.g. when a character is configured to spawn with an item that won't normally go in its inventory slots. + inventory.ForceToSlot(item, index: inventorySlotIndex); + return item; + } } - inventory.TryPutItem(item, null, item.AllowedSlots, false); + inventory.TryPutItem(item, user: null, allowedSlots: item.AllowedSlots, createNetworkEvent: false); } return item; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index f9ac10b382..470094abfe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -96,9 +96,13 @@ void DrawArrow(Hull targetHull, float arrowWidth, float arrowLength, Color clr) new Vector2(Math.Sign(targetHull.Rect.Center.X - rect.Center.X), 0.0f) : new Vector2(0.0f, Math.Sign((rect.Y - rect.Height / 2.0f) - (targetHull.Rect.Y - targetHull.Rect.Height / 2.0f))); - Vector2 arrowPos = new Vector2(WorldRect.Center.X, -(WorldRect.Y - WorldRect.Height / 2)); + Vector2 arrowPos = new Vector2(WorldRect.Center.X, WorldRect.Y - WorldRect.Height / 2); + if (Submarine != null) + { + arrowPos += (Submarine.DrawPosition - Submarine.Position); + } + arrowPos.Y = -arrowPos.Y; arrowPos += new Vector2(dir.X * (WorldRect.Width / 2), dir.Y * (WorldRect.Height / 2)); - bool invalidDir = false; if (dir == Vector2.Zero) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index cdb3fd9d0e..c4fb4318db 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -404,8 +404,8 @@ void drawProgressBar(int height, Point offset, float fillAmount, Color color) //GUI.DrawRectangle(spriteBatch, new Rectangle((int)fs.LastExtinguishPos.X, (int)-fs.LastExtinguishPos.Y, 5,5), Color.Yellow, true); } - - GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f); + float worldSurface = surface + Submarine.DrawPosition.Y; + GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -worldSurface), new Vector2(drawRect.Right, -worldSurface), Color.Cyan * 0.5f); for (int i = 0; i < waveY.Length - 1; i++) { GUI.DrawLine(spriteBatch, @@ -578,8 +578,7 @@ private void UpdateVertices(Camera cam, EntityGrid entityGrid, WaterRenderer ren corners[4] = new Vector3(x, bottom, 0.0f); //bottom right corners[5] = new Vector3(x + width, bottom, 0.0f); - - Vector2[] uvCoords = new Vector2[4]; + for (int n = 0; n < 4; n++) { uvCoords[n] = Vector2.Transform(new Vector2(corners[n].X, -corners[n].Y), transform); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs index 9a071dfb8b..1bb0614148 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs @@ -9,26 +9,56 @@ namespace Barotrauma { static partial class CaveGenerator { - public static List GenerateWallVertices(List triangles, LevelGenerationParams generationParams, float zCoord) + public static List GenerateWallVertices(List triangles, Color color, float zCoord) { - var vertices = new List(); + var vertices = new List(); for (int i = 0; i < triangles.Count; i++) { foreach (Vector2 vertex in triangles[i]) { - Vector2 uvCoords = vertex / generationParams.WallTextureSize; - vertices.Add(new VertexPositionTexture(new Vector3(vertex, zCoord), uvCoords)); + vertices.Add(new VertexPositionColor(new Vector3(vertex, zCoord), color)); } } - return vertices; } - public static List GenerateWallEdgeVertices(List cells, Level level, float zCoord) + /// + /// Generates texture coordinates for the vertices based on their positions + /// + public static VertexPositionColorTexture[] ConvertToTextured(VertexPositionColor[] verts, float textureSize) { - float outWardThickness = level.GenerationParams.WallEdgeExpandOutwardsAmount; + VertexPositionColorTexture[] texturedVerts = new VertexPositionColorTexture[verts.Length]; + for (int i = 0; i < verts.Length; i++) + { + VertexPositionColor vertex = verts[i]; + texturedVerts[i] = new VertexPositionColorTexture(vertex.Position, vertex.Color, textureCoordinate: Vector2.Zero); + } + GenerateTextureCoordinates(texturedVerts, textureSize); + return texturedVerts; + } - List vertices = new List(); + /// + /// Generates texture coordinates for the vertices based on their positions + /// + public static void GenerateTextureCoordinates(VertexPositionColorTexture[] verts, float textureSize) + { + for (int i = 0; i < verts.Length; i++) + { + VertexPositionColorTexture vertex = verts[i]; + Vector2 uvCoords = new Vector2(vertex.Position.X, vertex.Position.Y) / textureSize; + verts[i] = new VertexPositionColorTexture(verts[i].Position, verts[i].Color, uvCoords); + } + } + + public static List GenerateWallEdgeVertices( + List cells, + float expandOutwards, float expandInwards, + Color outerColor, Color innerColor, + Level level, float zCoord, bool preventExpandThroughCell = false) + { + float outWardThickness = expandOutwards; + + List vertices = new List(); foreach (VoronoiCell cell in cells) { Vector2 minVert = cell.Edges[0].Point1; @@ -49,7 +79,10 @@ public static List GenerateWallEdgeVertices(List e != edge && (edge.Point1.NearlyEquals(e.Point1) || edge.Point1.NearlyEquals(e.Point2))); + //the left-side edge on this same cell + GraphEdge myLeftEdge = cell.Edges.Find(e => e != edge && (edge.Point1.NearlyEquals(e.Point1) || edge.Point1.NearlyEquals(e.Point2))); + //the left-side edge on either this cell, or the adjacent one if this is attached to another cell + GraphEdge leftEdge = myLeftEdge; var leftAdjacentCell = leftEdge?.AdjacentCell(cell); if (leftAdjacentCell != null) { @@ -57,7 +90,10 @@ public static List GenerateWallEdgeVertices(List e != edge && (edge.Point2.NearlyEquals(e.Point1) || edge.Point2.NearlyEquals(e.Point2))); + //the right-side edge on this same cell + GraphEdge myRightEdge = cell.Edges.Find(e => e != edge && (edge.Point2.NearlyEquals(e.Point1) || edge.Point2.NearlyEquals(e.Point2))); + //the right-side edge on either this cell, or the adjacent one if this is attached to another cell + GraphEdge rightEdge = myRightEdge; var rightAdjacentCell = rightEdge?.AdjacentCell(cell); if (rightAdjacentCell != null) { @@ -67,18 +103,25 @@ public static List GenerateWallEdgeVertices(List expand in the direction of that edge leftNormal = edge.Point1.NearlyEquals(leftEdge.Point1) ? Vector2.Normalize(leftEdge.Point2 - leftEdge.Point1) : Vector2.Normalize(leftEdge.Point1 - leftEdge.Point2); + //maximum expansion is half of the size of the edge (otherwise the expansions from different sides of the edge could overlap or even extend "through" the cell) + inwardThickness1 = Math.Min(inwardThickness1, leftEdge.Length / 2); } else if (leftEdge != null) { + //use the average of this edge's and the adjacent edge's normals leftNormal = -Vector2.Normalize(edge.GetNormal(cell) + leftEdge.GetNormal(leftAdjacentCell ?? cell)); if (!MathUtils.IsValid(leftNormal)) { leftNormal = -edge.GetNormal(cell); } + //maximum expansion is the length of the adjacent edge (more expansion causes the textures to distort) + inwardThickness1 = Math.Min(Math.Min(inwardThickness1, leftEdge.Length), myLeftEdge.Length); } else { @@ -109,11 +152,13 @@ public static List GenerateWallEdgeVertices(List GenerateWallEdgeVertices(List 0) { continue; } + if (otherEdge != leftEdge) + { + inwardThickness1 = ClampThickness(otherEdge, edge.Point1, leftNormal, inwardThickness1); + } + if (otherEdge != rightEdge) + { + inwardThickness2 = ClampThickness(otherEdge, edge.Point2, rightNormal, inwardThickness2); + } + } + + static float ClampThickness(GraphEdge otherEdge, Vector2 thisPoint, Vector2 thisEdgeNormal, float currThickness) + { + if (MathUtils.GetLineIntersection( + thisPoint, thisPoint + thisEdgeNormal * currThickness, + otherEdge.Point1, otherEdge.Point2, areLinesInfinite: false, out Vector2 intersection1)) + { + return Math.Min(currThickness, Vector2.Distance(thisPoint, intersection1)); + } + return currThickness; + } + } + + //there needs to be some minimum amount of inward thickness, + //if the edge texture doesn't extend inside at all you can see through between the edge texture and the solid part of the cell + inwardThickness1 = Math.Max(inwardThickness1, Math.Min(100.0f, expandInwards)); + inwardThickness2 = Math.Max(inwardThickness2, Math.Min(100.0f, expandInwards)); + for (int i = 0; i < 2; i++) { Vector2[] verts = new Vector2[3]; - VertexPositionTexture[] vertPos = new VertexPositionTexture[3]; + VertexPositionColorTexture[] vertPos = new VertexPositionColorTexture[3]; if (i == 0) { @@ -161,9 +247,9 @@ public static List GenerateWallEdgeVertices(List GenerateWallEdgeVertices(List visibleObjectsBack = new List(); - private readonly List visibleObjectsMid = new List(); - private readonly List visibleObjectsFront = new List(); + // Pre-initialized to the max size, so that we don't have to resize the lists at runtime. TODO: Could the capacity (of some collections?) be lower? + private readonly List visibleObjectsBack = new List(MaxVisibleObjects); + private readonly List visibleObjectsMid = new List(MaxVisibleObjects); + private readonly List visibleObjectsFront = new List(MaxVisibleObjects); + private readonly HashSet allVisibleObjects = new HashSet(MaxVisibleObjects); private double NextRefreshTime; @@ -47,9 +49,25 @@ partial void UpdateProjSpecific(float deltaTime) } } - public IEnumerable GetVisibleObjects() + /// + /// Returns all visible objects, but not in order, because internally uses a HashSet. + /// + public IEnumerable GetAllVisibleObjects() { - return visibleObjectsBack.Union(visibleObjectsMid).Union(visibleObjectsFront); + allVisibleObjects.Clear(); + foreach (LevelObject obj in visibleObjectsBack) + { + allVisibleObjects.Add(obj); + } + foreach (LevelObject obj in visibleObjectsMid) + { + allVisibleObjects.Add(obj); + } + foreach (LevelObject obj in visibleObjectsFront) + { + allVisibleObjects.Add(obj); + } + return allVisibleObjects; } /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index c5df0ac7a8..3d7029c7a5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -11,10 +11,25 @@ namespace Barotrauma { class LevelWallVertexBuffer : IDisposable { - public VertexBuffer WallEdgeBuffer, WallBuffer; + /// + /// Buffer for the vertices of the "actual" wall texture. + /// + public VertexBuffer WallBuffer; + + /// + /// Buffer for the vertices of the repeating edge texture drawn at the edges of the walls. + /// + public VertexBuffer WallEdgeBuffer; + + /// + /// Buffer for the vertices of the inner, non-textured black part of the wall. + /// + public VertexBuffer WallInnerBuffer; + public readonly Texture2D WallTexture, EdgeTexture; private VertexPositionColorTexture[] wallVertices; private VertexPositionColorTexture[] wallEdgeVertices; + private VertexPositionColor[] wallInnerVertices; public bool IsDisposed { @@ -22,7 +37,7 @@ public bool IsDisposed private set; } - public LevelWallVertexBuffer(VertexPositionTexture[] wallVertices, VertexPositionTexture[] wallEdgeVertices, Texture2D wallTexture, Texture2D edgeTexture, Color color) + public LevelWallVertexBuffer(VertexPositionColorTexture[] wallVertices, VertexPositionColorTexture[] wallEdgeVertices, VertexPositionColor[] wallInnerVertices, Texture2D wallTexture, Texture2D edgeTexture) { if (wallVertices.Length == 0) { @@ -32,32 +47,41 @@ public LevelWallVertexBuffer(VertexPositionTexture[] wallVertices, VertexPositio { throw new ArgumentException("Failed to instantiate a LevelWallVertexBuffer (no wall edge vertices)."); } - this.wallVertices = LevelRenderer.GetColoredVertices(wallVertices, color); + this.wallVertices = wallVertices; WallBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, wallVertices.Length, BufferUsage.WriteOnly); WallBuffer.SetData(this.wallVertices); WallTexture = wallTexture; - this.wallEdgeVertices = LevelRenderer.GetColoredVertices(wallEdgeVertices, color); + this.wallEdgeVertices = wallEdgeVertices; WallEdgeBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, wallEdgeVertices.Length, BufferUsage.WriteOnly); WallEdgeBuffer.SetData(this.wallEdgeVertices); EdgeTexture = edgeTexture; + + if (wallInnerVertices != null) + { + this.wallInnerVertices = wallInnerVertices; + WallInnerBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColor.VertexDeclaration, wallInnerVertices.Length, BufferUsage.WriteOnly); + WallInnerBuffer.SetData(this.wallInnerVertices); + } } - public void Append(VertexPositionTexture[] wallVertices, VertexPositionTexture[] wallEdgeVertices, Color color) + public void Append(VertexPositionColorTexture[] newWallVertices, VertexPositionColorTexture[] newWallEdgeVertices, VertexPositionColor[] newWallInnerVertices) { - WallBuffer.Dispose(); - WallBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, this.wallVertices.Length + wallVertices.Length, BufferUsage.WriteOnly); - int originalWallVertexCount = this.wallVertices.Length; - Array.Resize(ref this.wallVertices, originalWallVertexCount + wallVertices.Length); - Array.Copy(LevelRenderer.GetColoredVertices(wallVertices, color), 0, this.wallVertices, originalWallVertexCount, wallVertices.Length); - WallBuffer.SetData(this.wallVertices); + WallBuffer = Append(WallBuffer, ref wallVertices, newWallVertices, VertexPositionColorTexture.VertexDeclaration); + WallEdgeBuffer = Append(WallEdgeBuffer, ref wallEdgeVertices, newWallEdgeVertices, VertexPositionColorTexture.VertexDeclaration); + WallInnerBuffer = Append(WallInnerBuffer, ref wallInnerVertices, newWallInnerVertices, VertexPositionColor.VertexDeclaration); - WallEdgeBuffer.Dispose(); - WallEdgeBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, this.wallEdgeVertices.Length + wallEdgeVertices.Length, BufferUsage.WriteOnly); - int originalWallEdgeVertexCount = this.wallEdgeVertices.Length; - Array.Resize(ref this.wallEdgeVertices, originalWallEdgeVertexCount + wallEdgeVertices.Length); - Array.Copy(LevelRenderer.GetColoredVertices(wallEdgeVertices, color), 0, this.wallEdgeVertices, originalWallEdgeVertexCount, wallEdgeVertices.Length); - WallEdgeBuffer.SetData(this.wallEdgeVertices); + static VertexBuffer Append(VertexBuffer buffer, ref T[] currentVertices, T[] newVertices, VertexDeclaration vertexDeclaration) where T : struct, IVertexType + { + buffer?.Dispose(); + int originalVertexCount = currentVertices.Length; + int newBufferSize = originalVertexCount + newVertices.Length; + buffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, vertexDeclaration, newBufferSize, BufferUsage.WriteOnly); + Array.Resize(ref currentVertices, newBufferSize); + Array.Copy(newVertices, 0, currentVertices, originalVertexCount, newVertices.Length); + buffer.SetData(currentVertices); + return buffer; + } } public void Dispose() @@ -70,7 +94,7 @@ public void Dispose() class LevelRenderer : IDisposable { - private static BasicEffect wallEdgeEffect, wallCenterEffect; + private static BasicEffect wallEdgeEffect, wallCenterEffect, wallInnerEffect; private Vector2 waterParticleOffset; private Vector2 waterParticleVelocity; @@ -129,7 +153,16 @@ public LevelRenderer(Level level) }; wallCenterEffect.CurrentTechnique = wallCenterEffect.Techniques["BasicEffect_Texture"]; } - + + if (wallInnerEffect == null) + { + wallInnerEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + { + VertexColorEnabled = true, + TextureEnabled = false + }; + wallInnerEffect.CurrentTechnique = wallInnerEffect.Techniques["BasicEffect_Texture"]; + } this.level = level; } @@ -184,7 +217,7 @@ public void Update(float deltaTime, Camera cam) //calculate the sum of the forces of nearby level triggers //and use it to move the water texture and water distortion effect Vector2 currentWaterParticleVel = level.GenerationParams.WaterParticleVelocity; - foreach (LevelObject levelObject in level.LevelObjectManager.GetVisibleObjects()) + foreach (LevelObject levelObject in level.LevelObjectManager.GetAllVisibleObjects()) { if (levelObject.Triggers == null) { continue; } //use the largest water flow velocity of all the triggers @@ -225,16 +258,16 @@ public static VertexPositionColorTexture[] GetColoredVertices(VertexPositionText return verts; } - public void SetVertices(VertexPositionTexture[] wallVertices, VertexPositionTexture[] wallEdgeVertices, Texture2D wallTexture, Texture2D edgeTexture, Color color) + public void SetVertices(VertexPositionColorTexture[] wallVertices, VertexPositionColorTexture[] wallEdgeVertices, VertexPositionColor[] wallInnerVertices, Texture2D wallTexture, Texture2D edgeTexture) { var existingBuffer = vertexBuffers.Find(vb => vb.WallTexture == wallTexture && vb.EdgeTexture == edgeTexture); if (existingBuffer != null) { - existingBuffer.Append(wallVertices, wallEdgeVertices,color); + existingBuffer.Append(wallVertices, wallEdgeVertices, wallInnerVertices); } else { - vertexBuffers.Add(new LevelWallVertexBuffer(wallVertices, wallEdgeVertices, wallTexture, edgeTexture, color)); + vertexBuffers.Add(new LevelWallVertexBuffer(wallVertices, wallEdgeVertices, wallInnerVertices, wallTexture, edgeTexture)); } } @@ -497,15 +530,16 @@ public void RenderWalls(GraphicsDevice graphicsDevice, Camera cam) } } - wallEdgeEffect.Alpha = 1.0f; - wallCenterEffect.Alpha = 1.0f; - - wallCenterEffect.World = transformMatrix; - wallEdgeEffect.World = transformMatrix; + wallEdgeEffect.Alpha = wallInnerEffect.Alpha = wallCenterEffect.Alpha = 1.0f; + wallCenterEffect.World = wallInnerEffect.World = wallEdgeEffect.World = transformMatrix; //render static walls foreach (var vertexBuffer in vertexBuffers) { + wallInnerEffect.CurrentTechnique.Passes[0].Apply(); + graphicsDevice.SetVertexBuffer(vertexBuffer.WallInnerBuffer); + graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(vertexBuffer.WallInnerBuffer.VertexCount / 3.0f)); + wallCenterEffect.Texture = vertexBuffer.WallTexture; wallCenterEffect.CurrentTechnique.Passes[0].Apply(); graphicsDevice.SetVertexBuffer(vertexBuffer.WallBuffer); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs index 7c60d6793d..1d301e382d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs @@ -1,10 +1,7 @@ -using Barotrauma.Extensions; -using FarseerPhysics; +using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections.Generic; -using System.Linq; namespace Barotrauma { @@ -26,22 +23,29 @@ public Matrix GetTransform() Matrix.CreateTranslation(new Vector3(ConvertUnits.ToDisplayUnits(Body.Position), 0.0f)); } - public void SetWallVertices(VertexPositionTexture[] wallVertices, VertexPositionTexture[] wallEdgeVertices, Texture2D wallTexture, Texture2D edgeTexture, Color color) + public void SetWallVertices( + VertexPositionColorTexture[] wallVertices, VertexPositionColorTexture[] wallEdgeVertices, + Texture2D wallTexture, Texture2D edgeTexture) { if (VertexBuffer != null && !VertexBuffer.IsDisposed) { VertexBuffer.Dispose(); } - VertexBuffer = new LevelWallVertexBuffer(wallVertices, wallEdgeVertices, wallTexture, edgeTexture, color); + VertexBuffer = new LevelWallVertexBuffer(wallVertices, wallEdgeVertices, wallInnerVertices: null, wallTexture, edgeTexture); } public void GenerateVertices() { float zCoord = this is DestructibleLevelWall ? Rand.Range(0.9f, 1.0f) : 0.9f; - List wallVertices = CaveGenerator.GenerateWallVertices(triangles, level.GenerationParams, zCoord); + var nonTexturedWallVerts = + CaveGenerator.GenerateWallVertices(triangles, color, zCoord: 0.9f).ToArray(); + var wallVerts = CaveGenerator.ConvertToTextured(nonTexturedWallVerts, level.GenerationParams.WallTextureSize); SetWallVertices( - wallVertices.ToArray(), - CaveGenerator.GenerateWallEdgeVertices(Cells, level, zCoord).ToArray(), + wallVertices: wallVerts, + wallEdgeVertices: CaveGenerator.GenerateWallEdgeVertices(Cells, + level.GenerationParams.WallEdgeExpandOutwardsAmount, level.GenerationParams.WallEdgeExpandInwardsAmount, + outerColor: color, innerColor: color, + level, zCoord) + .ToArray(), level.GenerationParams.WallSprite.Texture, - level.GenerationParams.WallEdgeSprite.Texture, - color); + level.GenerationParams.WallEdgeSprite.Texture); } public bool IsVisible(Rectangle worldView) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 4c0381ad6b..7992ad5f94 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -443,10 +443,10 @@ public void SetVertices(Vector2[] points, Vector2[] losPoints, bool mergeOverlap public bool Intersects(Rectangle rect) { - if (!Enabled) return false; + if (!Enabled) { return false; } Rectangle transformedBounds = BoundingBox; - if (ParentEntity != null && ParentEntity.Submarine != null) + if (ParentEntity is { Submarine: not null }) { transformedBounds.X += (int)ParentEntity.Submarine.Position.X; transformedBounds.Y += (int)ParentEntity.Submarine.Position.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index bee948d79a..4180c890b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -243,6 +243,7 @@ public void DebugDrawVertices(SpriteBatch spriteBatch) } } + /// A render target that contains the structures that should obstruct lights in the background. If not given, damageable walls and hulls are rendered to obstruct the background lights. public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) { if (!LightingEnabled) { return; } @@ -411,11 +412,21 @@ float lightPriority(float range, LightSource light) } spriteBatch.End(); - GameMain.GameScreen.DamageEffect.CurrentTechnique = GameMain.GameScreen.DamageEffect.Techniques["StencilShaderSolidColor"]; - GameMain.GameScreen.DamageEffect.Parameters["solidColor"].SetValue(Color.Black.ToVector4()); - spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, transformMatrix: spriteBatchTransform, effect: GameMain.GameScreen.DamageEffect); - Submarine.DrawDamageable(spriteBatch, GameMain.GameScreen.DamageEffect); - spriteBatch.End(); + if (backgroundObstructor != null) + { + spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, transformMatrix: Matrix.Identity, effect: GameMain.GameScreen.DamageEffect); + spriteBatch.Draw(backgroundObstructor, new Rectangle(0, 0, + (int)(GameMain.GraphicsWidth * currLightMapScale), (int)(GameMain.GraphicsHeight * currLightMapScale)), Color.Black); + spriteBatch.End(); + } + else + { + GameMain.GameScreen.DamageEffect.CurrentTechnique = GameMain.GameScreen.DamageEffect.Techniques["StencilShaderSolidColor"]; + GameMain.GameScreen.DamageEffect.Parameters["solidColor"].SetValue(Color.Black.ToVector4()); + spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, transformMatrix: spriteBatchTransform, effect: GameMain.GameScreen.DamageEffect); + Submarine.DrawDamageable(spriteBatch, GameMain.GameScreen.DamageEffect); + spriteBatch.End(); + } graphics.BlendState = BlendState.Additive; @@ -680,11 +691,14 @@ private Dictionary GetVisibleHulls(Camera cam) } return visibleHulls; } + + private static readonly List ShadowVertices = new List(500); + private static readonly List PenumbraVertices = new List(500); public void UpdateObstructVision(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, Vector2 lookAtPosition) { if ((!LosEnabled || LosMode == LosMode.None) && ObstructVisionAmount <= 0.0f) { return; } - if (ViewTarget == null) return; + if (ViewTarget == null) { return; } graphics.SetRenderTarget(LosTexture); @@ -767,11 +781,11 @@ public void UpdateObstructVision(GraphicsDevice graphics, SpriteBatch spriteBatc if (convexHulls != null) { - List shadowVerts = new List(); - List penumbraVerts = new List(); + ShadowVertices.Clear(); + PenumbraVertices.Clear(); foreach (ConvexHull convexHull in convexHulls) { - if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } + if (!convexHull.Intersects(camView)) { continue; } Vector2 relativeViewPos = pos; if (convexHull.ParentEntity?.Submarine != null) @@ -783,26 +797,26 @@ public void UpdateObstructVision(GraphicsDevice graphics, SpriteBatch spriteBatc for (int i = 0; i < convexHull.ShadowVertexCount; i++) { - shadowVerts.Add(convexHull.ShadowVertices[i]); + ShadowVertices.Add(convexHull.ShadowVertices[i]); } for (int i = 0; i < convexHull.PenumbraVertexCount; i++) { - penumbraVerts.Add(convexHull.PenumbraVertices[i]); + PenumbraVertices.Add(convexHull.PenumbraVertices[i]); } } - if (shadowVerts.Count > 0) + if (ShadowVertices.Count > 0) { ConvexHull.shadowEffect.World = shadowTransform; ConvexHull.shadowEffect.CurrentTechnique.Passes[0].Apply(); - graphics.DrawUserPrimitives(PrimitiveType.TriangleList, shadowVerts.ToArray(), 0, shadowVerts.Count / 3, VertexPositionColor.VertexDeclaration); + graphics.DrawUserPrimitives(PrimitiveType.TriangleList, ShadowVertices.ToArray(), 0, ShadowVertices.Count / 3, VertexPositionColor.VertexDeclaration); - if (penumbraVerts.Count > 0) + if (PenumbraVertices.Count > 0) { ConvexHull.penumbraEffect.World = shadowTransform; ConvexHull.penumbraEffect.CurrentTechnique.Passes[0].Apply(); - graphics.DrawUserPrimitives(PrimitiveType.TriangleList, penumbraVerts.ToArray(), 0, penumbraVerts.Count / 3, VertexPositionTexture.VertexDeclaration); + graphics.DrawUserPrimitives(PrimitiveType.TriangleList, PenumbraVertices.ToArray(), 0, PenumbraVertices.Count / 3, VertexPositionTexture.VertexDeclaration); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 498b8d1807..c8fcda5c39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -592,7 +592,17 @@ private void CheckConvexHullsInRange() private void CheckHullsInRange(Submarine sub) { //find the list of convexhulls that belong to the sub - ConvexHullList chList = convexHullsInRange.FirstOrDefault(chList => chList.Submarine == sub); + + // Performance-sensitive code, hence implemented without Linq. + ConvexHullList chList = null; + foreach (var chl in convexHullsInRange) + { + if (chl.Submarine == sub) + { + chList = chl; + break; + } + } //not found -> create one if (chList == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index ff6d391088..ce14db11d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -451,7 +451,7 @@ private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effec MathUtils.PositiveModulo(-textureOffset.X, Prefab.BackgroundSprite.SourceRect.Width * TextureScale.X * Scale), MathUtils.PositiveModulo(-textureOffset.Y, Prefab.BackgroundSprite.SourceRect.Height * TextureScale.Y * Scale)); - float rotationRad = rotationForSprite(this.rotationRad, Prefab.BackgroundSprite); + float rotationRad = GetRotationForSprite(this.rotationRad, Prefab.BackgroundSprite); Prefab.BackgroundSprite.DrawTiled( spriteBatch, @@ -492,7 +492,7 @@ private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effec advanceY = advanceY.FlipX(); } - float sectionSpriteRotationRad = rotationForSprite(this.rotationRad, Prefab.Sprite); + float sectionSpriteRotationRad = GetRotationForSprite(this.rotationRad, Prefab.Sprite); for (int i = 0; i < Sections.Length; i++) { @@ -501,11 +501,17 @@ private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effec { float newCutoff = MathHelper.Lerp(0.0f, 0.65f, Sections[i].damage / MaxHealth); - if (Math.Abs(newCutoff - Submarine.DamageEffectCutoff) > 0.01f || color != Submarine.DamageEffectColor) + if (Math.Abs(newCutoff - Submarine.DamageEffectCutoff) > 0.05f) { + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.BackToFront, + BlendState.NonPremultiplied, SamplerState.LinearWrap, + null, null, + damageEffect, + Screen.Selected.Cam.Transform); + damageEffect.Parameters["aCutoff"].SetValue(newCutoff); damageEffect.Parameters["cCutoff"].SetValue(newCutoff * 1.2f); - damageEffect.Parameters["inColor"].SetValue(color.ToVector4()); damageEffect.CurrentTechnique.Passes[0].Apply(); @@ -570,9 +576,13 @@ private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effec } } - static float rotationForSprite(float rotationRad, Sprite sprite) + static float GetRotationForSprite(float rotationRad, Sprite sprite) { - if (sprite.effects.HasFlag(SpriteEffects.FlipHorizontally) != sprite.effects.HasFlag(SpriteEffects.FlipVertically)) + // Use bitwise operations instead of HasFlag to avoid boxing, as this is performance-sensitive code. + bool flipHorizontally = (sprite.effects & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally; + bool flipVertically = (sprite.effects & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically; + + if (flipHorizontally != flipVertically) { rotationRad = -rotationRad; } @@ -604,6 +614,10 @@ static float rotationForSprite(float rotationRad, Sprite sprite) if (GetSection(i).damage > 0) { var textPos = SectionPosition(i, true); + if (Submarine != null) + { + textPos += (Submarine.DrawPosition - Submarine.Position); + } textPos.Y = -textPos.Y; GUI.DrawString(spriteBatch, textPos, "Damage: " + (int)((GetSection(i).damage / MaxHealth) * 100f) + "%", Color.Yellow); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index aab1e5d7e6..3a6ce3137e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -134,7 +134,7 @@ public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Pred foreach (Submarine sub in Loaded) { Rectangle worldBorders = sub.Borders; - worldBorders.Location += sub.WorldPosition.ToPoint(); + worldBorders.Location += (sub.DrawPosition + sub.HiddenSubPosition).ToPoint(); worldBorders.Y = -worldBorders.Y; GUI.DrawRectangle(spriteBatch, worldBorders, Color.White, false, 0, 5); @@ -161,38 +161,38 @@ public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Pred public static float DamageEffectCutoff; public static Color DamageEffectColor; - private static readonly List depthSortedDamageable = new List(); public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate predicate = null) { - var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList; - - depthSortedDamageable.Clear(); - - //insertion sort according to draw depth - foreach (MapEntity e in entitiesToRender) + if (!editing && visibleEntities != null) { - if (e is Structure structure && structure.DrawDamageEffect) + foreach (MapEntity e in visibleEntities) { - if (predicate != null) + if (e is Structure structure && structure.DrawDamageEffect) { - if (!predicate(e)) { continue; } + if (predicate != null) + { + if (!predicate(structure)) { continue; } + } + structure.DrawDamage(spriteBatch, damageEffect, editing); } - float drawDepth = structure.GetDrawDepth(); - int i = 0; - while (i < depthSortedDamageable.Count) + } + } + else + { + foreach (Structure structure in Structure.WallList) + { + if (structure.DrawDamageEffect) { - float otherDrawDepth = depthSortedDamageable[i].GetDrawDepth(); - if (otherDrawDepth < drawDepth) { break; } - i++; + if (predicate != null) + { + if (!predicate(structure)) { continue; } + } + structure.DrawDamage(spriteBatch, damageEffect, editing); } - depthSortedDamageable.Insert(i, structure); } } - foreach (Structure s in depthSortedDamageable) - { - s.DrawDamage(spriteBatch, damageEffect, editing); - } + if (damageEffect != null) { damageEffect.Parameters["aCutoff"].SetValue(0.0f); @@ -506,6 +506,16 @@ public void CheckForErrors() warnings.Add(SubEditorScreen.WarningType.WaterInHulls); Hull.ShowHulls = true; } + + if (Info.IsWreck) + { + Point vanillaBrainSize = new Point(204, 204); + if (WreckAI.GetPotentialBrainRooms(this, WreckAIConfig.GetRandom(), minSize: vanillaBrainSize).None()) + { + errorMsgs.Add(TextManager.Get("NoSuitableBrainRoomsWarning").Value); + warnings.Add(SubEditorScreen.WarningType.NoSuitableBrainRooms); + } + } if (!IsWarningSuppressed(SubEditorScreen.WarningType.NotEnoughContainers)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index c00daa1b5c..c8c38d34a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -46,6 +46,10 @@ public void Draw(SpriteBatch spriteBatch, Vector2 drawPos) } if (IsHighlighted || IsHighlighted) { clr = Color.Lerp(clr, Color.White, 0.8f); } + if (Stairs is { Removed: true }) { Stairs = null; } + if (Ladders is { Item.Removed: true }) { Ladders = null; } + if (ConnectedGap is { Removed: true }) { ConnectedGap = null; } + int iconSize = spawnType == SpawnType.Path ? WaypointSize : SpawnPointSize; if (ConnectedDoor != null || Ladders != null || Stairs != null || SpawnType != SpawnType.Path) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 3086d38358..a4a980be44 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -519,10 +519,12 @@ public void Update(float deltaTime) { string errorMsg = "Error while reading a message from server. "; if (GameMain.Client == null) { errorMsg += "Client disposed."; } - AppendExceptionInfo(ref errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - DebugConsole.ThrowError(errorMsg); - new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", e.TargetSite.ToString()))) + AppendExceptionInfo(ref errorMsg, out Entity causingEntity, e); + + string targetSite = e.TargetSite?.ToString() ?? "unknown"; + GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + targetSite, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.ThrowError(errorMsg, contentPackage: causingEntity?.ContentPackage); + new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", targetSite))) { DisplayInLoadingScreens = true }; @@ -662,7 +664,7 @@ or ServerPacketHeader.PING_REQUEST catch (Exception e) { string errorMsg = "Error while reading an ingame update message from server."; - AppendExceptionInfo(ref errorMsg, e); + AppendExceptionInfo(ref errorMsg, out Entity causingEntity, e); GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadDataMessage:ReadIngameUpdate", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw; } @@ -986,13 +988,15 @@ private void ReadStartGameFinalize(IReadMessage inc) { if (Level.Loaded.EqualityCheckValues[stage] != levelEqualityCheckValues[stage]) { - string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server" + - " (client value " + stage + ": " + Level.Loaded.EqualityCheckValues[stage].ToString("X") + - ", server value " + stage + ": " + levelEqualityCheckValues[stage].ToString("X") + - ", level value count: " + levelEqualityCheckValues.Count + - ", seed: " + Level.Loaded.Seed + - ", sub: " + (Submarine.MainSub == null ? "null" : (Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")")) + - ", mirrored: " + Level.Loaded.Mirrored + "). Round init status: " + roundInitStatus + "." + campaignErrorInfo; + string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server, " + + $"(client value {stage}:{Level.Loaded.EqualityCheckValues[stage].ToString("X")}, " + + $"server value {stage}: {levelEqualityCheckValues[stage].ToString("X")}, " + + $"level value count: {levelEqualityCheckValues.Count}, " + + $"seed: {Level.Loaded.Seed}, " + + $"missions: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + + $"sub: {(Submarine.MainSub == null ? "null" : (Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation))}, " + + $"mirrored: {Level.Loaded.Mirrored}). Round init status: {roundInitStatus}." + + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -1470,6 +1474,7 @@ private IEnumerable StartGame(IReadMessage inc) { string levelSeed = inc.ReadString(); float levelDifficulty = inc.ReadSingle(); + Identifier biomeId = inc.ReadIdentifier(); string subName = inc.ReadString(); string subHash = inc.ReadString(); string shuttleName = inc.ReadString(); @@ -1557,7 +1562,7 @@ private IEnumerable StartGame(IReadMessage inc) var selectedEnemySub = hasEnemySub && GameMain.NetLobbyScreen.SelectedEnemySub is { } enemySub ? Option.Some(enemySub) : Option.None; GameMain.GameSession = new GameSession(GameMain.NetLobbyScreen.SelectedSub, selectedEnemySub, gameMode, missionPrefabs: selectedMissions); - GameMain.GameSession.StartRound(levelSeed, levelDifficulty, levelGenerationParams: null, forceBiome: ServerSettings.Biome); + GameMain.GameSession.StartRound(levelSeed, levelDifficulty, levelGenerationParams: null, forceBiome: biomeId); } else { @@ -2075,16 +2080,15 @@ private void ReadLobbyUpdate(IReadMessage inc) UInt16 updateID = inc.ReadUInt16(); + UInt16 settingsLen = inc.ReadUInt16(); byte[] settingsData = inc.ReadBytes(settingsLen); bool isInitialUpdate = inc.ReadBoolean(); + DebugConsole.Log($"Received {(isInitialUpdate ? "initial" : string.Empty)} lobby update ID: {updateID}, last ID: {GameMain.NetLobbyScreen.LastUpdateID}."); + if (isInitialUpdate) - { - if (GameSettings.CurrentConfig.VerboseLogging) - { - DebugConsole.NewMessage("Received initial lobby update, ID: " + updateID + ", last ID: " + GameMain.NetLobbyScreen.LastUpdateID, Color.Gray); - } + { ReadInitialUpdate(inc); initialUpdateReceived = true; } @@ -2363,7 +2367,7 @@ or ServerNetSegment.EntityEvent GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsManager.ErrorSeverity.Critical, string.Join("\n", errorLines)); throw new Exception( - $"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." + + $"Exception thrown while reading a message of the type \"{segment.Identifier}\" at position {segment.Pointer}." + (prevSegments.Any() ? $" Previous segments: {string.Join(", ", prevSegments)}" : ""), ex); }); @@ -3760,16 +3764,24 @@ private void WriteEventErrorData(ClientNetError error, UInt16 expectedID, UInt16 eventErrorWritten = true; } - private static void AppendExceptionInfo(ref string errorMsg, Exception e) + private static void AppendExceptionInfo(ref string errorMsg, out Entity causingEntity, Exception e) { if (!errorMsg.EndsWith("\n")) { errorMsg += "\n"; } + + Exception innerMostException = e.GetInnermost(); + causingEntity = GetCausingEntity(e); + + if (causingEntity != null) + { + errorMsg += "Entity: " + causingEntity + "\n"; + } errorMsg += e.Message + "\n"; - var innermostException = e.GetInnermost(); - if (innermostException != e) + + if (innerMostException != e) { // If available, only append the stacktrace of the innermost exception, // because that's the most important one to fix - errorMsg += "Inner exception: " + innermostException.Message + "\n" + innermostException.StackTrace.CleanupStackTrace(); + errorMsg += "Inner exception: " + innerMostException.Message + "\n" + innerMostException.StackTrace.CleanupStackTrace(); } else { @@ -3777,6 +3789,24 @@ private static void AppendExceptionInfo(ref string errorMsg, Exception e) } } + /// + /// Checks if the exception or any of its inner exceptions are EntityEventExceptions, and returns the entity that caused the innermost EntityEventException. + /// + private static Entity GetCausingEntity(Exception e) + { + Entity causingEntity = null; + Exception currentException = e; + while (currentException != null) + { + if (currentException is EntityEventException entityEventException) + { + causingEntity = entityEventException.Entity; + } + currentException = currentException.InnerException; + } + return causingEntity; + } + #if DEBUG public void ForceTimeOut() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index c50dc2d862..d00026416f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -154,13 +154,25 @@ public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime) //16 = entity ID, 8 = msg length if (msg.BitPosition + 16 + 8 > msg.LengthBits) { - 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})."; + UInt16 potentialEntityId = Entity.NullEntityID; + try + { + potentialEntityId = msg.ReadUInt16(); + } + catch + { + //failed to read the ID, do nothing (we would've just used it for the error message) + } + Entity targetEntity = Entity.FindEntityByID(potentialEntityId); + + string errorMsg = $"Error while reading a message from the server (entity: {targetEntity?.ToString() ?? "unknown"})."; + errorMsg += $" Entity event data exceeds the size of the buffer (current position: {msg.BitPosition}, length: {msg.LengthBits})."; errorMsg += "\nPrevious entities:"; for (int j = tempEntityList.Count - 1; j >= 0; j--) { errorMsg += "\n" + (tempEntityList[j] == null ? "NULL" : tempEntityList[j].ToString()); } - DebugConsole.ThrowError(errorMsg); + DebugConsole.ThrowError(errorMsg, contentPackage: targetEntity?.ContentPackage); return false; } @@ -172,7 +184,7 @@ public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime) if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("received msg " + thisEventID + " (null entity)", - Microsoft.Xna.Framework.Color.Orange); + Color.Orange); } tempEntityList.Add(null); if (thisEventID == (UInt16)(lastReceivedID + 1)) { lastReceivedID++; } @@ -187,7 +199,7 @@ public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime) //skip the event if we've already received it or if the entity isn't found if (thisEventID != (UInt16)(lastReceivedID + 1) || entity == null) { - if (thisEventID != (UInt16) (lastReceivedID + 1)) + if (thisEventID != (UInt16)(lastReceivedID + 1)) { if (GameSettings.CurrentConfig.VerboseLogging) { @@ -195,7 +207,7 @@ public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime) "Received msg " + thisEventID + " (waiting for " + (lastReceivedID + 1) + ")", NetIdUtils.IdMoreRecent(thisEventID, (UInt16)(lastReceivedID + 1)) ? GUIStyle.Red - : Microsoft.Xna.Framework.Color.Yellow); + : Color.Yellow); } } else if (entity == null) @@ -215,12 +227,18 @@ public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime) if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("received msg " + thisEventID + " (" + entity.ToString() + ")", - Microsoft.Xna.Framework.Color.Green); + Color.Green); } lastReceivedID++; - ReadEvent(msg, entity, sendingTime); - msg.ReadPadBits(); - + try + { + ReadEvent(msg, entity, sendingTime); + msg.ReadPadBits(); + } + catch (Exception exception) + { + throw new EntityEventException("Failed to read event." , entity as Entity, exception); + } if (msg.BitPosition != msgPosition + msgLength * 8) { var prevEntity = tempEntityList.Count >= 2 ? tempEntityList[tempEntityList.Count - 2] : null; @@ -231,7 +249,7 @@ public bool Read(ServerNetSegment type, IReadMessage msg, float sendingTime) GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - throw new Exception(errorMsg); + throw new EntityEventException(errorMsg, entity as Entity); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamConnectSocket.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamConnectSocket.cs index 47e3b69e51..aceb50a951 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamConnectSocket.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/P2PSocket/SteamConnectSocket.cs @@ -74,7 +74,16 @@ public static Result Create(SteamP2PEndpoint endpoint, Callbac { if (!SteamManager.IsInitialized) { return Result.Failure(new Error(ErrorCode.SteamNotInitialized)); } - var connectionManager = Steamworks.SteamNetworkingSockets.ConnectRelay(endpoint.SteamId.Value); + ConnectionManager connectionManager; + try + { + connectionManager = Steamworks.SteamNetworkingSockets.ConnectRelay(endpoint.SteamId.Value); + } + catch (ArgumentException e) + { + DebugConsole.ThrowError("Failed to connect via SteamP2P. Are you logged in to Steam, is the same Steam account already connected to the server?", e); + return Result.Failure(new Error(ErrorCode.FailedToCreateSteamP2PSocket)); + } if (connectionManager is null) { return Result.Failure(new Error(ErrorCode.FailedToCreateSteamP2PSocket)); } connectionManager.SetEndpointAndCallbacks(endpoint, callbacks); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index 66b5544bdd..a87ba866d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -202,6 +202,7 @@ private void UpdateCapture() { DebugConsole.ThrowError("Capture device has been disconnected. You can select another available device in the settings."); Disconnected = true; + TryRefreshDevice(); break; } } @@ -401,5 +402,65 @@ public override void Dispose() captureThread = null; if (captureDevice != IntPtr.Zero) { Alc.CaptureCloseDevice(captureDevice); } } + + public static void TryRefreshDevice() + { + DebugConsole.NewMessage("Refreshing audio capture device"); + + List deviceList = Alc.GetStringList(IntPtr.Zero, Alc.CaptureDeviceSpecifier).ToList(); + int alcError = Alc.GetError(IntPtr.Zero); + if (alcError != Alc.NoError) + { + DebugConsole.ThrowError("Failed to list available audio input devices: " + alcError.ToString()); + return; + } + + if (deviceList.Any()) + { + string device; + + if (deviceList.Find(n => n.Equals(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, StringComparison.OrdinalIgnoreCase)) + is string availablePreviousDevice) + { + DebugConsole.NewMessage($" Previous device choice available: {availablePreviousDevice}"); + device = availablePreviousDevice; + } + else + { + device = Alc.GetString(IntPtr.Zero, Alc.CaptureDefaultDeviceSpecifier); + DebugConsole.NewMessage($" Reverting to default device: {device}"); + } + + if (string.IsNullOrEmpty(device)) + { + device = deviceList[0]; + DebugConsole.NewMessage($" No default device found, resorting to first available device: {device}"); + } + + // Save the new device choice and generate a new voice capture instance with it + var currentConfig = GameSettings.CurrentConfig; + currentConfig.Audio.VoiceCaptureDevice = device; + GameSettings.SetCurrentConfig(currentConfig); + if (Instance is VoipCapture currentCaptureInstance) + { + currentCaptureInstance.Dispose(); + } + Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice); + } + + // Didn't end up with any capture device, so let's disable voice capture for now + if (Instance == null) + { + DebugConsole.NewMessage($" No devices found, disabling"); + var currentConfig = GameSettings.CurrentConfig; + currentConfig.Audio.VoiceSetting = VoiceMode.Disabled; + GameSettings.SetCurrentConfig(currentConfig); + } + + if (GUI.SettingsMenuOpen) + { + SettingsMenu.Instance?.CreateAudioAndVCTab(true); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 21d0f374d1..77b3202cc2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -10,6 +10,7 @@ class ParticlePrefab : Prefab, ISerializableEntity { public static readonly PrefabCollection Prefabs = new PrefabCollection(); + [Flags] public enum DrawTargetType { Air = 1, Water = 2, Both = 3 } public readonly List Sprites; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index 425740cadb..339e660444 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -64,12 +64,12 @@ public void DebugDraw(SpriteBatch spriteBatch, Color color, bool forceColor = fa if (drawOffset != Vector2.Zero) { Vector2 pos = ConvertUnits.ToDisplayUnits(FarseerBody.Position); - if (Submarine != null) pos += Submarine.DrawPosition; + if (Submarine != null) { pos += Submarine.DrawPosition; } GUI.DrawLine(spriteBatch, new Vector2(pos.X, -pos.Y), new Vector2(DrawPosition.X, -DrawPosition.Y), - Color.Cyan, 0, 5); + Color.Purple * 0.75f, 0, 5); } if (IsValidShape(Radius, Height, Width)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 366b3bc3a5..cf804cdbfd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -145,6 +145,7 @@ public struct CampaignSettingElements public SettingValue OxygenMultiplier; public SettingValue FuelMultiplier; public SettingValue MissionRewardMultiplier; + public SettingValue ExperienceRewardMultiplier; public SettingValue ShopPriceMultiplier; public SettingValue ShipyardPriceMultiplier; public SettingValue RepairFailMultiplier; @@ -167,6 +168,7 @@ public readonly CampaignSettings CreateSettings() OxygenMultiplier = OxygenMultiplier.GetValue(), FuelMultiplier = FuelMultiplier.GetValue(), MissionRewardMultiplier = MissionRewardMultiplier.GetValue(), + ExperienceRewardMultiplier = ExperienceRewardMultiplier.GetValue(), ShopPriceMultiplier = ShopPriceMultiplier.GetValue(), ShipyardPriceMultiplier = ShipyardPriceMultiplier.GetValue(), RepairFailMultiplier = RepairFailMultiplier.GetValue(), @@ -344,6 +346,19 @@ protected static CampaignSettingElements CreateCampaignSettingList(GUIComponent verticalSize, OnValuesChanged); + // Experience reward multiplier + CampaignSettings.MultiplierSettings experienceMultiplierSettings = CampaignSettings.GetMultiplierSettings("ExperienceRewardMultiplier"); + SettingValue experienceMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.experiencerewardmultiplier"), + TextManager.Get("campaignoption.experiencerewardmultiplier.tooltip"), + prevSettings.ExperienceRewardMultiplier, + valueStep: experienceMultiplierSettings.Step, + minValue: experienceMultiplierSettings.Min, + maxValue: experienceMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + // Shop buying prices multiplier CampaignSettings.MultiplierSettings shopPriceMultiplierSettings = CampaignSettings.GetMultiplierSettings("ShopPriceMultiplier"); SettingValue shopPriceMultiplier = CreateGUIFloatInputCarousel( @@ -501,6 +516,7 @@ protected static CampaignSettingElements CreateCampaignSettingList(GUIComponent oxygenMultiplier.SetValue(settings.OxygenMultiplier); fuelMultiplier.SetValue(settings.FuelMultiplier); rewardMultiplier.SetValue(settings.MissionRewardMultiplier); + experienceMultiplier.SetValue(settings.ExperienceRewardMultiplier); shopPriceMultiplier.SetValue(settings.ShopPriceMultiplier); shipyardPriceMultiplier.SetValue(settings.ShipyardPriceMultiplier); repairFailMultiplier.SetValue(settings.RepairFailMultiplier); @@ -530,6 +546,7 @@ void OnValuesChanged() OxygenMultiplier = oxygenMultiplier, FuelMultiplier = fuelMultiplier, MissionRewardMultiplier = rewardMultiplier, + ExperienceRewardMultiplier = experienceMultiplier, ShopPriceMultiplier = shopPriceMultiplier, ShipyardPriceMultiplier = shipyardPriceMultiplier, RepairFailMultiplier = repairFailMultiplier, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 5818844bfe..9e2528488c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -236,10 +236,36 @@ public override void CreateLoadMenu(IEnumerable saveFiles { if (saveList.SelectedData is not CampaignMode.SaveInfo saveInfo) { return false; } if (string.IsNullOrWhiteSpace(saveInfo.FilePath)) { return false; } - LoadGame?.Invoke(saveInfo.FilePath, backupIndex: Option.None); - - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + + if (saveInfo.RespawnMode != RespawnMode.None && saveInfo.RespawnMode != GameMain.NetworkMember?.ServerSettings?.RespawnMode) + { + var msgBox = new GUIMessageBox(TextManager.Get("Warning"), + TextManager.GetWithVariables("RespawnModeMismatch", + ("[currentrespawnmode]", TextManager.Get($"respawnmode.{GameMain.NetworkMember?.ServerSettings?.RespawnMode}")), + ("[savedrespawnmode]", TextManager.Get($"respawnmode.{saveInfo.RespawnMode}"))), + new LocalizedString[] { TextManager.Get("RespawnModeMismatch.GoBack"), TextManager.Get("RespawnModeMismatch.LoadAnyway") }); + msgBox.Buttons[0].OnClicked = (button, obj) => + { + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked = (button, obj) => + { + msgBox.Close(); + LoadSaveGame(); + return true; + }; + return false; + } + + LoadSaveGame(); return true; + + void LoadSaveGame() + { + LoadGame?.Invoke(saveInfo.FilePath, backupIndex: Option.None); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + } }, Enabled = false }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index fb27297d72..ffb4b0086e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -443,20 +443,22 @@ public void SelectLocation(Location location, LocationConnection connection) GUILayoutGroup difficultyIndicatorGroup = null; if (mission.Difficulty.HasValue) { - difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.Z, 0) }, + difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.9f), missionName.RectTransform, anchor: Anchor.CenterRight) { AbsoluteOffset = new Point((int)missionName.Padding.Z, 0) }, isHorizontal: true, childAnchor: Anchor.CenterRight) { AbsoluteSpacing = 1, - UserData = "difficulty" + UserData = "difficulty", }; + difficultyIndicatorGroup.SetAsFirstChild(); var difficultyColor = mission.GetDifficultyColor(); for (int i = 0; i < mission.Difficulty; i++) { - new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest) { IsFixedSize = true }, "DifficultyIndicator", scaleToFit: true) + new GUIImage(new RectTransform(Vector2.One * 0.9f, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest) { IsFixedSize = true }, "DifficultyIndicator", scaleToFit: true) { Color = difficultyColor, SelectedColor = difficultyColor, - HoverColor = difficultyColor + HoverColor = difficultyColor, + ToolTip = mission.GetDifficultyToolTipText() }; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 8b390e1fce..6f04215f96 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -1771,7 +1771,7 @@ public bool CreateCharacter(Identifier name, string mainFolder, bool isHumanoid, // Ragdoll RagdollParams.ClearCache(); - string ragdollPath = RagdollParams.GetDefaultFile(name, contentPackage); + string ragdollPath = RagdollParams.GetDefaultFile(name); RagdollParams ragdollParams = isHumanoid ? RagdollParams.CreateDefault(ragdollPath, name, ragdoll) : RagdollParams.CreateDefault(ragdollPath, name, ragdoll); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 77e7650995..098b215444 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Transactions; namespace Barotrauma { @@ -15,6 +16,8 @@ partial class GameScreen : Screen private RenderTarget2D renderTargetWater; private RenderTarget2D renderTargetFinal; + private RenderTarget2D renderTargetDamageable; + public readonly Effect DamageEffect; private readonly Texture2D damageStencil; private readonly Texture2D distortTexture; @@ -65,6 +68,7 @@ private void CreateRenderTargets(GraphicsDevice graphics) renderTargetBackground = new RenderTarget2D(graphics, GameMain.GraphicsWidth, GameMain.GraphicsHeight); renderTargetWater = new RenderTarget2D(graphics, GameMain.GraphicsWidth, GameMain.GraphicsHeight); renderTargetFinal = new RenderTarget2D(graphics, GameMain.GraphicsWidth, GameMain.GraphicsHeight, false, SurfaceFormat.Color, DepthFormat.None); + renderTargetDamageable = new RenderTarget2D(graphics, GameMain.GraphicsWidth, GameMain.GraphicsHeight, false, SurfaceFormat.Color, DepthFormat.None); } public override void AddToGUIUpdateList() @@ -302,7 +306,7 @@ static bool IsFromOutpostDrawnBehindSubs(Entity e) //Draw background structures and wall background sprites //(= the background texture that's revealed when a wall is destroyed) into the background render target //These will be visible through the LOS effect. - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform); Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null) && !IsFromOutpostDrawnBehindSubs(e)); Submarine.DrawPaintedColors(spriteBatch, false); spriteBatch.End(); @@ -311,8 +315,19 @@ static bool IsFromOutpostDrawnBehindSubs(Entity e) GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:BackStructures", sw.ElapsedTicks); sw.Restart(); + graphics.SetRenderTarget(renderTargetDamageable); + graphics.Clear(Color.Transparent); + DamageEffect.CurrentTechnique = DamageEffect.Techniques["StencilShader"]; + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, SamplerState.LinearWrap, effect: DamageEffect, transformMatrix: cam.Transform); + Submarine.DrawDamageable(spriteBatch, DamageEffect, false); + spriteBatch.End(); + + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontDamageable", sw.ElapsedTicks); + sw.Restart(); + graphics.SetRenderTarget(null); - GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam, renderTarget); + GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam, renderTargetDamageable); sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:Lighting", sw.ElapsedTicks); @@ -330,24 +345,24 @@ static bool IsFromOutpostDrawnBehindSubs(Entity e) Level.Loaded.DrawBack(graphics, spriteBatch, cam); } - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform); Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null) && IsFromOutpostDrawnBehindSubs(e)); spriteBatch.End(); //draw alpha blended particles that are in water and behind subs #if LINUX || OSX - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform); #else - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform); #endif GameMain.ParticleManager.Draw(spriteBatch, true, false, Particles.ParticleBlendState.AlphaBlend); spriteBatch.End(); //draw additive particles that are in water and behind subs - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, null, DepthStencilState.None, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform); GameMain.ParticleManager.Draw(spriteBatch, true, false, Particles.ParticleBlendState.Additive); spriteBatch.End(); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.None); spriteBatch.Draw(renderTarget, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); spriteBatch.End(); @@ -366,8 +381,8 @@ static bool IsFromOutpostDrawnBehindSubs(Entity e) GraphicsQuad.Render(); //Draw the rest of the structures, characters and front structures - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); - Submarine.DrawBack(spriteBatch, false, e => !(e is Structure) || e.SpriteDepth < 0.9f); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform); + Submarine.DrawBack(spriteBatch, false, e => e is not Structure || e.SpriteDepth < 0.9f); DrawCharacters(deformed: false, firstPass: true); spriteBatch.End(); @@ -375,7 +390,7 @@ static bool IsFromOutpostDrawnBehindSubs(Entity e) GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:BackCharactersItems", sw.ElapsedTicks); sw.Restart(); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform); DrawCharacters(deformed: true, firstPass: true); DrawCharacters(deformed: true, firstPass: false); DrawCharacters(deformed: false, firstPass: false); @@ -420,19 +435,19 @@ void DrawCharacters(bool deformed, bool firstPass) GraphicsQuad.Render(); //draw alpha blended particles that are inside a sub - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.DepthRead, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.DepthRead, transformMatrix: cam.Transform); GameMain.ParticleManager.Draw(spriteBatch, true, true, Particles.ParticleBlendState.AlphaBlend); spriteBatch.End(); graphics.SetRenderTarget(renderTarget); //draw alpha blended particles that are not in water - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.DepthRead, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.DepthRead, transformMatrix: cam.Transform); GameMain.ParticleManager.Draw(spriteBatch, false, null, Particles.ParticleBlendState.AlphaBlend); spriteBatch.End(); //draw additive particles that are not in water - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, null, DepthStencilState.None, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform); GameMain.ParticleManager.Draw(spriteBatch, false, null, Particles.ParticleBlendState.Additive); spriteBatch.End(); @@ -449,20 +464,10 @@ void DrawCharacters(bool deformed, bool firstPass) GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontParticles", sw.ElapsedTicks); sw.Restart(); - DamageEffect.CurrentTechnique = DamageEffect.Techniques["StencilShader"]; - spriteBatch.Begin(SpriteSortMode.Immediate, - BlendState.NonPremultiplied, SamplerState.LinearWrap, - null, null, - DamageEffect, - cam.Transform); - Submarine.DrawDamageable(spriteBatch, DamageEffect, false); - spriteBatch.End(); - - sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontDamageable", sw.ElapsedTicks); - sw.Restart(); + GraphicsQuad.UseBasicEffect(renderTargetDamageable); + GraphicsQuad.Render(); - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform); Submarine.DrawFront(spriteBatch, false, null); spriteBatch.End(); @@ -471,7 +476,7 @@ void DrawCharacters(bool deformed, bool firstPass) sw.Restart(); //draw additive particles that are inside a sub - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, null, DepthStencilState.Default, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, depthStencilState: DepthStencilState.Default, transformMatrix: cam.Transform); GameMain.ParticleManager.Draw(spriteBatch, true, true, Particles.ParticleBlendState.Additive); foreach (var discharger in Items.Components.ElectricalDischarger.List) { @@ -487,7 +492,7 @@ void DrawCharacters(bool deformed, bool firstPass) GraphicsQuad.Render(); } - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, depthStencilState: DepthStencilState.None, transformMatrix: cam.Transform); foreach (Character c in Character.CharacterList) { c.DrawFront(spriteBatch, cam); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 6c398a78d5..629189c134 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -363,7 +363,7 @@ Vector2 GetRandomizeButtonRelativeSize() => 0.2f * seedContainer.Rect.Width > se s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && !nonPlayerFiles.Any(f => f.Path == s.FilePath)); GameSession gameSession = new GameSession(subInfo, Option.None, CampaignDataPath.Empty, GameModePreset.TestMode, CampaignSettings.Empty, null); - gameSession.StartRound(Level.Loaded.LevelData); + gameSession.StartRound(Level.Loaded.LevelData, mirrorLevel.Selected); (gameSession.GameMode as TestGameMode).OnRoundEnd = () => { GameMain.LevelEditorScreen.Select(); @@ -560,9 +560,13 @@ private void UpdateLevelObjectsList() new Vector2(relWidth, relWidth * ((float)levelObjectList.Content.Rect.Width / levelObjectList.Content.Rect.Height)), levelObjectList.Content.RectTransform) { MinSize = new Point(0, 60) }, style: "ListBoxElementSquare") { - UserData = levelObjPrefab + UserData = levelObjPrefab, + ToolTip = levelObjPrefab.Name + }; + var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), style: null) + { + CanBeFocused = false }; - var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), style: null); GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), text: ToolBox.LimitString(levelObjPrefab.Name, GUIStyle.SmallFont, paddedFrame.Rect.Width), textAlignment: Alignment.Center, font: GUIStyle.SmallFont) @@ -876,9 +880,22 @@ private void SortLevelObjectsList(LevelData levelData) { var levelObj = levelObjFrame.UserData as LevelObjectPrefab; float commonness = levelObj.GetCommonness(levelData); - levelObjFrame.Color = commonness > 0.0f ? GUIStyle.Green * 0.4f : Color.Transparent; - levelObjFrame.SelectedColor = commonness > 0.0f ? GUIStyle.Green * 0.6f : Color.White * 0.5f; - levelObjFrame.HoverColor = commonness > 0.0f ? GUIStyle.Green * 0.7f : Color.White * 0.6f; + + Color color = GUIStyle.Green; + + if (commonness > 0.0f && levelData?.GenerationParams != null) + { + if (levelObj.MinSurfaceWidth > levelData.GenerationParams.CellSubdivisionLength && + levelObj.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.Wall)) + { + color = Color.Orange; + levelObjFrame.ToolTip = $"Potential issue: the level walls in \"{levelData.GenerationParams.Identifier}\" are set to be subdivided every {levelData.GenerationParams.CellSubdivisionLength} pixels, but the level object requires wall segments of at least {levelObj.MinSurfaceWidth} px. The object may be rarer than intended (or fail to spawn at all) in the level."; + } + } + + levelObjFrame.Color = commonness > 0.0f ? color * 0.4f : Color.Transparent; + levelObjFrame.SelectedColor = commonness > 0.0f ? color * 0.6f : Color.White * 0.5f; + levelObjFrame.HoverColor = commonness > 0.0f ? color * 0.7f : Color.White * 0.6f; levelObjFrame.GetAnyChild().Color = commonness > 0.0f ? Color.White : Color.DarkGray; if (commonness <= 0.0f) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index af3705aafc..b89849fb3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -790,12 +790,16 @@ private GUIComponent CreateGameModeSettingsPanel(GUIComponent parent) var winScoreContainer = CreateLabeledSlider(gameModeSettingsContent, headerTag: string.Empty, valueLabelTag: string.Empty, tooltipTag: "ServerSettingsWinScorePvPTooltip", out var winScorePvPSlider, out var winScorePvPSliderLabel); - winScorePvPSlider.Range = new Vector2(1, 1000); - winScorePvPSlider.StepValue = 1; + winScorePvPSlider.Range = new Vector2(10, 1000); + winScorePvPSlider.StepValue = 10; winScorePvPSlider.OnMoved = (scrollBar, _) => { if (scrollBar.UserData is not GUITextBlock text) { return false; } text.Text = TextManager.GetWithVariable("ServerSettingsWinScoreValuePvP", "[value]", ((int)Math.Round(scrollBar.BarScrollValue, digits: 0)).ToString()); + return true; + }; + winScorePvPSlider.OnReleased = (scrollBar, _) => + { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; @@ -931,7 +935,7 @@ private GUIComponent CreateGameModeSettingsPanel(GUIComponent parent) //do this before adding the contents, otherwise they get disabled too (and we just want to disable the dropdown itself) clientDisabledElements.AddRange(biomeHolder.GetAllChildren()); biomeDropdown.AddItem(TextManager.Get("random"), "Random".ToIdentifier()); - foreach (var biome in Biome.Prefabs) + foreach (var biome in Biome.Prefabs.OrderBy(b => b.MinDifficulty)) { if (biome.IsEndBiome) { continue; } biomeDropdown.AddItem(biome.DisplayName, biome.Identifier); @@ -1195,7 +1199,7 @@ private GUIComponent CreateGeneralSettingsPanel(GUIComponent parent) var respawnModeHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; respawnModeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), respawnModeHolder.RectTransform), TextManager.Get("RespawnMode"), wrap: true); respawnModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.6f, 1.0f), respawnModeHolder.RectTransform)); - foreach (var respawnMode in Enum.GetValues(typeof(RespawnMode)).Cast()) + foreach (var respawnMode in Enum.GetValues(typeof(RespawnMode)).Cast().Where(rm => rm != RespawnMode.None)) { respawnModeSelection.AddElement(respawnMode, TextManager.Get($"respawnmode.{respawnMode}"), TextManager.Get($"respawnmode.{respawnMode}.tooltip")); } @@ -3005,10 +3009,13 @@ private void RefreshOutpostDropdown() } } outpostDropdown.ListBox.Select(prevSelected); + GameMain.Client.ServerSettings.AssignGUIComponent(nameof(ServerSettings.SelectedOutpostName), outpostDropdown); } else { outpostDropdown.Parent.Visible = false; + //remove assignment, we shouldn't try selecting the outpost when there's none to select + GameMain.Client.ServerSettings.AssignGUIComponent(nameof(ServerSettings.SelectedOutpostName), null); } outpostDropdownUpToDate = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index 947330466b..08bc32e599 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -1003,6 +1003,8 @@ private bool AllLanguagesVisible private bool ShouldShowServer(ServerInfo serverInfo) { + if (serverInfo == null) { return false; } + #if !DEBUG //never show newer versions //(ignore revision number, it doesn't affect compatibility) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 5626151998..60d3eff907 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -56,7 +56,8 @@ public enum WarningType WaterInHulls, LowOxygenOutputWarning, TooLargeForEndGame, - NotEnoughContainers + NotEnoughContainers, + NoSuitableBrainRooms } public static Vector2 MouseDragStart = Vector2.Zero; @@ -1308,7 +1309,7 @@ private void CreateEntityElement(MapEntityPrefab ep, int entitiesPerRow, GUIComp } GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), - text: name, textAlignment: Alignment.Center, font: GUIStyle.SmallFont) + text: RichString.Rich(name), textAlignment: Alignment.Center, font: GUIStyle.SmallFont) { CanBeFocused = false }; @@ -1320,7 +1321,7 @@ private void CreateEntityElement(MapEntityPrefab ep, int entitiesPerRow, GUIComp textBlock.Text = frame.ToolTip = ep.Identifier.Value; textBlock.TextColor = GUIStyle.Red; } - textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); + textBlock.Text = ToolBox.LimitString(textBlock.Text.SanitizedString, textBlock.Font, textBlock.Rect.Width); if (ep.Category == MapEntityCategory.ItemAssembly && ep.ContentPackage?.Files.Length == 1 @@ -5792,7 +5793,7 @@ public override void Update(double deltaTime) foreach (LightComponent lightComponent in item.GetComponents()) { lightComponent.Light.Color = - (item.body == null || item.body.Enabled || item.ParentInventory is ItemInventory { Container.HideItems: true }) && + (item.body == null || item.body.Enabled || item.ParentInventory is ItemInventory { Container.HideItems: false }) && /*the light is only visible when worn -> can't be visible in the editor*/ lightComponent.Parent is not Wearable ? lightComponent.LightColor : diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 44dcd3c798..de3bf0e22e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -107,6 +107,11 @@ private void SwitchContent(GUIFrame newContent) public void SelectTab(Tab tab) { + if (tab == Tab.AudioAndVC && CurrentDeviceMismatchesDisplayed()) + { + CreateAudioAndVCTab(refresh: true); + } + CurrentTab = tab; SwitchContent(tabContents[tab].Content); tabber.Children.ForEach(c => @@ -134,6 +139,11 @@ private void AddButtonToTabber(Tab tab, GUIFrame content) private GUIFrame CreateNewContentFrame(Tab tab) { + if (tabContents.TryGetValue(tab, out (GUIButton Button, GUIFrame Content) tabContent)) + { + return tabContent.Content; + } + var content = new GUIFrame(new RectTransform(Vector2.One * 0.95f, contentFrame.RectTransform, Anchor.Center, Pivot.Center), style: null); AddButtonToTabber(tab, content); return content; @@ -180,7 +190,7 @@ private static void DropdownEnum(GUILayoutGroup parent, Func(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) { - var dropdown = new GUIDropDown(NewItemRectT(parent)); + var dropdown = new GUIDropDown(NewItemRectT(parent), elementCount: values.Count); values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v, toolTip: tooltipFunc?.Invoke(v) ?? null)); int childIndex = values.IndexOf(currentValue); dropdown.Select(childIndex); @@ -269,6 +279,11 @@ private void CreateGraphicsTab() DropdownEnum(left, (m) => TextManager.Get($"{m}"), null, unsavedConfig.Graphics.DisplayMode, v => unsavedConfig.Graphics.DisplayMode = v); Spacer(left); + var displayLabel = Label(left, TextManager.Get("TargetDisplay"), GUIStyle.SubHeadingFont); + displayLabel.ToolTip = TextManager.Get("TargetDisplay.Tooltip"); + Dropdown(left, m => TextManager.GetWithVariables(m == 0 ? "PrimaryDisplayFormat" : "SecondaryDisplayFormat", ("[num]", m.ToString()), ("[name]", Display.GetDisplayName(m))), null, Enumerable.Range(0, Display.GetNumberOfDisplays()).ToArray(), unsavedConfig.Graphics.Display, v => unsavedConfig.Graphics.Display = v); + Spacer(left); + Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, v => unsavedConfig.Graphics.VSync = v); Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, v => unsavedConfig.Graphics.CompressTextures = v); Spacer(right); @@ -347,17 +362,45 @@ private static void GetAudioDevices(int listSpecifier, int defaultSpecifier, out current = list[0]; } } - - private void CreateAudioAndVCTab() + + private static bool IsCurrentDevice(string savedDeviceName, int deviceType) + { + try + { + string currentDevice = Alc.GetString(IntPtr.Zero, deviceType); + if (string.IsNullOrEmpty(savedDeviceName) || string.IsNullOrEmpty(currentDevice)) + { + return false; + } + return currentDevice.Equals(savedDeviceName, StringComparison.OrdinalIgnoreCase); + } + catch (Exception ex) + { + Console.WriteLine($"Error checking output device name: {ex.Message}"); + return false; + } + } + + private static bool CurrentDeviceMismatchesDisplayed() + { + return !IsCurrentDevice(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, Alc.CaptureDefaultDeviceSpecifier) || + !IsCurrentDevice(GameSettings.CurrentConfig.Audio.AudioOutputDevice, Alc.DefaultDeviceSpecifier); + } + + public void CreateAudioAndVCTab(bool refresh = false) { if (GameMain.Client == null - && VoipCapture.Instance == null) + && (refresh || VoipCapture.Instance == null)) { string currDevice = unsavedConfig.Audio.VoiceCaptureDevice; GetAudioDevices(Alc.CaptureDeviceSpecifier, Alc.CaptureDefaultDeviceSpecifier, out var deviceList, ref currDevice); if (deviceList.Any()) { + if (VoipCapture.Instance is VoipCapture currentCaptureInstance) + { + currentCaptureInstance.Dispose(); + } VoipCapture.Create(unsavedConfig.Audio.VoiceCaptureDevice); } if (VoipCapture.Instance == null) @@ -367,7 +410,10 @@ private void CreateAudioAndVCTab() } GUIFrame content = CreateNewContentFrame(Tab.AudioAndVC); - + if (refresh) + { + content.ClearChildren(); + } var (audio, voiceChat) = CreateSidebars(content, split: true); static void audioDeviceElement( @@ -401,6 +447,15 @@ static void audioDeviceElement( string currentOutputDevice = unsavedConfig.Audio.AudioOutputDevice; audioDeviceElement(audio, v => unsavedConfig.Audio.AudioOutputDevice = v, Alc.OutputDevicesSpecifier, Alc.DefaultDeviceSpecifier, ref currentOutputDevice); + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), audio.RectTransform), text: TextManager.Get("RefreshAudioDevices"), style: "GUIButtonSmall") + { + ToolTip = TextManager.Get("RefreshAudioDevicesToolTip"), + OnClicked = (btn, obj) => + { + CreateAudioAndVCTab(refresh: true); + return true; + } + }; Spacer(audio); Label(audio, TextManager.Get("SoundVolume"), GUIStyle.SubHeadingFont); @@ -443,6 +498,15 @@ static void audioDeviceElement( string currentInputDevice = unsavedConfig.Audio.VoiceCaptureDevice; audioDeviceElement(voiceChat, v => unsavedConfig.Audio.VoiceCaptureDevice = v, Alc.CaptureDeviceSpecifier, Alc.CaptureDefaultDeviceSpecifier, ref currentInputDevice); + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), voiceChat.RectTransform), text: TextManager.Get("RefreshAudioDevices"), style: "GUIButtonSmall") + { + ToolTip = TextManager.Get("RefreshAudioDevicesToolTip"), + OnClicked = (btn, obj) => + { + CreateAudioAndVCTab(refresh: true); + return true; + } + }; Spacer(voiceChat); Label(voiceChat, TextManager.Get("VCInputMode"), GUIStyle.SubHeadingFont); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs index 96df436265..5647038aa5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs @@ -258,11 +258,11 @@ public static IReadOnlyList GetStringList(IntPtr device, int param) public static void GetInteger(IntPtr device, int param, out int data) { - int[] dataArr = new int[1]; - GCHandle handle = GCHandle.Alloc(dataArr,GCHandleType.Pinned); + data = 0; // (Optimization: let's pin an integer on the stack instead of an array on the heap, which previously allocated almost a GB of memory) + GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned); GetIntegerv(device, param, 1, handle.AddrOfPinnedObject()); + data = Marshal.ReadInt32(handle.AddrOfPinnedObject()); handle.Free(); - data = dataArr[0]; } #endregion diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index c5745b5dc5..781d0f17ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -670,6 +670,7 @@ public void Update() DebugConsole.ThrowError("Playback device has been disconnected. You can select another available device in the settings."); SetAudioOutputDevice(""); Disconnected = true; + TryRefreshDevice(); return; } } @@ -885,5 +886,52 @@ public void Dispose() throw new Exception("Failed to close ALC device!"); } } + + public static void TryRefreshDevice() + { + DebugConsole.NewMessage("Refreshing audio playback device"); + + List deviceList = Alc.GetStringList(IntPtr.Zero, Alc.OutputDevicesSpecifier).ToList(); + int alcError = Alc.GetError(IntPtr.Zero); + if (alcError != Alc.NoError) + { + DebugConsole.ThrowError("Failed to list available audio playback devices: " + alcError.ToString()); + return; + } + + if (deviceList.Any()) + { + string device; + + if (deviceList.Find(n => n.Equals(GameSettings.CurrentConfig.Audio.AudioOutputDevice, StringComparison.OrdinalIgnoreCase)) + is string availablePreviousDevice) + { + DebugConsole.NewMessage($" Previous device choice available: {availablePreviousDevice}"); + device = availablePreviousDevice; + } + else + { + device = Alc.GetString(IntPtr.Zero, Alc.DefaultDeviceSpecifier); + DebugConsole.NewMessage($" Reverting to default device: {device}"); + } + + if (string.IsNullOrEmpty(device)) + { + device = deviceList[0]; + DebugConsole.NewMessage($" No default device found, resorting to first available device: {device}"); + } + + // Save the new device choice and initialize it + var currentConfig = GameSettings.CurrentConfig; + currentConfig.Audio.AudioOutputDevice = device; + GameSettings.SetCurrentConfig(currentConfig); + GameMain.SoundManager.InitializeAlcDevice(device); + } + + if (GUI.SettingsMenuOpen) + { + SettingsMenu.Instance?.CreateAudioAndVCTab(true); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs index c3a929ec95..c2f07cf96f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs @@ -48,6 +48,8 @@ public bool IsFiltered(ServerInfo info) private static bool IsFiltered(ServerInfo info, SpamServerFilterType type, string value) { + if (info == null) { return true; } + string desc = info.ServerMessage, name = info.ServerName; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 091591c631..77cb3016d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -298,24 +298,27 @@ public void DrawSilhouette(SpriteBatch spriteBatch, Vector2 pos, Vector2 origin, /// Last version of the game that had broken handling of sprites that were scaled, flipped and offset /// public static readonly Version LastBrokenTiledSpriteGameVersion = new Version(major: 1, minor: 2, build: 7, revision: 0); + + public void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, float rotation = 0f, Vector2? origin = null, Color? color = null, Vector2? startOffset = null, Vector2? textureScale = null, float? depth = null) + { + DrawTiled(spriteBatch, position, targetSize, effects, rotation, origin, color, startOffset, textureScale, depth); + } public void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, + SpriteEffects spriteEffects, float rotation = 0f, Vector2? origin = null, Color? color = null, Vector2? startOffset = null, Vector2? textureScale = null, - float? depth = null, - SpriteEffects? spriteEffects = null) + float? depth = null) { if (Texture == null) { return; } - spriteEffects ??= effects; - - bool flipHorizontal = spriteEffects.Value.HasFlag(SpriteEffects.FlipHorizontally); - bool flipVertical = spriteEffects.Value.HasFlag(SpriteEffects.FlipVertically); + bool flipHorizontal = (spriteEffects & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally; // optimized from spriteEffects.HasFlag(SpriteEffects.FlipHorizontally) + bool flipVertical = (spriteEffects & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically; // optimized from spriteEffects.HasFlag(SpriteEffects.FlipVertically) float addedRotation = rotation + this.rotation; if (flipHorizontal != flipVertical) { addedRotation = -addedRotation; } @@ -354,7 +357,7 @@ void drawSection(Vector2 slicePos, Rectangle sliceRect) rotation: addedRotation, origin: Vector2.Zero, scale: scale, - effects: spriteEffects.Value, + effects: spriteEffects, layerDepth: depth ?? this.depth); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index 2316c122ca..b23b99d608 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -777,8 +777,7 @@ void AskToDeleteLocal() { GUIMessageBox msgBox = new(TextManager.Get("DeleteMods"), TextManager.GetWithVariables("DeleteModsConfirm", ("[amount]", selectedMods.Length.ToString())), new LocalizedString[] { TextManager.Get("Confirm"), TextManager.Get("Cancel")}, textAlignment: Alignment.TopCenter); - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += (_, _) => + msgBox.Buttons[0].OnClicked += (_, _) => { foreach (ContentPackage mod in selectedMods) { @@ -788,6 +787,7 @@ void AskToDeleteLocal() msgBox.Close(); return true; }; + msgBox.Buttons[1].OnClicked += msgBox.Close; } }); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/MathUtils.cs index d8b4d38d11..473a29f705 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/MathUtils.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using System.Collections.Generic; namespace Barotrauma @@ -16,4 +17,22 @@ public int Compare(Lights.SegmentPoint a, Lights.SegmentPoint b) return -CompareCCW.Compare(a.WorldPos, b.WorldPos, center); } } + + public class CompareVertexPositionColorCCW : IComparer + { + private Vector2 center; + + public CompareVertexPositionColorCCW(Vector2 center) + { + this.center = center; + } + public int Compare(VertexPositionColor a, VertexPositionColor b) + { + return -CompareCW.Compare(new Vector2(a.Position.X, a.Position.Y), new Vector2(b.Position.X, b.Position.Y), center); + } + public static int Compare(VertexPositionColor a, VertexPositionColor b, Vector2 center) + { + return -CompareCW.Compare(new Vector2(a.Position.X, a.Position.Y), new Vector2(b.Position.X, b.Position.Y), center); + } + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index 7b13a797d4..2958609f69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -76,11 +76,14 @@ public static void Create(Character character) float zoom = (float)texWidth / (float)boundingBox.Width; int texHeight = (int)(zoom * boundingBox.Height); - Camera cam = new Camera(); + Camera cam = new Camera() + { + AutoUpdateToScreenResolution = false, + MaxZoom = zoom, + MinZoom = zoom * 0.5f, + Zoom = zoom + }; cam.SetResolution(new Point(texWidth, texHeight)); - cam.MaxZoom = zoom; - cam.MinZoom = zoom * 0.5f; - cam.Zoom = zoom; cam.Position = boundingBox.Center.ToVector2(); cam.UpdateTransform(false); diff --git a/Barotrauma/BarotraumaClient/Content/Effects/damageshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/damageshader.xnb index 4b95d883feb67d25dc3adad5278be84be922ef21..929e4e43c252344132ee3261996e33ac086eaa64 100644 GIT binary patch delta 628 zcmZXQze}585XbM{-#RpJV}?Z3iV#9+hYljIQRr)w)IzO}aqAEYfl^B&h`1yngJ=-! z106C|)I~&s{sV4R+oe;bbm$Op6r^ME?e|{ZppXadecrqK-g9@Kz4(6IF1*gIn4I3W z>!r^Q(cFuC!Qros-4mu^GKq*?(h{U6$rvDw{^&$Ah1_(HUq1X4{J1^8-kAS6vwFc$ zO3uLj3|2(IgMSE{T6#bJ6uuZ~LGV}oHq!k};)VyYN66s6LKJJMnCYdMVsHW&?O+UD zBd5aRt2NGeLg1OFKBHr*-*6+YqwxkspL*21ugZ9z0`L!_KBzrTSQTQYZ!kR zKt0i(TQ{zY$<&}Vq-xqMv-+k@;A`t+lf54aFrdt9(H$3U=vSuX2^`F3*PxS=jr3X9 zRaL#oF*RU(STKoEWJzHo!g;)YG&;0=#i_^jmz6~FUnkeW=^@ZY=R$-t>?$IlI|I&4 zQpO5TH%$K_lp!ubAy8SpF*A4@KsHT3wTHZq;u5SINUa0pEilwAfpKb$CG3%VOUfv+ aTY~=;rtMY|oW$(5|e6mVOlsv6weoB=2#xn~5R){%@%DW+VWSYNv!fdE{ zQRZ5W@!#VNm)cgq2skdQWZeucn~#rs)nZ)5hJsCU?KhfeMysJM&8~AGFyH<6Z#@Ww zz7?-C8JpzGFTZxaHcpFkSC1F#_1=1G)=DzR#yMY5N|Jn}*1{(F**v&eaZIP~Vr)?- zX>l;tw2e=hk2=d(X#H3hGKV~>K;;7QKkI2T?@iNq-Lv-Zyy0}v_m3s}vX-S+eqyQZ S*-~Jpf)`pGe`(pw1dS)TdUW9c diff --git a/Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.xnb index 5cffd1d1a6f5697268c8d718c6bff009fdf87944..52ac9ea0f5e9de7fec834a343aa8f6ede00853d9 100644 GIT binary patch delta 296 zcmaDP_(*WVRM!h`Mmw1V3X0=P^D@)&i*k$O%Tkj~92i&_7#J7@7?>0om<<>(!IMlv7gEGV@XufOeD_ zD3l@OCiAgPnEaJVjL~2+53?ep;bcALN=Bo}E0|RnjVGUF)@3Z4{F_-1NNTav39>RU zGBU6N%_ufxV+2|)2ecEY%!g5XasX@QWMd{0Y+W{)o0)I&AvTf8y38_@Ke5Fyl^IM9 RU~dvYv4oL0om<E@Sd!4i-HiY0gq7$i~3P$iNCT zqu7w05oi_n#9PxR$FgQle$JQ*G-?{7(d4Uaia_nw%+iyE*khQI4JQ|{HyNPlV`Si9 ZWMBarotrauma FakeFish, Undertow Games Barotrauma - 1.6.19.1 + 1.7.7.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma @@ -55,7 +55,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 9b6e9ca4fc..681948e489 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.6.19.1 + 1.7.7.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma @@ -57,7 +57,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx index 3ec9646515..42fa721f9d 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx @@ -1,4 +1,4 @@ - + Texture2D xTexture; sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; @@ -7,8 +7,6 @@ sampler StencilSampler = sampler_state { Texture = ; }; float4 solidColor; -float4 inColor; - float aCutoff; float aMultiplier; @@ -33,7 +31,7 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord float4 solidColorStencil(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { - float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor; + float4 c = xTexture.Sample(TextureSampler, texCoord) * color; float4 stencilColor = xStencil.Sample(StencilSampler, texCoord); float aDiff = stencilColor.a - aCutoff; diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx index 69370113cb..4601a6ed9d 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx @@ -1,4 +1,4 @@ - + Texture xTexture; sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; @@ -7,8 +7,6 @@ sampler StencilSampler = sampler_state { Texture = ; }; float4 solidColor; -float4 inColor; - float aCutoff; float aMultiplier; @@ -17,7 +15,7 @@ float cMultiplier; float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { - float4 c = tex2D(TextureSampler, texCoord) * inColor; + float4 c = tex2D(TextureSampler, texCoord) * color; float4 stencilColor = tex2D(StencilSampler, texCoord); float aDiff = stencilColor.a - aCutoff; @@ -33,7 +31,7 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord float4 solidColorStencil(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { - float4 c = tex2D(TextureSampler, texCoord) * inColor; + float4 c = tex2D(TextureSampler, texCoord) * color; float4 stencilColor = tex2D(StencilSampler, texCoord); float aDiff = stencilColor.a - aCutoff; diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 584b9485d6..7dc6f008e9 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.6.19.1 + 1.7.7.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma @@ -63,7 +63,7 @@ - + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 7bb8aad501..38fca056f8 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.6.19.1 + 1.7.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -55,7 +55,7 @@ - + diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 0e91806d91..40e5ebb200 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.6.19.1 + 1.7.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -59,7 +59,7 @@ - + diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 86467b4210..fc53217c5d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -1,6 +1,7 @@ using Barotrauma.Networking; using System.Linq; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -85,5 +86,16 @@ partial void OnTalentGiven(TalentPrefab talentPrefab) { GameServer.Log($"{GameServer.CharacterLogName(this)} has gained the talent '{talentPrefab.DisplayName}'", ServerLog.MessageType.Talent); } + + private void SyncInGameEditables(Item item) + { + foreach (ItemComponent itemComponent in item.Components) + { + foreach (var serializableProperty in SerializableProperty.GetProperties(itemComponent)) + { + GameMain.Server.CreateEntityEvent(item, new Item.ChangePropertyEventData(serializableProperty, itemComponent)); + } + } + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 6d8c1801ee..9ba3a6cd6b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -427,21 +427,33 @@ public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c) tempBuffer.WriteSingle(SimPosition.Y); float MaxVel = NetConfig.MaxPhysicsBodyVelocity; AnimController.Collider.LinearVelocity = new Vector2( - MathHelper.Clamp(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel), - MathHelper.Clamp(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel)); + NetConfig.Quantize(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel, 12), + NetConfig.Quantize(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12)); tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel, 12); tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12); - bool fixedRotation = AnimController.Collider.FarseerBody.FixedRotation || !AnimController.Collider.PhysEnabled; + AnimController.TargetMovement = new Vector2( + NetConfig.Quantize(AnimController.TargetMovement.X, -Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12), + NetConfig.Quantize(AnimController.TargetMovement.Y, -Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12)); + tempBuffer.WriteRangedSingle(AnimController.TargetMovement.X, -Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12); + tempBuffer.WriteRangedSingle(AnimController.TargetMovement.Y, -Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12); + + bool fixedRotation = AnimController.Collider.FarseerBody.FixedRotation; tempBuffer.WriteBoolean(fixedRotation); if (!fixedRotation) { tempBuffer.WriteSingle(AnimController.Collider.Rotation); float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; - AnimController.Collider.AngularVelocity = NetConfig.Quantize(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel, 8); + AnimController.Collider.AngularVelocity = + AnimController.Collider.PhysEnabled ? + 0.0f : + NetConfig.Quantize(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel, 8); tempBuffer.WriteRangedSingle(MathHelper.Clamp(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel), -MaxAngularVel, MaxAngularVel, 8); } + + tempBuffer.WriteBoolean(AnimController.IgnorePlatforms); + bool writeStatus = healthUpdateTimer <= 0.0f; tempBuffer.WriteBoolean(writeStatus); if (writeStatus) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Limb.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Limb.cs new file mode 100644 index 0000000000..b4e16fe328 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Limb.cs @@ -0,0 +1,70 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Limb : ISerializableEntity, ISpatialEntity + { + /// + /// An invisible "ghost body" used for doing lag compensation server side by allowing clients' shots to hit bodies at the positions where + /// they "used to be" back when the client fired a weapon. + /// + public PhysicsBody LagCompensatedBody { get; private set; } + + /// + /// A queue of past positions of the limb. + /// + public Queue MemState { get; } = new Queue(); + + partial void InitProjSpecific(ContentXElement element) + { + LagCompensatedBody = new PhysicsBody(Params, findNewContacts: false) + { + BodyType = FarseerPhysics.BodyType.Static, + CollisionCategories = Physics.CollisionLagCompensationBody, + CollidesWith = Physics.CollisionNone, + UserData = this + }; + } + + partial void UpdateProjSpecific(float deltaTime) + { + if (GameMain.Server == null) { return; } + + MemState.Enqueue(new PosInfo(body.SimPosition, body.Rotation, body.LinearVelocity, body.AngularVelocity, (float)Timing.TotalTime)); + + //clear old states + while ( + MemState.Any() && + MemState.Peek().Timestamp < Timing.TotalTime - GameMain.Server.ServerSettings.MaxLagCompensationSeconds) + { + MemState.Dequeue(); + } + } + + public static void SetLagCompensatedBodyPositions(Client client) + { + if (GameMain.Server == null) { return; } + //convert from milliseconds to seconds, assume latency is symmetrical (time from client to server is half of the roundtrip time / ping) + float latency = client.Ping / 1000.0f / 2; + float time = (float)Timing.TotalTime - MathUtils.Min(latency, GameMain.Server.ServerSettings.MaxLagCompensationSeconds); + + foreach (var character in Character.CharacterList) + { + foreach (var limb in character.AnimController.Limbs) + { + if (limb.body.Enabled == false || limb.IgnoreCollisions) { continue; } + var matchingState = limb.MemState.FirstOrDefault(l => l.Timestamp <= time); + if (matchingState == null) { continue; } + limb.LagCompensatedBody.SetTransformIgnoreContacts(matchingState.Position, matchingState.Rotation ?? 0.0f); + } + } + } + + partial void RemoveProjSpecific() + { + LagCompensatedBody.Remove(); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 0eac855518..4c047101bf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -2729,6 +2729,11 @@ string eventDebugStr(ServerEntityEvent ev) { GameMain.Server.CreateEntityEvent(wall); } + foreach (Hull hull in Hull.HullList) + { + if (hull.IdFreed) { continue; } + hull.CreateStatusEvent(); + } })); commands.Add(new Command("stallfiletransfers", "stallfiletransfers [seconds]: A debug command that makes all file transfers take at least the specified duration.", (string[] args) => { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index 39fa32cab9..322aeee5be 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using System; using System.Collections.Generic; using System.Linq; @@ -60,20 +61,35 @@ public void IgnoreClient(Client c, float seconds) private bool IsBlockedByAnotherConversation(IEnumerable targets, float duration) { - foreach (Entity e in targets) + if (targets == null || targets.None()) { - if (e is not Character character || !character.IsRemotePlayer) { continue; } - Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); - if (targetClient != null) + //if the action doesn't target anyone in specific, it's shown to every client + foreach (var client in GameMain.Server.ConnectedClients) { - if (lastActiveAction.ContainsKey(targetClient) && - lastActiveAction[targetClient].ParentEvent != ParentEvent && - Timing.TotalTime < lastActiveAction[targetClient].lastActiveTime + duration) - { - return true; - } + if (IsBlockedByAnotherConversation(client, duration)) { return true; } } } + else + { + foreach (Entity e in targets) + { + if (e is not Character character || !character.IsRemotePlayer) { continue; } + Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + if (targetClient != null && IsBlockedByAnotherConversation(targetClient, duration)) { return true; } + } + } + return false; + } + + private bool IsBlockedByAnotherConversation(Client targetClient, float duration) + { + if (lastActiveAction.ContainsKey(targetClient) && + !lastActiveAction[targetClient].ParentEvent.IsFinished && + lastActiveAction[targetClient].ParentEvent != ParentEvent && + Timing.TotalTime < lastActiveAction[targetClient].lastActiveTime + duration) + { + return true; + } return false; } @@ -91,6 +107,7 @@ partial void ShowDialog(Character speaker, Character targetCharacter) { targetClients.Add(targetClient); lastActiveAction[targetClient] = this; + lastActiveTime = Timing.TotalTime; ServerWrite(speaker, targetClient, interrupt); } } @@ -105,6 +122,7 @@ partial void ShowDialog(Character speaker, Character targetCharacter) { targetClients.Add(c); lastActiveAction[c] = this; + lastActiveTime = Timing.TotalTime; ServerWrite(speaker, c, interrupt); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index 21dc40f61f..736c985424 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -1,6 +1,7 @@ #nullable enable using Barotrauma.Extensions; using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; @@ -193,6 +194,26 @@ private void CheckTargetSubmarineControl(float deltaTime) } } + public void AddToScore(CharacterTeamType team, int amount) + { + if (!HasWinScore) { return; } + int index; + switch (team) + { + case CharacterTeamType.Team1: + index = 0; + break; + case CharacterTeamType.Team2: + index = 1; + break; + default: + DebugConsole.AddSafeError($"Attempted to increase the score of an invalid team ({team})."); + return; + } + Scores[index] = MathHelper.Clamp(Scores[index] + amount, 0, WinScore); + GameMain.Server?.UpdateMissionState(this); + } + private void AddKill(Character character) { kills.Add(new KillCount(character, character.CauseOfDeath?.Killer)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs index ec766fd9aa..43efff6f25 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Barotrauma.Networking; namespace Barotrauma @@ -12,9 +13,9 @@ public override void ServerWriteInitial(IWriteMessage msg, Client c) { item.WriteSpawnData(msg, item.ID, - parentInventoryIDs.ContainsKey(item) ? parentInventoryIDs[item] : Entity.NullEntityID, - parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0, - inventorySlotIndices.ContainsKey(item) ? inventorySlotIndices[item] : -1); + parentInventoryIDs.GetValueOrDefault(item, Entity.NullEntityID), + parentItemContainerIndices.GetValueOrDefault(item, (byte)0), + inventorySlotIndices.GetValueOrDefault(item, -1)); } ServerWriteScanTargetStatus(msg); } @@ -30,7 +31,7 @@ private void ServerWriteScanTargetStatus(IWriteMessage msg) msg.WriteByte((byte)scanTargets.Count); foreach (var kvp in scanTargets) { - msg.WriteUInt16(kvp.Key != null ? kvp.Key.ID : Entity.NullEntityID); + msg.WriteUInt16(kvp.Key?.ID ?? Entity.NullEntityID); msg.WriteBoolean(kvp.Value); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs index 0e1e1a8399..3f5692b464 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs @@ -1,5 +1,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; +using System; namespace Barotrauma.Items.Components { @@ -13,10 +14,18 @@ public override void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEven msg.WriteBoolean(writeAttachData); if (!writeAttachData) { return; } + UInt16 attacherId = Entity.NullEntityID; + if (TryExtractEventData(extraData, out AttachEventData attachEventData) && + attachEventData.Attacher != null) + { + attacherId = attachEventData.Attacher.ID; + } + msg.WriteBoolean(Attached); msg.WriteSingle(body.SimPosition.X); msg.WriteSingle(body.SimPosition.Y); msg.WriteUInt16(item.Submarine?.ID ?? Entity.NullEntityID); + msg.WriteUInt16(attacherId); } public void ServerEventRead(IReadMessage msg, Client c) @@ -34,7 +43,7 @@ public void ServerEventRead(IReadMessage msg, Client c) AttachToWall(); OnUsed.Invoke(new ItemUseInfo(item, c.Character)); - item.CreateServerEvent(this); + item.CreateServerEvent(this, new AttachEventData(simPosition, c.Character)); c.Character.Inventory?.CreateNetworkEvent(); GameServer.Log(GameServer.CharacterLogName(c.Character) + " attached " + item.Name + " to a wall", ServerLog.MessageType.ItemInteraction); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 91d95e6a23..4713be50f5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -424,7 +424,14 @@ public void CreateServerEvent(T ic, ItemComponent.IEventData extraData) where if (!components.Contains(ic)) { return; } var eventData = new ComponentStateEventData(ic, extraData); - if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation for the item \"{Prefab.Identifier}\" failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false."); } + if (!ic.ValidateEventData(eventData)) + { + string errorMsg = + $"Server-side component event creation for the item \"{Prefab.Identifier}\" failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false. " + + $"Data: {extraData?.GetType().ToString() ?? "null"}"; + GameAnalyticsManager.AddErrorEventOnce($"Item.CreateServerEvent:ValidateEventData:{Prefab.Identifier}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); + } GameMain.Server.CreateEntityEvent(this, eventData); } @@ -435,10 +442,12 @@ public void TryCreateServerEventSpam() foreach (ItemComponent ic in components) { - if (!(ic is IServerSerializable)) { continue; } - var eventData = new ComponentStateEventData(ic, ic.ServerGetEventData()); - if (!ic.ValidateEventData(eventData)) { continue; } - GameMain.Server.CreateEntityEvent(this, eventData); + if (ic is not IServerSerializable) { continue; } + var eventData = ic.ServerGetEventData(); + if (eventData == null) { continue; } + var componentData = new ComponentStateEventData(ic, eventData); + if (!ic.ValidateEventData(componentData)) { continue; } + GameMain.Server.CreateEntityEvent(this, componentData); } } #endif diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 814152a260..463ff34274 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -76,6 +76,12 @@ partial void UpdateProjSpecific(float deltaTime, Camera cam) } } + + public void CreateStatusEvent() + { + GameMain.NetworkMember?.CreateEntityEvent(this, new StatusEventData()); + } + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed hull event: expected {nameof(Hull)}.{nameof(IEventData)}"); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 16245aa682..2765dc5317 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -22,6 +22,8 @@ public UInt16 LastRecvServerSettingsUpdate public UInt16 LastRecvLobbyUpdate = NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID); + public bool InitialLobbyUpdateSent; + public UInt16 LastSentChatMsgID = 0; //last msg this client said public UInt16 LastRecvChatMsgID = 0; //last msg this client knows about @@ -166,8 +168,8 @@ public void InitClientSync() LastSentChatMsgID = 0; LastRecvChatMsgID = ChatMessage.LastID; - LastRecvLobbyUpdate = 0; - + LastRecvLobbyUpdate = NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID); + InitialLobbyUpdateSent = false; LastRecvEntityEventID = 0; UnreceivedEntityEventCount = 0; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index d6b837cb73..2c87d78590 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1286,11 +1286,8 @@ private void ClientReadIngame(IReadMessage inc) //check if midround syncing is needed due to missed unique events if (!midroundSyncingDone) { entityEventManager.InitClientMidRoundSync(c); } MissionAction.NotifyMissionsUnlockedThisRound(c); - if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign) - { - mpCampaign.SendCrewState(); - } - else if (GameMain.GameSession.GameMode is PvPMode) + + if (GameMain.GameSession.GameMode is PvPMode) { if (c.TeamID == CharacterTeamType.None) { @@ -1299,6 +1296,10 @@ private void ClientReadIngame(IReadMessage inc) } else { + if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.SendCrewState(); + } //everyone's in team 1 in non-pvp game modes c.TeamID = CharacterTeamType.Team1; } @@ -2231,12 +2232,13 @@ private void ClientWriteLobby(Client c) outmsg.WriteUInt16((UInt16)settingsBuf.LengthBytes); outmsg.WriteBytes(settingsBuf.Buffer, 0, settingsBuf.LengthBytes); - outmsg.WriteBoolean(c.LastRecvLobbyUpdate < 1); - if (c.LastRecvLobbyUpdate < 1) + outmsg.WriteBoolean(!c.InitialLobbyUpdateSent); + if (!c.InitialLobbyUpdateSent) { isInitialUpdate = true; initialUpdateBytes = outmsg.LengthBytes; ClientWriteInitial(c, outmsg); + c.InitialLobbyUpdateSent = true; initialUpdateBytes = outmsg.LengthBytes - initialUpdateBytes; } outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.Name); @@ -3106,6 +3108,7 @@ private void SendStartMessage(int seed, string levelSeed, GameSession gameSessio { msg.WriteString(levelSeed); msg.WriteSingle(ServerSettings.SelectedLevelDifficulty); + msg.WriteIdentifier(ServerSettings.Biome == "Random".ToIdentifier() ? Identifier.Empty : ServerSettings.Biome); msg.WriteString(gameSession.SubmarineInfo.Name); msg.WriteString(gameSession.SubmarineInfo.MD5Hash.StringRepresentation); var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ? @@ -3740,13 +3743,13 @@ public void SendChatMessage(string message, ChatMessageType? type = null, Client } else //msg sent by an AI character { - senderName = senderCharacter.Name; + senderName = senderCharacter.DisplayName; } } else //msg sent by a client { senderCharacter = senderClient.Character; - senderName = senderCharacter == null ? senderClient.Name : senderCharacter.Name; + senderName = senderCharacter == null ? senderClient.Name : senderCharacter.DisplayName; if (type == ChatMessageType.Private) { if (senderCharacter != null && !senderCharacter.IsDead || targetClient.Character != null && !targetClient.Character.IsDead) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 8c0fceec92..6bab9e4ecd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -151,11 +151,11 @@ private void UpdateClient(Client client, float deltaTime) //increase the strength of the herpes affliction in steps instead of linearly //otherwise clients could determine their exact karma value from the strength float herpesStrength = 0.0f; - if (client.Karma < 20) + if (client.Karma < HerpesThreshold * 0.5f) herpesStrength = 100.0f; - else if (client.Karma < 30) + else if (client.Karma < HerpesThreshold * 0.75f) herpesStrength = 60.0f; - else if (client.Karma < 40.0f) + else if (client.Karma < HerpesThreshold) herpesStrength = 30.0f; var existingAffliction = client.Character.CharacterHealth.GetAffliction(AfflictionPrefab.SpaceHerpesType); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index fc721f6f8b..2cd0896316 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -229,7 +229,9 @@ public void Update(List clients) GameMain.GameSession.RoundDuration > NetConfig.RoundStartSyncDuration) { lastWarningTime = Timing.TotalTime; - GameServer.Log("WARNING: ServerEntityEventManager is lagging behind! Last sent id: " + lastSentToAnyone.ToString() + ", latest create id: " + ID.ToString(), ServerLog.MessageType.ServerMessage); + string warningMsg = $"WARNING: ServerEntityEventManager is lagging behind! Last sent id: {lastSentToAnyone}, latest create id: {ID}"; + warningMsg += "\n" + GetHighEventCountsWarning(events, maxEventsToList: 3); + GameServer.Log(warningMsg, ServerLog.MessageType.ServerMessage); events.ForEach(e => e.ResetCreateTime()); //TODO: reset clients if this happens, maybe do it if a majority are behind rather than all of them? } @@ -323,30 +325,20 @@ public void Write(in SegmentTableWriter segmentTable, Client c } //too many events for one packet - //(normal right after a round has just started, don't show a warning if it's been less than 10 seconds) - if (eventsToSync.Count > 200 && GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10.0) + //(normal right after a round has just started, don't show a warning if it's been less than 30 seconds) + if (eventsToSync.Count > 200 && GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 30.0) { if (eventsToSync.Count > 200 && !client.NeedsMidRoundSync && Timing.TotalTime > lastEventCountHighWarning + 2.0) { Color color = eventsToSync.Count > 500 ? Color.Red : Color.Orange; if (eventsToSync.Count < 300) { color = Color.Yellow; } string warningMsg = "WARNING: event count very high: " + eventsToSync.Count; - - var sortedEvents = eventsToSync.GroupBy(e => e.Entity.ToString()) - .Select(e => new { Value = e.Key, Count = e.Count() }) - .OrderByDescending(e => e.Count); - - int count = 1; - foreach (var sortedEvent in sortedEvents) - { - warningMsg += "\n" + count + ". " + (sortedEvent.Value?.ToString() ?? "null") + " x" + sortedEvent.Count; - count++; - if (count > 3) { break; } - } + warningMsg += "\n" + GetHighEventCountsWarning(eventsToSync, maxEventsToList: 3); if (GameSettings.CurrentConfig.VerboseLogging) { GameServer.Log(warningMsg, ServerLog.MessageType.Error); } + server.SendConsoleMessage(warningMsg, client, color); DebugConsole.NewMessage(warningMsg, color); lastEventCountHighWarning = Timing.TotalTime; } @@ -373,6 +365,31 @@ public void Write(in SegmentTableWriter segmentTable, Client c } } + private string GetHighEventCountsWarning(IEnumerable events, int maxEventsToList) + { + string warningMsg = string.Empty; + + var sortedEvents = events.GroupBy(e => e.Entity.ToString()) + .Select(e => new { Value = e.First(), Count = e.Count() }) + .OrderByDescending(e => e.Count); + + int count = 1; + foreach (var sortedEvent in sortedEvents) + { + Entity targetEntity = sortedEvent.Value.Entity; + if (!warningMsg.IsNullOrEmpty()) { warningMsg += "\n"; } + warningMsg += count + ". " + (targetEntity?.ToString() ?? "null") + " x" + sortedEvent.Count; + if (targetEntity != null && targetEntity.ContentPackage != ContentPackageManager.VanillaCorePackage) + { + warningMsg += $" (content package: {targetEntity.ContentPackage.Name})"; + } + count++; + if (count > maxEventsToList) { break; } + } + + return warningMsg; + } + /// /// Returns a list of events that should be sent to the client from the eventList /// diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 87878e9c41..6a40cdd8ce 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -447,7 +447,7 @@ protected override void ProcessAuthTicket(ClientAuthTicketAndVersionPacket packe { if (pendingClient.AccountInfo.AccountId != packet.AccountId) { - RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); + rejectClient(); } return; } @@ -503,10 +503,16 @@ void rejectClient() pendingClient.AuthSessionStarted = true; TaskPool.Add($"{nameof(LidgrenServerPeer)}.ProcessAuth", authenticator.VerifyTicket(authTicket), t => { - if (!t.TryGetResult(out AccountInfo accountInfo) - || accountInfo.IsNone) + if (!t.TryGetResult(out AccountInfo accountInfo) || accountInfo.IsNone) { - rejectClient(); + if (GameMain.Server.ServerSettings.RequireAuthentication) + { + rejectClient(); + } + else + { + acceptClient(new AccountInfo(new UnauthenticatedAccountId(packet.Name))); + } return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 6102068821..1bdf3986f8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -74,7 +74,7 @@ private void WriteNetProperties(IWriteMessage outMsg, Client c) { var property = netProperties[key]; property.SyncValue(); - if (NetIdUtils.IdMoreRecent(property.LastUpdateID, c.LastRecvLobbyUpdate)) + if (NetIdUtils.IdMoreRecent(property.LastUpdateID, c.LastRecvLobbyUpdate) || !c.InitialLobbyUpdateSent) { outMsg.WriteUInt32(key); netProperties[key].Write(outMsg); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 05e5b5635d..9881ee0030 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.6.19.1 + 1.7.7.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer @@ -61,7 +61,7 @@ - + diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml index 9ab82603db..f6424d1a74 100644 --- a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -1,4 +1,4 @@ - + @@ -20,6 +20,7 @@ OxygenMultiplier="1.2" FuelMultiplier="1.2" MissionRewardMultiplier="1.0" + ExperienceRewardMultiplier="1.0" ShopPriceMultiplier="0.9" ShipyardPriceMultiplier="0.9" RepairFailMultiplier="1.0" @@ -38,6 +39,7 @@ OxygenMultiplier="1.0" FuelMultiplier="1.0" MissionRewardMultiplier="1.0" + ExperienceRewardMultiplier="1.0" ShopPriceMultiplier="1.0" ShipyardPriceMultiplier="1.0" RepairFailMultiplier="1.0" @@ -56,6 +58,7 @@ OxygenMultiplier="0.7" FuelMultiplier="0.9" MissionRewardMultiplier="1.0" + ExperienceRewardMultiplier="1.0" ShopPriceMultiplier="1.5" ShipyardPriceMultiplier="1.5" RepairFailMultiplier="2.0" @@ -74,6 +77,7 @@ OxygenMultiplier="0.4" FuelMultiplier="0.8" MissionRewardMultiplier="0.8" + ExperienceRewardMultiplier="0.8" ShopPriceMultiplier="2.0" ShipyardPriceMultiplier="2.0" RepairFailMultiplier="5.0" diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerRun.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerRun.xml new file mode 100644 index 0000000000..41448aef43 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerRun.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerSwimFast.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerSwimFast.xml new file mode 100644 index 0000000000..5a5abac554 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerSwimFast.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerSwimSlow.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerSwimSlow.xml new file mode 100644 index 0000000000..0e8efab83d --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerSwimSlow.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerWalk.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerWalk.xml new file mode 100644 index 0000000000..39297cf28a --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Animations/CrawlerWalk.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml new file mode 100644 index 0000000000..f90685787b --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Crawler.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Ragdolls/CrawlerDefaultRagdoll.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Ragdolls/CrawlerDefaultRagdoll.xml new file mode 100644 index 0000000000..d3a6deb732 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/Ragdolls/CrawlerDefaultRagdoll.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/crawler.png b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Characters/Crawler/crawler.png new file mode 100644 index 0000000000000000000000000000000000000000..06feffae2aa2c5bdfcbc9b4c986b55822073a342 GIT binary patch literal 220731 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 z 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� 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

n!`rJ{Fo=g~zbeAQj9VF}~f;K`)(tM>t0 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 V{kEO@lqmQ|<>s9mI0e&y{{!)3u>Al4 literal 0 HcmV?d00001 diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Human.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Human.xml new file mode 100644 index 0000000000..6c7b98bee7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Human.xml @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Mudraptor.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Mudraptor.xml new file mode 100644 index 0000000000..84e84bd6f7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Mudraptor.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/README.txt b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/README.txt new file mode 100644 index 0000000000..300ba99232 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/README.txt @@ -0,0 +1,27 @@ +This mod includes a couple of differently configured character overrides and variants: + +- Human: overrides the human character with a broken version whose ragdoll fails to load. + - Expected behavior: loading the human character will fail and cause console errors, and the game will load the vanilla version instead. + +- Mudraptor: overrides Mudraptor with a broken version whose ragdoll fails to load. + - Expected behavior: loading a Mudraptor will fail and cause console errors, and the game will load the vanilla Crawler ragdoll instead. + - Variants of Mudraptor should fail to load as well, and switch to the Crawler ragdoll instead. + +- Crawler: overrides Crawler with a green version with sunglasses. + - Expected behavior: Crawler spawns as a green version with sunglasses. + - This change should also affect variants of Crawler: Crawler_large should also be green and have sunglasses (even though the mod does + not modify it directly). + - Crawler_hatchling (variant of Crawler) should look unchanged, but load correctly (despite it being a vanilla character whose base + character has now been overridden by a mod). + +- Testcyborgworm_m: adds a variant of the Cyborgworm (identical to the normal Cyborgworm). + - Expected behavior: Testcyborgworm_m looks identical to Cyborgworm. + - This has previously caused issues, because the Cyborgworm uses multiple textures, some of which aren't in the character folder, + and these used to load incorrectly when the character is a variant. + - Note that the character is configured incorrectly: it's defined to be an override, but there's no character (Testcyborgworm_m) it'd override. + It works regardless, so this can be used as a test case for checking that these incorrectly defined characters still load. + +- Spineling_morbusine_m: adds a variant of Spineling_morbusine (identical to the normal Spineling_morbusine). + - Expected behavior: Spineling_morbusine_m looks identical to Spineling_morbusine. + - This has previously caused issues, because Spineling_morbusine defines the ragdoll slightly differently than other monsters + (not in the usual Ragdoll folder, but a hard-coded path to a ragdoll file in the character's folder). \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Spineling_morbusine_m.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Spineling_morbusine_m.xml new file mode 100644 index 0000000000..8a7839df09 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Spineling_morbusine_m.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Testcyborgworm_m.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Testcyborgworm_m.xml new file mode 100644 index 0000000000..3c3c7f0fc6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/Testcyborgworm_m.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/filelist.xml b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/filelist.xml new file mode 100644 index 0000000000..1073ed60d5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/LocalMods/[DebugOnlyTest]Character override and variant tests/filelist.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs index f2914465c5..419e6c1cb3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Barotrauma.Steam; using FarseerPhysics; @@ -15,6 +16,84 @@ namespace Barotrauma static class AchievementManager { + private static readonly ImmutableHashSet SupportedAchievements = ImmutableHashSet.Create( + "killmoloch".ToIdentifier(), + "killhammerhead".ToIdentifier(), + "killendworm".ToIdentifier(), + "artifactmission".ToIdentifier(), + "combatmission1".ToIdentifier(), + "combatmission2".ToIdentifier(), + "healcrit".ToIdentifier(), + "repairdevice".ToIdentifier(), + "traitorwin".ToIdentifier(), + "killtraitor".ToIdentifier(), + "killclown".ToIdentifier(), + "healopiateaddiction".ToIdentifier(), + "survivecrushdepth".ToIdentifier(), + "survivereactormeltdown".ToIdentifier(), + "healhusk".ToIdentifier(), + "killpoison".ToIdentifier(), + "killnuke".ToIdentifier(), + "killtool".ToIdentifier(), + "clowncostume".ToIdentifier(), + "lastmanstanding".ToIdentifier(), + "lonesailor".ToIdentifier(), + "subhighvelocity".ToIdentifier(), + "nodamagerun".ToIdentifier(), + "subdeep".ToIdentifier(), + "maxintensity".ToIdentifier(), + "discovercoldcaverns".ToIdentifier(), + "discovereuropanridge".ToIdentifier(), + "discoverhydrothermalwastes".ToIdentifier(), + "discovertheaphoticplateau".ToIdentifier(), + "discoverthegreatsea".ToIdentifier(), + "travel10".ToIdentifier(), + "travel100".ToIdentifier(), + "xenocide".ToIdentifier(), + "genocide".ToIdentifier(), + "cargomission".ToIdentifier(), + "subeditor24h".ToIdentifier(), + "crewaway".ToIdentifier(), + "captainround".ToIdentifier(), + "securityofficerround".ToIdentifier(), + "engineerround".ToIdentifier(), + "mechanicround".ToIdentifier(), + "medicaldoctorround".ToIdentifier(), + "assistantround".ToIdentifier(), + "campaigncompleted".ToIdentifier(), + "salvagewreckmission".ToIdentifier(), + "escortmission".ToIdentifier(), + "killcharybdis".ToIdentifier(), + "killlatcher".ToIdentifier(), + "killspineling_giant".ToIdentifier(), + "killcrawlerbroodmother".ToIdentifier(), + "ascension".ToIdentifier(), + "campaignmetadata_pathofthebikehorn_7".ToIdentifier(), + "campaignmetadata_coalitionspecialhire1_hired_true".ToIdentifier(), + "campaignmetadata_coalitionspecialhire2_hired_true".ToIdentifier(), + "campaignmetadata_separatistspecialhire1_hired_true".ToIdentifier(), + "campaignmetadata_separatistspecialhire2_hired_true".ToIdentifier(), + "campaignmetadata_huskcultspecialhire1_hired_true".ToIdentifier(), + "campaignmetadata_clownspecialhire1_hired_true".ToIdentifier(), + "scanruin".ToIdentifier(), + "clearruin".ToIdentifier(), + "beaconmission".ToIdentifier(), + "abandonedoutpostrescue".ToIdentifier(), + "abandonedoutpostassassinate".ToIdentifier(), + "abandonedoutpostdestroyhumans".ToIdentifier(), + "abandonedoutpostdestroymonsters".ToIdentifier(), + "nestmission".ToIdentifier(), + "miningmission".ToIdentifier(), + "combatmissionseparatistsvscoalition".ToIdentifier(), + "combatmissioncoalitionvsseparatists".ToIdentifier(), + "getoutalive".ToIdentifier(), + "abyssbeckons".ToIdentifier(), + "europasfinest".ToIdentifier(), + "kingofthehull".ToIdentifier(), + "killmantis".ToIdentifier(), + "ancientnovelty".ToIdentifier(), + "whatsmirksbelow".ToIdentifier()); + private const float UpdateInterval = 1.0f; private static readonly HashSet unlockedAchievements = new HashSet(); @@ -42,6 +121,29 @@ private sealed class RoundData private static PathFinder pathFinder; private static readonly Dictionary cachedDistances = new Dictionary(); + static AchievementManager() + { +#if DEBUG + if (SteamManager.IsInitialized && SteamManager.TryGetAllAvailableAchievements(out var achievements) && achievements.Any()) + { + foreach (var achievement in achievements) + { + if (!SupportedAchievements.Contains(achievement.Identifier.ToIdentifier())) + { + DebugConsole.ThrowError($"Achievement \"{achievement.Identifier}\" is present on Steam's backend but not in achievements supported by {nameof(AchievementManager)}."); + } + } + foreach (Identifier achievementId in SupportedAchievements) + { + if (achievements.None(a => a.Identifier.ToIdentifier() == achievementId)) + { + DebugConsole.ThrowError($"Could not find achievement \"{achievementId}\" on Steam's backend."); + } + } + } +#endif + } + public static void OnStartRound(Biome biome = null) { roundData = new RoundData(); @@ -584,6 +686,7 @@ public static void UnlockAchievement(Identifier identifier, bool unlockClients = { if (CheatsEnabled) { return; } if (Screen.Selected is { IsEditor: true }) { return; } + if (!SupportedAchievements.Contains(identifier)) { return; } #if CLIENT if (GameMain.GameSession?.GameMode is TestGameMode) { return; } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index fc4b5fbad2..0bc42e6274 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -2628,7 +2628,7 @@ private bool Aim(float deltaTime, ISpatialEntity target, Item weapon) float margin = MathHelper.PiOver4 * distanceFactor; if (angle < margin || dist < minDistance) { - var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; + var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true); if (pickedBody != null) { @@ -2643,7 +2643,6 @@ private bool Aim(float deltaTime, ISpatialEntity target, Item weapon) return true; } } - Character t = null; if (pickedBody.UserData is Character c) { @@ -2657,6 +2656,16 @@ private bool Aim(float deltaTime, ISpatialEntity target, Item weapon) { return true; } + if (pickedBody.UserData is Item item && item.Prefab.DamagedByProjectiles) + { + // Target behind an item -> allow shooting. + return true; + } + if (pickedBody.UserData is Holdable holdable && holdable.Item.Prefab.DamagedByProjectiles) + { + // Target behind a blocking but destructible item -> allow shooting. + return true; + } } } return false; @@ -2889,7 +2898,7 @@ public void UpdateTargets() if (aiTarget.ShouldBeIgnored()) { continue; } if (ignoredTargets.Contains(aiTarget)) { continue; } if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; } - if (!TargetOutposts && GameMain.GameSession.GameMode is not TestGameMode) + if (!TargetOutposts && GameMain.GameSession?.GameMode is not TestGameMode) { if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsOutpost) { continue; } } @@ -3918,7 +3927,13 @@ private bool TryResetOriginalState(Identifier tag) return false; } + ///

+ /// Parameters originally defined in the AI params and modified temporarily. + /// private readonly Dictionary> modifiedParams = new Dictionary>(); + /// + /// Parameters created temporarily. Not originally defined in the AI params at all. + /// private readonly Dictionary tempParams = new Dictionary(); private readonly List tempParamsList = new List(); @@ -3952,11 +3967,6 @@ private void ChangeParams(Identifier tag, AIState state, float? priority = null, { if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out CharacterParams.TargetParams targetParams)) { - if (state == AIState.Attack) - { - // Only applies to new temp target params. Shouldn't affect any existing definitions (handled below). - targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub; - } tempParams.Add(tag, targetParams); } } @@ -3970,6 +3980,15 @@ private void ChangeParams(Identifier tag, AIState state, float? priority = null, targetParams.Priority = Math.Max(targetParams.Priority, priority.Value); } targetParams.State = state; + if (state == AIState.Attack) + { + targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub; + targetParams.IgnoreInside = false; + targetParams.IgnoreOutside = false; + targetParams.IgnoreTargetInside = false; + targetParams.IgnoreTargetOutside = false; + targetParams.IgnoreIncapacitated = false; + } } modifiedParams.TryAdd(tag, existingTargetParams); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index caf6bbfc8e..fbf0b0b943 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -574,7 +574,7 @@ private void CheckEnemies() foreach (Character c in Character.CharacterList) { if (c.Submarine != Character.Submarine) { continue; } - if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; } + if (c.Removed || c.IsDead || c.IsIncapacitated || c.InDetectable) { continue; } if (IsFriendly(c)) { continue; } Vector2 toTarget = c.WorldPosition - WorldPosition; float dist = toTarget.LengthSquared(); @@ -1045,7 +1045,7 @@ protected void ReportProblems() { foreach (Character target in Character.CharacterList) { - if (target.CurrentHull != hull || !target.Enabled) { continue; } + if (target.CurrentHull != hull || !target.Enabled || target.InDetectable) { continue; } if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false)) { if (!target.IsHandcuffed && AddTargets(Character, target) && newOrder == null) @@ -1696,16 +1696,25 @@ private void CheckCrouching(float deltaTime) public bool AllowCampaignInteraction() { - if (Character == null || Character.Removed || Character.IsIncapacitated) { return false; } - - switch (ObjectiveManager.CurrentObjective) - { - case AIObjectiveCombat _: - case AIObjectiveFindSafety _: - case AIObjectiveExtinguishFires _: - case AIObjectiveFightIntruders _: - case AIObjectiveFixLeaks _: - return false; + if (Character == null || Character.Removed) { return false; } + + //some events might want to allow talking/examining characters that are incapacitated or in some "emergency" ai state, + //so let's ignore those here + var type = Character.CampaignInteractionType; + if (type != CampaignMode.InteractionType.None && + type != CampaignMode.InteractionType.Talk && + type != CampaignMode.InteractionType.Examine) + { + if (Character.IsIncapacitated) { return false; } + switch (ObjectiveManager.CurrentObjective) + { + case AIObjectiveCombat _: + case AIObjectiveFindSafety _: + case AIObjectiveExtinguishFires _: + case AIObjectiveFightIntruders _: + case AIObjectiveFixLeaks _: + return false; + } } return true; } @@ -2272,7 +2281,7 @@ public static float GetHullSafety(Hull hull, IEnumerable visibleHulls, Cha public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) { - if (other.IsHusk) + if (other.IsHusk && !onlySameTeam) { // Disguised as husk return me.IsDisguisedAsHusk; @@ -2305,16 +2314,15 @@ public static bool IsFriendly(Character me, Character other, bool onlySameTeam = { if (!me.IsSameSpeciesOrGroup(other)) { return false; } } - if (GameMain.GameSession?.GameMode is CampaignMode) + if (GameMain.GameSession?.GameMode is CampaignMode && + //ignore hostile faction if offering services that don't get disabled by faction hostility + (me.CampaignInteractionType == CampaignMode.InteractionType.None || CampaignMode.HostileFactionDisablesInteraction(me.CampaignInteractionType))) { if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) || (me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC)) { Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other; - //NPCs that allow some campaign interaction are not turned hostile by low reputation - if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; } - if (npc.AIController is HumanAIController npcAI) { return !npcAI.IsInHostileFaction(); @@ -2347,7 +2355,7 @@ public bool IsInHostileFaction() return false; } - public static bool IsActive(Character c) => c != null && c.Enabled && !c.IsUnconscious; + public static bool IsActive(Character c) => c is { Enabled: true, IsUnconscious: false }; public static bool IsTrueForAllBotsInTheCrew(Character character, Func predicate) { @@ -2359,7 +2367,7 @@ public static bool IsTrueForAllBotsInTheCrew(Character character, Func o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item)) - { - // Not targeting the same item. - continue; - } - bool isTargetOrdered = IsOrderedToOperateTarget(otherAI); - if (!isOrder && isTargetOrdered) - { - // If the other bot is ordered to operate the item, let him do it, unless we are ordered too - other = c; - break; - } - else - { - if (isOrder && !isTargetOrdered) - { - // We are ordered and the target is not -> allow to operate - continue; - } - else - { - if (!IsOperatingTarget(otherAI)) - { - // The other bot is doing something else -> stick to the target. - continue; - } - if (target is Steering) - { - // Steering is hard-coded -> cannot use the required skills collection defined in the xml - if (Character.GetSkillLevel(Tags.HelmSkill) <= c.GetSkillLevel(Tags.HelmSkill)) - { - other = c; - break; - } - } - else if (target.DegreeOfSuccess(Character) <= target.DegreeOfSuccess(c)) - { - other = c; - break; - } - } - } - } - } - return other != null; - bool IsOrderedToOperateTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item; - bool IsOperatingTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item; - } - - public bool IsItemRepairedByAnother(Item target, out Character other) - { - other = null; - if (Character == null) { return false; } - if (target == null) { return false; } - bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController); - foreach (var c in Character.CharacterList) - { - if (!IsActive(c)) { continue; } - if (c == Character) { continue; } - if (c.TeamID != Character.TeamID) { continue; } - other = c; - if (c.IsPlayer) - { - if (target.Repairables.Any(r => r.CurrentFixer == c)) - { - // If the other character is player, don't try to repair - return true; - } - } - else if (c.AIController is HumanAIController operatingAI) - { - var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective(); - if (repairItemsObjective == null) { continue; } - if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target) - { - // Not targeting the same item. - continue; - } - bool isTargetOrdered = IsOrderedToRepairThis(operatingAI); - if (!isOrder && isTargetOrdered) - { - // If the other bot is ordered to repair the item, let him do it, unless we are ordered too - return true; - } - else - { - if (isOrder && !isTargetOrdered) - { - // We are ordered and the target is not -> allow to repair - continue; - } - else - { - if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective) - { - // The other bot is ordered to do something else - continue; - } - return target.Repairables.Max(r => r.DegreeOfSuccess(Character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c)); - } - } - } - } - return false; - bool IsOrderedToRepairThis(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target; - } - #region Wrappers public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam); public bool IsTrueForAnyBotInTheCrew(Func predicate) => IsTrueForAnyBotInTheCrew(Character, predicate); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 7ece54d711..b9c0d75f30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -562,6 +562,7 @@ public bool CanAccessDoor(Door door, Func buttonFilter = null) bool buttonsFound = false; // Check wired controllers (e.g. buttons) // Always run the buttonFilter delegate (inside CanAccessButton method), if defined, because it's used for find a valid controller component that can be used for closing the door, when needed. + // TODO: connectionFilter is ignored in the recursive searches, so it does nothing here. foreach (Controller button in door.Item.GetConnectedComponents(recursive: true, connectionFilter: c => c.Name is "toggle" or "set_state")) { buttonsFound = true; @@ -727,12 +728,15 @@ private void CheckDoorsInPath() float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition); //heavily prefer buttons linked to the door, so sub builders can help the bots figure out which button to use by linking them if (door.Item.linkedTo.Contains(button.Item)) { distance *= 0.1f; } - if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item)) + if (closestButton == null || distance < closestDist) { - closestButton = button; - closestDist = distance; + if (distance < MathUtils.Pow2(button.Item.InteractDistance + GetColliderLength()) && character.CanSeeTarget(button.Item)) + { + closestButton = button; + closestDist = distance; + } } - return true; + return closestButton != null; }); if (canAccess) { @@ -755,41 +759,19 @@ private void CheckDoorsInPath() } else if (closestButton != null) { - if (closestDist < MathUtils.Pow2(closestButton.Item.InteractDistance + GetColliderLength())) + if (pressButton) { - if (pressButton) + if (closestButton.Item.TryInteract(character, forceSelectKey: true)) { - if (closestButton.Item.TryInteract(character, forceSelectKey: true)) - { - lastDoor = (door, shouldBeOpen); - buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0; - } - else - { - buttonPressTimer = 0; - } + lastDoor = (door, shouldBeOpen); + buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0; } - break; - } - else - { - // Can't reach the button closest to the character. - // It's possible that we could reach another buttons. - // If this becomes an issue, we could go through them here and check if any of them are reachable - // (would have to cache a collection of buttons instead of a single reference in the CanAccess filter method above) - var body = Submarine.PickBody(character.SimPosition, character.GetRelativeSimPosition(closestButton.Item), collisionCategory: Physics.CollisionWall | Physics.CollisionLevel); - if (body != null) + else { - if (body.UserData is Item item) - { - var d = item.GetComponent(); - if (d == null || d.IsOpen) { return; } - } - // The button is on the wrong side of the door or a wall - currentPath.Unreachable = true; + buttonPressTimer = 0; } - return; } + break; } } else if (shouldBeOpen) @@ -871,6 +853,16 @@ private void CheckDoorsInPath() { if (!CanAccessDoor(door, button => { + if (Vector2.DistanceSquared(door.Item.WorldPosition, button.Item.WorldPosition) > MathUtils.Pow2(button.Item.InteractDistance + GetColliderLength())) + { + // Too far from the door. + return false; + } + if (!ISpatialEntity.IsTargetVisible(button.Item, door.Item)) + { + // Obstructed. + return false; + } // Ignore buttons that are on the wrong side of the door, unless there's a motion sensor connected to the door, which can be triggered by the character. if (door.IsHorizontal) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 43a0197ba6..92751fdcd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -59,7 +59,7 @@ protected override void OnObjectiveCompleted(AIObjective objective, Character ta public static bool IsValidTarget(Character target, Character character, bool targetCharactersInOtherSubs) { if (target == null || target.Removed) { return false; } - if (target.IsDead) { return false; } + if (target.IsDead || target.InDetectable) { return false; } if (target.IsUnconscious && target.Params.Health.ConstantHealthRegeneration <= 0.0f) { return false; } if (target == character) { return false; } if (target.Submarine == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index a185ad9685..6f8df01331 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -387,14 +387,14 @@ public void Wander(float deltaTime) chairCheckTimer -= deltaTime; if (chairCheckTimer <= 0.0f && character.SelectedSecondaryItem == null) { - foreach (Item item in Item.ItemList) + foreach (Item chair in Item.ChairItems) { - if (item.CurrentHull != currentHull || !item.HasTag(Tags.ChairItem)) { continue; } + if (chair.CurrentHull != currentHull) { continue; } //not possible in vanilla game, but a mod might have holdable/attachable chairs - if (item.ParentInventory != null || item.body is { Enabled: true }) { continue; } - var controller = item.GetComponent(); + if (chair.ParentInventory != null || chair.body is { Enabled: true }) { continue; } + var controller = chair.GetComponent(); if (controller == null || controller.User != null) { continue; } - item.TryInteract(character, forceSelectKey: true); + chair.TryInteract(character, forceSelectKey: true); } chairCheckTimer = chairCheckInterval; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 1d17d2100c..c98721a8fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -15,7 +16,7 @@ class AIObjectiveOperateItem : AIObjective public override bool AllowMultipleInstances => true; protected override bool AllowInAnySub => true; protected override bool AllowWhileHandcuffed => false; - public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret); + public override bool PrioritizeIfSubObjectivesActive => component is Reactor or Turret; private readonly ItemComponent component, controller; private readonly Entity operateTarget; @@ -88,12 +89,12 @@ protected override float GetPriority() Priority = 0; return Priority; } - var reactor = component?.Item.GetComponent(); + var reactor = component.Item.GetComponent(); if (reactor != null) { if (!isOrder) { - if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC) + if (reactor.LastUserWasPlayer && character.IsOnPlayerTeam) { // The reactor was previously operated by a player -> ignore. Priority = 0; @@ -126,7 +127,7 @@ bool IsAnotherOrderTargetingSameItem(AIObjective objective) } else if (!isOrder) { - var steering = component?.Item.GetComponent(); + var steering = component.Item.GetComponent(); if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsCaptain, onlyActive: true, onlyConnectedSubs: true))) { // Ignore if already set to autopilot or if there's a captain onboard @@ -137,7 +138,7 @@ bool IsAnotherOrderTargetingSameItem(AIObjective objective) if (targetItem.CurrentHull == null || targetItem.Submarine != character.Submarine && !isOrder || targetItem.CurrentHull.FireSources.Any() || - HumanAIController.IsItemOperatedByAnother(target, out _) || + IsItemOperatedByAnother(target) || Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)) || component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character)) { @@ -154,8 +155,8 @@ bool IsAnotherOrderTargetingSameItem(AIObjective objective) else if (!OverridePriority.HasValue) { float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier); - float max = AIObjectiveManager.LowestOrderPriority - 1; - if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && reactor.AutoTemp && Option == "powerup") + const float max = AIObjectiveManager.LowestOrderPriority - 1; + if (reactor is { PowerOn: true, FissionRate: > 1, AutoTemp: true } && Option == "powerup") { // Already on, no need to operate. value = 0; @@ -171,12 +172,12 @@ public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjecti Entity operateTarget = null, bool useController = false, ItemComponent controller = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier, option) { - component = item ?? throw new ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target."); + component = item ?? throw new ArgumentNullException(nameof(item), "Attempted to create an AIObjectiveOperateItem with a null target."); this.requireEquip = requireEquip; this.operateTarget = operateTarget; this.useController = useController; - if (useController) { this.controller = controller ?? component?.Item?.FindController(); } - var target = GetTarget(); + if (useController) { this.controller = controller ?? component.Item?.FindController(); } + ItemComponent target = GetTarget(); if (target == null) { Abandon = true; @@ -320,5 +321,68 @@ public override void Reset() goToObjective = null; getItemObjective = null; } + + private bool IsItemOperatedByAnother(ItemComponent target) + { + if (target?.Item == null) { return false; } + bool isOrdered = IsOrderedToOperateTarget(HumanAIController); + foreach (Character c in Character.CharacterList) + { + if (!HumanAIController.IsActive(c)) { continue; } + if (c == character) { continue; } + if (c.TeamID != character.TeamID) { continue; } + if (c.IsPlayer) + { + if (c.SelectedItem == target.Item) + { + // If the other character is player, don't try to operate + return true; + } + } + else if (c.AIController is HumanAIController otherAI) + { + if (otherAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item)) + { + // Not targeting the same item. + continue; + } + bool isOtherCharacterOrdered = IsOrderedToOperateTarget(otherAI); + switch (isOrdered) + { + case false when isOtherCharacterOrdered: + // We are not ordered and the target is ordered -> let the other character operate the target item. + return true; + case true when !isOtherCharacterOrdered: + // We are ordered and the other character is not -> allow to us to operate the target item. + continue; + default: + { + // Neither or both are ordered to operate this item. + if (!IsOperatingTarget(otherAI)) + { + // The other bot is doing something else -> stick to the target. + continue; + } + if (target is Steering) + { + // Steering is hard-coded -> cannot use the required skills collection defined in the xml + if (character.GetSkillLevel(Tags.HelmSkill) <= c.GetSkillLevel(Tags.HelmSkill)) + { + return true; + } + } + else if (target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c)) + { + return true; + } + break; + } + } + } + } + return false; + bool IsOrderedToOperateTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item; + bool IsOperatingTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index aad92fb397..58d30395c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -50,7 +50,7 @@ protected override float GetPriority() } return Priority; } - if (HumanAIController.IsItemRepairedByAnother(Item, out _)) + if (AIObjectiveRepairItems.IsItemRepairedByAnother(character, Item)) { Priority = 0; IsCompleted = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 3cd5544b24..f96df16ed0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -76,7 +76,7 @@ protected override bool IsValidTarget(Item item) { if (item.Repairables.None(r => r.RequiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; } } - return !HumanAIController.IsItemRepairedByAnother(item, out _); + return !IsItemRepairedByAnother(character, item); } public static bool ViableForRepair(Item item, Character character, HumanAIController humanAIController) @@ -161,5 +161,57 @@ public static bool IsValidTarget(Item item, Character character) return true; } + + public static bool IsItemRepairedByAnother(Character character, Item target) + { + if (target == null) { return false; } + bool isOrder = IsOrderedToPrioritizeTarget(character.AIController as HumanAIController); + foreach (Character c in Character.CharacterList) + { + if (!HumanAIController.IsActive(c)) { continue; } + if (c == character) { continue; } + if (c.TeamID != character.TeamID) { continue; } + if (c.IsPlayer) + { + if (target.Repairables.Any(r => r.CurrentFixer == c)) + { + // If the other character is player, don't try to repair + return true; + } + } + else if (c.AIController is HumanAIController otherAI) + { + var repairItemsObjective = otherAI.ObjectiveManager.GetObjective(); + if (repairItemsObjective == null) { continue; } + if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target) + { + // Not targeting the same item. + continue; + } + bool isTargetOrdered = IsOrderedToPrioritizeTarget(otherAI); + switch (isOrder) + { + case false when isTargetOrdered: + // We are not ordered and the target is ordered -> let the other character repair the target. + return true; + case true when !isTargetOrdered: + // We are ordered and the target is not -> allow us to repair the target. + continue; + default: + { + // Neither or both are ordered to repair this item. + if (otherAI.ObjectiveManager.CurrentObjective is not AIObjectiveRepairItems) + { + // The other bot is doing something else -> stick to the target. + continue; + } + return target.Repairables.Max(r => r.DegreeOfSuccess(character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c)); + } + } + } + } + return false; + bool IsOrderedToPrioritizeTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index afdb12c89d..da75de89fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -149,7 +149,7 @@ Item FindOxygenTank(Character c) => if (HumanAIController.VisibleHulls.Contains(Target.CurrentHull) && Target.CurrentHull.DisplayName != null) { character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", - ("[targetname]", Target.Name, FormatCapitals.No), + ("[targetname]", Target.DisplayName, FormatCapitals.No), ("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, null, 1.0f, $"foundunconscioustarget{Target.Name}".ToIdentifier(), 60.0f); } @@ -239,7 +239,7 @@ Item FindOxygenTank(Character c) => if (Target.CurrentHull?.DisplayName != null) { character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", - ("[targetname]", Target.Name, FormatCapitals.No), + ("[targetname]", Target.DisplayName, FormatCapitals.No), ("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, null, 1.0f, $"foundwoundedtarget{Target.Name}".ToIdentifier(), 60.0f); } @@ -287,6 +287,8 @@ private void GiveTreatment(float deltaTime) currentTreatmentSuitabilities, limb: Target.CharacterHealth.GetAfflictionLimb(affliction), user: character, + checkTreatmentThreshold: true, + checkTreatmentSuggestionThreshold: false, predictFutureDuration: 10.0f); foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) @@ -330,7 +332,10 @@ private void GiveTreatment(float deltaTime) { //get "overall" suitability for no specific limb at this point Target.CharacterHealth.GetSuitableTreatments( - currentTreatmentSuitabilities, user: character, predictFutureDuration: 10.0f); + currentTreatmentSuitabilities, user: character, + checkTreatmentThreshold: true, + checkTreatmentSuggestionThreshold: false, + predictFutureDuration: 10.0f); //didn't have any suitable treatments available, try to find some medical items if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability)) { @@ -387,7 +392,7 @@ private void GiveTreatment(float deltaTime) if (Target != character && character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", - ("[targetname]", Target.Name, FormatCapitals.No), + ("[targetname]", Target.DisplayName, FormatCapitals.No), ("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value, null, 2.0f, $"listrequiredtreatments{Target.Name}".ToIdentifier(), 60.0f); } @@ -483,7 +488,7 @@ protected override bool CheckObjectiveState() if (IsCompleted && Target != character && character.IsOnPlayerTeam) { string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed"; - string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.Name)?.Value; + string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.DisplayName)?.Value; character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f); } return IsCompleted; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 4c8331b52f..306fb786e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -47,9 +47,9 @@ protected override bool IsValidTarget(Character target) if (objectiveManager.GetFirstActiveObjective() == null) { charactersWithMinorInjuries.Add(target); - character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value, + character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.DisplayName).Value, delay: 1.0f, - identifier: $"notreatableafflictions{target.Name}".ToIdentifier(), + identifier: $"notreatableafflictions{target.DisplayName}".ToIdentifier(), minDurationBetweenSimilar: 10.0f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index 1666232653..0ad1c19ae1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -50,7 +50,8 @@ public void SetOrder(Character orderedCharacter) if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option) && o.TargetEntity == TargetItem)) { - if (orderedCharacter != CommandingCharacter) + bool orderGivenByDifferentCharacter = orderedCharacter != CommandingCharacter; + if (orderGivenByDifferentCharacter) { CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false), minDurationBetweenSimilar: 5, @@ -62,9 +63,12 @@ public void SetOrder(Character orderedCharacter) .WithOrderGiver(CommandingCharacter) .WithManualPriority(CharacterInfo.HighestManualOrderPriority); OrderedCharacter.SetOrder(CurrentOrder, CommandingCharacter != OrderedCharacter); - OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f, - minDurationBetweenSimilar: 5, - identifier: ("ReceiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier()); + if (orderGivenByDifferentCharacter) + { + OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f, + minDurationBetweenSimilar: 5, + identifier: ("ReceiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier()); + } } TimeSinceLastAttempt = 0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index c2299f4cd4..b3255db1a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -5,6 +5,7 @@ using Barotrauma.Networking; using System.Linq; using System; +using System.Diagnostics; namespace Barotrauma { @@ -89,12 +90,12 @@ partial class WreckAI : SubmarineTurretAI, IServerSerializable { public bool IsAlive { get; private set; } - private readonly List allItems; private readonly List thalamusItems; private readonly List thalamusStructures; private readonly List wayPoints = new List(); private readonly List hulls = new List(); private readonly List spawnOrgans = new List(); + private readonly List jammedDoors = new List(); private readonly Item brain; private bool initialCellsSpawned; @@ -105,7 +106,7 @@ partial class WreckAI : SubmarineTurretAI, IServerSerializable private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity); - private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T); + private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).OfType(); private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag)); @@ -122,93 +123,52 @@ private WreckAI(Submarine wreck) : base(wreck) { GetConfig(); if (Config == null) { return; } - var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p)); + var thalamusPrefabs = ItemPrefab.Prefabs.Where(IsThalamus); var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient); if (brainPrefab == null) { - DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI."); + DebugConsole.ThrowError($"WreckAI {wreck.Info.Name}: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.", contentPackage: Config.ContentPackage); return; } - allItems = wreck.GetItems(false); - thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab)); - hulls.AddRange(wreck.GetHulls(false)); - var potentialBrainHulls = new List<(Hull hull, float weight)>(); + thalamusItems = GetThalamusEntities(wreck, Config.Entity).ToList(); + hulls.AddRange(wreck.GetHulls(alsoFromConnectedSubs: false)); brain = new Item(brainPrefab, Vector2.Zero, wreck); thalamusItems.Add(brain); Point minSize = brain.Rect.Size.Multiply(brain.Scale); - // Bigger hulls are allowed, but not preferred more than what's sufficent. - Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f); - // Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls. - Rectangle shrinkedBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width - 500, wreck.Borders.Height)); - foreach (Hull hull in hulls) - { - float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition); - float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter)); - float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width)); - float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height)); - float weight = verticalSizeFactor * horizontalSizeFactor * distanceFactor; - if (hull.GetLinkedEntities().Any()) - { - // Ignore hulls that have any linked hulls to keep the calculations simple. - continue; - } - else if (hull.ConnectedGaps.Any(g => g.Open > 0 && (!g.IsRoomToRoom || g.Position.Y < hull.Position.Y))) - { - // Ignore hulls that have open gaps to outside or below the center point, because we'll want the room to be full of water and not be accessible without breaking the wall. - continue; - } - else if (thalamusItems.Any(i => i.CurrentHull == hull)) - { - // Don't create the brain in a room that already has thalamus items inside it. - continue; - } - else if (hull.Rect.Width < minSize.X || hull.Rect.Height < minSize.Y) - { - // Don't select too small rooms. - continue; - } - if (weight > 0) - { - potentialBrainHulls.Add((hull, weight)); - } - } + var potentialBrainHulls = GetPotentialBrainRooms(wreck, Config, minSize, thalamusItems); Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Select(pbh => pbh.hull).ToList(), potentialBrainHulls.Select(pbh => pbh.weight).ToList(), Rand.RandSync.ServerAndClient); var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(IsThalamus); if (brainHull == null) { - DebugConsole.AddWarning("Wreck AI: Cannot find a proper room for the brain. Using a random room."); + DebugConsole.ThrowError($"Wreck AI {wreck.Info.Name}: Cannot find a suitable room for the Thalamus brain. Using a random room. " + + $"The wreck should be fixed so that there's at least one room where the following conditions are met: No linked hulls, no open gaps in the floor or to outside the sub, and no other Thalamus items present in the hull.", + contentPackage: Config.ContentPackage); + brainHull = hulls.GetRandom(Rand.RandSync.ServerAndClient); } if (brainHull == null) { - DebugConsole.ThrowError("Wreck AI: Cannot find any room for the brain! Failed to create the Thalamus."); + DebugConsole.ThrowError($"Wreck AI {wreck.Info.Name}: Cannot find any room for the brain! Failed to create the Thalamus.", contentPackage: Config.ContentPackage); return; } + Debug.WriteLine($"Wreck AI {wreck.Info.Name}: Selected brain room: {brainHull.DisplayName}"); brainHull.WaterVolume = brainHull.Volume; brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false); brain.CurrentHull = brainHull; - var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient); - if (backgroundPrefab != null) - { - new Structure(brainHull.Rect, backgroundPrefab, wreck); - } - var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient); - if (horizontalWallPrefab != null) + + // Jam the doors, mainly to prevent any mechanisms from opening them. Also makes it a little bit more difficult for the player to breach into the brain room, because they now have to break the door. + foreach (Door door in brainHull.ConnectedGaps.Select(g => g.ConnectedDoor)) { - int height = (int)horizontalWallPrefab.Size.Y; - int halfHeight = height / 2; - int quarterHeight = halfHeight / 2; - new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck); - new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck); + if (door == null) { continue; } + door.IsJammed = true; + jammedDoors.Add(door); } - var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient); - if (verticalWallPrefab != null) + + var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient); + if (backgroundPrefab != null) { - int width = (int)verticalWallPrefab.Size.X; - int halfWidth = width / 2; - int quarterWidth = halfWidth / 2; - new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck); - new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck); + var background = new Structure(brainHull.Rect, backgroundPrefab, wreck); + background.SpriteDepth -= 0.01f; } foreach (Item item in thalamusItems) { @@ -360,6 +320,7 @@ private void SpawnInitialCells() public void Kill() { + jammedDoors.ForEach(d => d.IsJammed = false); thalamusItems.ForEach(i => i.Condition = 0); foreach (var turret in turrets) { @@ -376,27 +337,24 @@ public void Kill() protectiveCells.ForEach(c => c.OnDeath -= OnCellDeath); if (!IsClient) { - if (Config != null) + if (Config is { KillAgentsWhenEntityDies: true }) { - if (Config.KillAgentsWhenEntityDies) + protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null)); + if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent)) { - protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null)); - if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent)) + foreach (var character in Character.CharacterList) { - foreach (var character in Character.CharacterList) + // Kills ALL offensive agents that are near the thalamus. Not the ideal solution, + // but as long as spawning is handled via status effects, I don't know if there is any better way. + // In practice there shouldn't be terminal cells from different thalamus organisms at the same time. + // And if there was, the distance check should prevent killing the agents of a different organism. + if (character.SpeciesName == Config.OffensiveAgent) { - // Kills ALL offensive agents that are near the thalamus. Not the ideal solution, - // but as long as spawning is handled via status effects, I don't know if there is any better way. - // In practice there shouldn't be terminal cells from different thalamus organisms at the same time. - // And if there was, the distance check should prevent killing the agents of a different organism. - if (character.SpeciesName == Config.OffensiveAgent) + // Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this. + float maxDistance = Sonar.DefaultSonarRange; + if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance) { - // Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this. - float maxDistance = Sonar.DefaultSonarRange; - if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance) - { - character.Kill(CauseOfDeathType.Unknown, null); - } + character.Kill(CauseOfDeathType.Unknown, null); } } } @@ -515,5 +473,62 @@ public void ServerEventWrite(IWriteMessage msg, Client client, NetEntityEvent.ID msg.WriteBoolean(IsAlive); } #endif + + public static List<(Hull hull, float weight)> GetPotentialBrainRooms(Submarine wreck, WreckAIConfig wreckAI, Point minSize, IEnumerable thalamusItems = null) + { + var potentialBrainHulls = new List<(Hull hull, float weight)>(); + // Bigger hulls are allowed, but not preferred more than what's sufficient. + Vector2 sufficientSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f); + Rectangle worldBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width, wreck.Borders.Height)); + thalamusItems ??= GetThalamusEntities(wreck, wreckAI.Entity); + foreach (Hull hull in wreck.GetHulls(alsoFromConnectedSubs: false)) + { + if (hull.GetLinkedEntities().Any()) + { + // Ignore hulls that have any linked hulls to keep the calculations simple. + continue; + } + else if (hull.ConnectedGaps.Any(g => (g.Open > 0 || g.ConnectedDoor?.Item.Condition <= 0) && (!g.IsRoomToRoom || g.Position.Y < hull.Position.Y))) + { + // Ignore hulls that have open gaps to outside or below the center point, because we'll want the room to be full of water and not be accessible without breaking the wall. + // Gaps in the broken doors are not yet open at this stage. Also Door.IsBroken is not yet up-to-date, so we'll have to check the item condition. + continue; + } + else if (thalamusItems.Any(i => i.CurrentHull == hull && !i.HasTag(Tags.WireItem))) + { + // Don't create the brain in a room that already has thalamus items inside it. + continue; + } + else if (hull.Rect.Width < minSize.X || hull.Rect.Height < minSize.Y) + { + // Don't select too small rooms. + continue; + } + float weight = 0; + if (hull.IsAirlock) + { + // Prefer something else than airlocks + weight = 0; + } + else + { + float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition); + float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(worldBounds.Width, worldBounds.Height) / 2f, distanceFromCenter)); + float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficientSize.X, hull.Rect.Width)); + float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficientSize.Y, hull.Rect.Height)); + weight = verticalSizeFactor * horizontalSizeFactor * distanceFactor; + } + if (weight > 0 || potentialBrainHulls.None()) + { + potentialBrainHulls.Add((hull, weight)); + } + } + Debug.WriteLine($"Wreck AI {wreck.Info.Name}: Potential brain rooms: {potentialBrainHulls.Count}"); + foreach ((Hull hull, float weight) in potentialBrainHulls) + { + Debug.WriteLine($"Wreck AI: Potential brain room: {hull.DisplayName}, {weight.FormatSingleDecimal()}"); + } + return potentialBrainHulls; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 5f9ba04f73..723963e509 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -921,20 +921,16 @@ protected void UpdateClimbing() { isRemote = character.IsRemotelyControlled; } - if (isRemote) + //if the character is remotely controlled, + //let the server decide when to deselect the ladder and stop climbing + if (!isRemote) { - if (Math.Abs(targetMovement.X) > 0.05f || - (TargetMovement.Y < 0.0f && ConvertUnits.ToSimUnits(trigger.Height) + handPos.Y < HeadPosition) || - (TargetMovement.Y > 0.0f && handPos.Y > 0.1f)) + if ((character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right)) && + (!character.IsKeyDown(InputType.Up) && !character.IsKeyDown(InputType.Down))) { isClimbing = false; } } - else if ((character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right)) && - (!character.IsKeyDown(InputType.Up) && !character.IsKeyDown(InputType.Down))) - { - isClimbing = false; - } if (!isClimbing) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 5a6cb8e323..316d3db5d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -147,20 +147,7 @@ protected override void UpdateAnim(float deltaTime) if (!character.CanMove) { - levitatingCollider = false; - Collider.FarseerBody.FixedRotation = false; - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) - { - Collider.Enabled = false; - Collider.LinearVelocity = mainLimb.LinearVelocity; - Collider.SetTransformIgnoreContacts(mainLimb.SimPosition, mainLimb.Rotation); - //reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving - //(except when dragging, then we need the pull joints) - if (!Draggable || character.SelectedBy == null) - { - ResetPullJoints(); - } - } + UpdateRagdollControlsMovement(); if (character.IsDead && deathAnimTimer < deathAnimDuration) { deathAnimTimer += deltaTime; @@ -186,11 +173,11 @@ protected override void UpdateAnim(float deltaTime) if (InWater) { - Collider.SetTransform(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f); + Collider.SetTransformIgnoreContacts(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f); } else { - Collider.SetTransform(new Vector2( + Collider.SetTransformIgnoreContacts(new Vector2( Collider.SimPosition.X, Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)), 0.0f); @@ -995,7 +982,7 @@ public override void Flip() if (RagdollParams.IsSpritesheetOrientationHorizontal) { //horizontally aligned limbs need to be flipped 180 degrees - l.body.SetTransform(l.SimPosition, l.body.Rotation + MathHelper.Pi * Dir); + l.body.SetTransformIgnoreContacts(l.SimPosition, l.body.Rotation + MathHelper.Pi * Dir); } //no need to do anything when flipping vertically oriented limbs //the sprite gets flipped horizontally, which does the job diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 0d1699f2db..6769becd01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -296,25 +296,7 @@ protected override void UpdateAnim(float deltaTime) fallingProneAnimTimer += deltaTime; UpdateFallingProne(1.0f); } - levitatingCollider = false; - Collider.FarseerBody.FixedRotation = false; - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) - { - if (Collider.Enabled) - { - //deactivating the collider -> make the main limb inherit the collider's velocity because it'll control the movement now - MainLimb.body.LinearVelocity = Collider.LinearVelocity; - Collider.Enabled = false; - } - Collider.LinearVelocity = MainLimb.LinearVelocity; - Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation); - //reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving - //(except when dragging, then we need the pull joints) - if (!Draggable || character.SelectedBy == null) - { - ResetPullJoints(); - } - } + UpdateRagdollControlsMovement(); return; } fallingProneAnimTimer = 0.0f; @@ -324,7 +306,7 @@ protected override void UpdateAnim(float deltaTime) { var lowestLimb = FindLowestLimb(); - Collider.SetTransform(new Vector2( + Collider.SetTransformIgnoreContacts(new Vector2( Collider.SimPosition.X, Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)), Collider.Rotation); @@ -356,7 +338,7 @@ protected override void UpdateAnim(float deltaTime) float angleDiff = MathUtils.GetShortestAngle(Collider.Rotation, 0.0f); if (Math.Abs(angleDiff) > 0.001f) { - Collider.SetTransform(Collider.SimPosition, Collider.Rotation + angleDiff); + Collider.SetTransformIgnoreContacts(Collider.SimPosition, Collider.Rotation + angleDiff); } } @@ -581,9 +563,7 @@ void UpdateStanding() footMid += (Math.Max(Math.Abs(walkPosX) * limpAmount, 0.0f) * Math.Min(Math.Abs(TargetMovement.X), 0.3f)) * Dir; } - movement = overrideTargetMovement == Vector2.Zero ? - MathUtils.SmoothStep(movement, TargetMovement, movementLerp) : - overrideTargetMovement; + movement = overrideTargetMovement ?? MathUtils.SmoothStep(movement, TargetMovement, movementLerp); if (Math.Abs(movement.X) < 0.005f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 20b349ca55..9fd4a952d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -111,7 +111,7 @@ public bool Frozen //a movement vector that overrides targetmovement if trying to steer //a Character to the position sent by server in multiplayer mode - protected Vector2 overrideTargetMovement; + protected Vector2? overrideTargetMovement; protected float floorY, standOnFloorY; protected Fixture floorFixture; @@ -141,6 +141,12 @@ public bool Frozen private Category prevCollisionCategory = Category.None; + /// + /// When the character is alive/conscious, the collider drives the character's movement and is used to sync the character's position in MP. + /// When unconscious, the ragdoll controls the movement and the collider just sticks to the main limb. + /// + public bool ColliderControlsMovement => character.CanMove; + public bool IsStuck => Limbs.Any(l => l.IsStuck); public PhysicsBody Collider @@ -188,7 +194,7 @@ public int ColliderIndex Vector2 pos = collider[colliderIndex].SimPosition; pos.Y -= collider[colliderIndex].Height * 0.5f; pos.Y += collider[value].Height * 0.5f; - collider[value].SetTransform(pos, collider[colliderIndex].Rotation); + collider[value].SetTransformIgnoreContacts(pos, collider[colliderIndex].Rotation); collider[value].LinearVelocity = collider[colliderIndex].LinearVelocity; collider[value].AngularVelocity = collider[colliderIndex].AngularVelocity; @@ -285,7 +291,7 @@ public bool SimplePhysicsEnabled foreach (Limb limb in Limbs) { if (limb.IsSevered || !limb.body.PhysEnabled) { continue; } - limb.body.SetTransform(Collider.SimPosition, Collider.Rotation); + limb.body.SetTransformIgnoreContacts(Collider.SimPosition, Collider.Rotation); //reset pull joints (they may be somewhere far away if the character has moved from the position where animations were last updated) limb.PullJointEnabled = false; limb.PullJointWorldAnchorB = limb.SimPosition; @@ -300,11 +306,11 @@ public Vector2 TargetMovement { get { - return (overrideTargetMovement == Vector2.Zero) ? targetMovement : overrideTargetMovement; + return overrideTargetMovement ?? targetMovement; } set { - if (!MathUtils.IsValid(value)) return; + if (!MathUtils.IsValid(value)) { return; } targetMovement.X = MathHelper.Clamp(value.X, -MAX_SPEED, MAX_SPEED); targetMovement.Y = MathHelper.Clamp(value.Y, -MAX_SPEED, MAX_SPEED); } @@ -1299,6 +1305,11 @@ public void UpdateRagdoll(float deltaTime, Camera cam) } } + float MaxVel = NetConfig.MaxPhysicsBodyVelocity; + Collider.LinearVelocity = new Vector2( + NetConfig.Quantize(Collider.LinearVelocity.X, -MaxVel, MaxVel, 12), + NetConfig.Quantize(Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12)); + if (forceStanding) { inWater = false; @@ -1443,7 +1454,7 @@ public void UpdateRagdoll(float deltaTime, Camera cam) else { // Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck. - if (Collider.LinearVelocity == Vector2.Zero) + if (Collider.LinearVelocity == Vector2.Zero && !character.IsRemotePlayer) { character.IsRagdolled = true; if (character.IsBot) @@ -1458,6 +1469,30 @@ public void UpdateRagdoll(float deltaTime, Camera cam) forceNotStanding = false; } + /// + /// Update the logic that needs to run when the ragdoll is what controls the character's movement instead of the collider + /// (making the collider stick to the ragdoll's main limb). + /// + protected void UpdateRagdollControlsMovement() + { + levitatingCollider = false; + Collider.FarseerBody.FixedRotation = false; + if (Collider.Enabled) + { + //deactivating the collider -> make the main limb inherit the collider's velocity because it'll control the movement now + MainLimb.body.LinearVelocity = Collider.LinearVelocity; + Collider.Enabled = false; + } + Collider.LinearVelocity = MainLimb.LinearVelocity; + Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation); + //reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving + //(except when dragging, then we need the pull joints) + if (!Draggable || character.SelectedBy == null) + { + ResetPullJoints(); + } + } + private void CheckBodyInRest(float deltaTime) { if (SimplePhysicsEnabled) { return; } @@ -2094,7 +2129,7 @@ protected void CheckDistFromCollider() partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos); private void UpdateNetPlayerPosition(float deltaTime) { - if (GameMain.NetworkMember == null) return; + if (GameMain.NetworkMember == null) { return; } float lowestSubPos = float.MaxValue; if (Submarine.Loaded.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 5bbb477689..d7fa0e0de7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -197,7 +197,22 @@ public float ItemDamage /// /// Used for multiplying all the damage. /// - public float DamageMultiplier { get; set; } = 1; + public float DamageMultiplier + { + get => _damageMultiplier ?? initialDamageMultiplier; + set + { + if (!_damageMultiplier.HasValue) + { + SetInitialDamageMultiplier(value); + } + _damageMultiplier = value; + } + } + private float? _damageMultiplier; + private float initialDamageMultiplier = 1.0f; + public void ResetDamageMultiplier() => _damageMultiplier = initialDamageMultiplier; + public void SetInitialDamageMultiplier(float value) => initialDamageMultiplier = value; /// /// Used for multiplying all the ranges. @@ -275,6 +290,8 @@ public string ApplyForceOnLimbs [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldEnd { get; private set; } + public bool HasRootForce => RootForceWorldStart != Vector2.Zero || RootForceWorldMiddle != Vector2.Zero || RootForceWorldEnd != Vector2.Zero; + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:"Applied to the main limb. The transition smoothing of the applied force."), Editable] public TransitionMode RootTransitionEasing { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 32dad407a4..c3ca1c2e16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -35,6 +35,8 @@ partial class Character : Entity, IDamageable, ISerializableEntity, IClientSeria public const float MaxHighlightDistance = 150.0f; public const float MaxDragDistance = 200.0f; + public override ContentPackage ContentPackage => Prefab?.ContentPackage; + partial void UpdateLimbLightSource(Limb limb); private bool enabled = true; @@ -663,9 +665,23 @@ public float Mass public bool RequireConsciousnessForCustomInteract = true; public bool AllowCustomInteract { - get { return (!RequireConsciousnessForCustomInteract || (!IsIncapacitated && Stun <= 0.0f)) && !Removed; } + get + { + if (CampaignMode.HostileFactionDisablesInteraction(CampaignInteractionType) && + AIController is HumanAIController humanAi && humanAi.IsInHostileFaction()) + { + return false; + } + + return (!RequireConsciousnessForCustomInteract || (!IsIncapacitated && Stun <= 0.0f)) && !Removed; + } } + public bool ShouldShowCustomInteractText => + !CustomInteractHUDText.IsNullOrEmpty() && + AllowCustomInteract && + (AIController is not HumanAIController humanAi || humanAi.AllowCampaignInteraction()); + private float lockHandsTimer; public bool LockHands { @@ -1209,6 +1225,7 @@ public bool UseHealthWindow } public CampaignMode.InteractionType CampaignInteractionType; + public Identifier MerchantIdentifier; private bool accessRemovedCharacterErrorShown; @@ -1262,6 +1279,10 @@ public override Vector2 DrawPosition public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID; public bool IsInPlayerSub => Submarine != null && Submarine.Info.IsPlayer; + /// + /// Alias for , so the same property name works on both items and characters. + /// + public bool InPlayerSubmarine => IsInPlayerSub; public float AITurretPriority { @@ -1406,7 +1427,7 @@ protected Character(CharacterPrefab prefab, Vector2 position, string seed, Chara if (characterInfo?.HumanPrefabIds is { } prefabIds && prefabIds.NpcSetIdentifier != default && prefabIds.NpcIdentifier != default) { - humanPrefab = NPCSet.Get( + HumanPrefab = NPCSet.Get( characterInfo.HumanPrefabIds.NpcSetIdentifier, characterInfo.HumanPrefabIds.NpcIdentifier); } @@ -1759,6 +1780,11 @@ public bool IsKeyDown(InputType inputType) } } + if (this == Controlled && inputType == InputType.Run && ToggleRun) + { + return true; + } + return keys[(int)inputType].Held; } @@ -1957,6 +1983,8 @@ public bool DisableRunning } } + public bool ToggleRun; + public bool CanRunWhileDragging() { if (selectedCharacter is not { IsDraggable: true }) { return true; } @@ -2162,8 +2190,9 @@ public void Control(float deltaTime, Camera cam) SmoothedCursorPosition = cursorPosition - smoothedCursorDiff; } - bool aiControlled = this is AICharacter && Controlled != this && !IsRemotelyControlled; - if (!aiControlled) + bool aiControlled = this is AICharacter && Controlled != this && !IsRemotePlayer; + bool controlledByServer = GameMain.NetworkMember is { IsClient: true } && IsRemotelyControlled; + if (!aiControlled && !controlledByServer) { Vector2 targetMovement = GetTargetMovement(); AnimController.TargetMovement = targetMovement; @@ -2192,7 +2221,8 @@ public void Control(float deltaTime, Camera cam) { AnimController.TargetDir = Direction.Right; } - else + //only humanoids' flipping is controlled by the cursor, monster flipping is driven by their movement in FishAnimController + else if (AnimController is HumanoidAnimController) { if (CursorPosition.X < AnimController.Collider.Position.X - cursorFollowMargin) { @@ -2255,15 +2285,9 @@ public void Control(float deltaTime, Camera cam) } else if (IsKeyDown(InputType.Attack)) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this) - { - if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false) - { - currentAttackTarget = default; - } - currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _); - } - else if (IsPlayer) + //normally the attack target, where to aim the attack and such is handled by EnemyAIController, + //but in the case of player-controlled monsters, we handle it here + if (IsPlayer) { float dist = -1; Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position); @@ -2308,13 +2332,16 @@ public void Control(float deltaTime, Camera cam) } } var currentContexts = GetAttackContexts(); - var validLimbs = AnimController.Limbs.Where(l => + var attackLimbs = AnimController.Limbs.Where(static l => l.attack != null); + bool hasAttacksWithoutRootForce = attackLimbs.Any(static l=> !l.attack.HasRootForce); + var validLimbs = attackLimbs.Where(l => { if (l.IsSevered || l.IsStuck) { return false; } if (l.Disabled) { return false; } var attack = l.attack; - if (attack == null) { return false; } if (attack.CoolDownTimer > 0) { return false; } + //disallow attacks with root force if there's any other attacks available + if (hasAttacksWithoutRootForce && attack.HasRootForce) { return false; } if (!attack.IsValidContext(currentContexts)) { return false; } if (attackTarget != null) { @@ -2352,6 +2379,14 @@ public void Control(float deltaTime, Camera cam) } } } + else if (GameMain.NetworkMember is { IsClient: true } && Controlled != this) + { + if (currentAttackTarget.DamageTarget is Entity { Removed: true }) + { + currentAttackTarget = default; + } + currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _); + } } if (Inventory != null) @@ -2472,119 +2507,11 @@ public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = nu seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb(); if (target is Character targetCharacter) { - return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing); - } - else - { - return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing); - } - } - - public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false) - { - if (seeingEntity is Character seeingCharacter) - { - return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing); - } - if (target is Character targetCharacter) - { - return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing); - } - else - { - return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing); - } - } - - private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false) - { - System.Diagnostics.Debug.Assert(target != null); - if (target == null || target.Removed) { return false; } - if (seeingEntity == null) { return false; } - if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; } - if (!target.AnimController.SimplePhysicsEnabled) - { - //find the limbs that are furthest from the target's position (from the viewer's point of view) - Limb leftExtremity = null, rightExtremity = null; - float leftMostDot = 0.0f, rightMostDot = 0.0f; - Vector2 dir = target.WorldPosition - seeingEntity.WorldPosition; - Vector2 leftDir = new Vector2(dir.Y, -dir.X); - Vector2 rightDir = new Vector2(-dir.Y, dir.X); - foreach (Limb limb in target.AnimController.Limbs) - { - if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; } - if (limb.Hidden) { continue; } - Vector2 limbDir = limb.WorldPosition - seeingEntity.WorldPosition; - float leftDot = Vector2.Dot(limbDir, leftDir); - if (leftDot > leftMostDot) - { - leftMostDot = leftDot; - leftExtremity = limb; - continue; - } - float rightDot = Vector2.Dot(limbDir, rightDir); - if (rightDot > rightMostDot) - { - rightMostDot = rightDot; - rightExtremity = limb; - continue; - } - } - if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; } - if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; } - } - return false; - } - - private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false) - { - System.Diagnostics.Debug.Assert(target != null); - if (target == null) { return false; } - if (seeingEntity == null) { return false; } - // TODO: Could we just use the method below? If not, let's refactor it so that we can. - Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition); - if (checkFacing && seeingEntity is Character seeingCharacter) - { - if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; } - } - //both inside the same sub (or both outside) - //OR the we're inside, the other character outside - if (target.Submarine == seeingEntity.Submarine || target.Submarine == null) - { - return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null; - } - //we're outside, the other character inside - else if (seeingEntity.Submarine == null) - { - return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null; + return ISpatialEntity.IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing); } - //both inside different subs else { - return - Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null && - Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null; - } - - bool IsBlocking(Fixture f) - { - var body = f.Body; - if (body == null) { return false; } - if (body.UserData is Structure wall) - { - if (!wall.CastShadow && seeThroughWindows) { return false; } - return wall != target; - } - else if (body.UserData is Item item) - { - if (item.GetComponent() is { HasWindow: true } door && seeThroughWindows) - { - if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; } - } - - return item != target; - } - return true; + return ISpatialEntity.CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing); } } @@ -2733,7 +2660,7 @@ public bool CanBeHealedBy(Character character, bool checkFriendlyTeam = true) => public bool CanBeDraggedBy(Character character) { if (!IsDraggable) { return false; } - return IsKnockedDown || LockHands || IsPet || (IsBot && character.TeamID == TeamID); + return IsKnockedDown || LockHands || (IsPet && character.IsFriendly(this)) || (IsBot && character.TeamID == TeamID); } /// @@ -3650,7 +3577,12 @@ bool bodyMovingTooFast(PhysicsBody body) { humanAnimController.Crouching = false; } - if (IsRagdolled) { AnimController.IgnorePlatforms = true; } + //ragdolling manually makes the character go through platforms + //EXCEPT for clients, they rely on the server telling whether platforms should be ignored or not + if (IsRagdolled && GameMain.NetworkMember is not { IsClient: true }) + { + AnimController.IgnorePlatforms = true; + } AnimController.ResetPullJoints(); SelectedItem = SelectedSecondaryItem = null; return; @@ -4102,6 +4034,7 @@ public void SetOrder(Order order, bool isNewOrder, bool speak = true, bool force if (character.TeamID != TeamID) { continue; } if (character.AIController is not HumanAIController) { continue; } if (!HumanAIController.IsActive(character)) { continue; } + if (character.Info == null) { continue; } foreach (var currentOrder in character.CurrentOrders) { if (currentOrder == null) { continue; } @@ -4117,12 +4050,15 @@ public void SetOrder(Order order, bool isNewOrder, bool speak = true, bool force case OrderCategory.Movement: // If there character has another movement order, dismiss that order Order orderToReplace = null; - foreach (var currentOrder in CurrentOrders) + if (CurrentOrders != null) { - if (currentOrder == null) { continue; } - if (currentOrder.Category != OrderCategory.Movement) { continue; } - orderToReplace = currentOrder; - break; + foreach (var currentOrder in CurrentOrders) + { + if (currentOrder == null) { continue; } + if (currentOrder.Category != OrderCategory.Movement) { continue; } + orderToReplace = currentOrder; + break; + } } if (orderToReplace is { AutoDismiss: true }) { @@ -4158,6 +4094,7 @@ public void SetOrder(Order order, bool isNewOrder, bool speak = true, bool force private void AddCurrentOrder(Order newOrder) { + if (CurrentOrders == null) { return; } if (newOrder == null || newOrder.Identifier == "dismissed") { if (newOrder.Option != Identifier.Empty) @@ -4199,9 +4136,9 @@ private void AddCurrentOrder(Order newOrder) } } - private bool RemoveDuplicateOrders(Order order) + private void RemoveDuplicateOrders(Order order) { - bool removed = false; + if (CurrentOrders == null) { return; } int? priorityOfRemoved = null; for (int i = CurrentOrders.Count - 1; i >= 0; i--) { @@ -4210,12 +4147,11 @@ private bool RemoveDuplicateOrders(Order order) { priorityOfRemoved = orderInfo.ManualPriority; CurrentOrders.RemoveAt(i); - removed = true; break; } } - if (!priorityOfRemoved.HasValue) { return removed; } + if (!priorityOfRemoved.HasValue) { return; } for (int i = 0; i < CurrentOrders.Count; i++) { @@ -4226,11 +4162,9 @@ private bool RemoveDuplicateOrders(Order order) } } - CurrentOrders.RemoveAll(order => order.ManualPriority <= 0); + CurrentOrders.RemoveAll(o => o.ManualPriority <= 0); // Sort the current orders so the one with the highest priority comes first CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); - - return removed; } public Order GetCurrentOrderWithTopPriority() @@ -4315,6 +4249,30 @@ public void Speak(string message, ChatMessageType? messageType = null, float del aiChatMessageQueue.Add(new AIChatMessage(message, messageType, identifier, delay)); } +#if CLIENT + public void SendSinglePlayerMessage(AIChatMessage message, bool canUseRadio, WifiComponent radio) + { + if (message.MessageType == null) + { + message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default; + } + if (GameMain.GameSession?.CrewManager is { IsSinglePlayer: true } crewManager) + { + string modifiedMessage = ChatMessage.ApplyDistanceEffect(message.Message, message.MessageType.Value, this, Controlled); + if (!string.IsNullOrEmpty(modifiedMessage)) + { + 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); + } + } + ShowSpeechBubble(ChatMessage.MessageColor[(int)message.MessageType.Value], message.Message); + } +#endif + private void UpdateAIChatMessages(float deltaTime) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } @@ -4331,28 +4289,13 @@ private void UpdateAIChatMessages(float deltaTime) message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default; } #if CLIENT - if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) - { - string modifiedMessage = ChatMessage.ApplyDistanceEffect(message.Message, message.MessageType.Value, this, Controlled); - if (!string.IsNullOrEmpty(modifiedMessage)) - { - 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); - } - } + SendSinglePlayerMessage(message, canUseRadio, radio); #endif #if SERVER if (GameMain.Server != null && message.MessageType != ChatMessageType.Order) { GameMain.Server.SendChatMessage(message.Message, message.MessageType.Value, null, this); } -#endif -#if CLIENT - ShowSpeechBubble(ChatMessage.MessageColor[(int)message.MessageType.Value], message.Message); #endif sentMessages.Add(message); } @@ -4432,10 +4375,12 @@ public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attac { attackAfflictions = attack.Afflictions.Keys; } - + + float damageMultiplier = attack.DamageMultiplier * attackData.DamageMultiplier; + var attackResult = targetLimb == null ? - AddDamage(worldPosition, attackAfflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier * attackData.DamageMultiplier) : - DamageLimb(worldPosition, targetLimb, attackAfflictions, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier * attackData.DamageMultiplier, penetration: penetration + attackData.AddedPenetration, shouldImplode: attackData.ShouldImplode); + AddDamage(worldPosition, attackAfflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker, damageMultiplier) : + DamageLimb(worldPosition, targetLimb, attackAfflictions, attack.Stun, playSound, attackImpulse, attacker, damageMultiplier, penetration: penetration + attackData.AddedPenetration, shouldImplode: attackData.ShouldImplode); if (attacker != null) { @@ -5311,7 +5256,7 @@ public void SpawnInventoryItems(Inventory inventory, ContentXElement itemData) { SpawnInventoryItemsRecursive(inventory, itemData, new List()); } - + private void SpawnInventoryItemsRecursive(Inventory inventory, ContentXElement element, List extraDuffelBags) { foreach (var itemElement in element.Elements()) @@ -5326,8 +5271,8 @@ private void SpawnInventoryItemsRecursive(Inventory inventory, ContentXElement e } #if SERVER newItem.GetComponent()?.SyncHistory(); - if (newItem.GetComponent() is WifiComponent wifiComponent) { newItem.CreateServerEvent(wifiComponent); } if (newItem.GetComponent() is GeneticMaterial geneticMaterial) { newItem.CreateServerEvent(geneticMaterial); } + SyncInGameEditables(newItem); #endif int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 }); if (!slotIndices.Any()) @@ -5550,7 +5495,7 @@ public List GetVisibleHulls() /// /// Removes the talents the character has unlocked in their talent tree. /// - public void ResetTalents(bool applyXpPenalty) + public void ResetTalents(int talentPointReduction) { characterTalents.Clear(); abilityResistances.Clear(); @@ -5558,13 +5503,17 @@ public void ResetTalents(bool applyXpPenalty) CharacterHealth.RemoveAfflictions(affliction => affliction.Prefab.AfflictionType == Tags.AfflictionTypeTalentBuff); statValues.Clear(); - if (applyXpPenalty) + for (int i = 0; i < talentPointReduction; i++) { int currentLevel = info.GetCurrentLevel(); if (currentLevel > 0) { info.SetExperience(info.ExperiencePoints - CharacterInfo.ExperienceRequiredPerLevel(currentLevel)); } + else + { + break; + } } } @@ -5919,7 +5868,7 @@ public float GetAbilityResistance(Identifier resistanceId) } // NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance - return hadResistance ? resistance : 1f; + return hadResistance ? Math.Max(0, resistance) : 1f; } public float GetAbilityResistance(AfflictionPrefab affliction) @@ -5938,7 +5887,7 @@ public float GetAbilityResistance(AfflictionPrefab affliction) } // NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance - return hadResistance ? resistance : 1f; + return hadResistance ? Math.Max(0, resistance) : 1f; } public void ChangeAbilityResistance(TalentResistanceIdentifier identifier, float value) @@ -5975,7 +5924,7 @@ public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType // NPCs are friendly to the same team and the friendly NPCs CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC, // Friendly NPCs are friendly to both player teams - CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2, + CharacterTeamType.FriendlyNPC => otherTeam is CharacterTeamType.Team1 or CharacterTeamType.Team2, // None (bandits and such) consider friendly NPCs friendly, not attacking them unless they attack first // Otherwise bandits would for example attach the hostages. CharacterTeamType.None => otherTeam == CharacterTeamType.FriendlyNPC, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index a153949dcc..a96d2e805a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1282,7 +1282,7 @@ private bool IsAllowed(XElement element, string spriteName) partial void LoadAttachmentSprites(); - public int CalculateSalary() + public int CalculateSalary(int baseSalary = 0, float salaryMultiplier = 1.0f) { if (Name == null || Job == null) { return 0; } @@ -1292,7 +1292,7 @@ public int CalculateSalary() salary += (int)(skill.Level * skill.PriceMultiplier); } - return (int)(salary * Job.Prefab.PriceMultiplier); + return (int)(baseSalary + (salary * Job.Prefab.PriceMultiplier * salaryMultiplier)); } /// @@ -1485,11 +1485,9 @@ public void RefundTalents() //e.g. talents from endocrine booster or extra talents some special NPC has var talentsFromOutsideTree = GetUnlockedTalentsOutsideTree().ToList(); - bool applyXpPenalty = talentResetCount > 0; - UnlockedTalents.Clear(); SavedStatValues.Clear(); - Character?.ResetTalents(applyXpPenalty); + Character?.ResetTalents(talentPointReduction: talentResetCount); TalentRefundPoints--; talentResetCount++; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterNetworking.cs index 5d8e294ec4..85aa05d697 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterNetworking.cs @@ -14,24 +14,29 @@ class CharacterStateInfo : PosInfo public readonly AnimController.Animation Animation; - public CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None) - : this(pos, rotation, velocity, angularVelocity, 0, time, dir, selectedCharacter, selectedItem, selectedSecondaryItem, animation) + public bool IgnorePlatforms; + + public readonly Vector2 TargetMovement; + + public CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, Vector2 targetMovement, AnimController.Animation animation = AnimController.Animation.None, bool ignorePlatforms = false) + : this(pos, rotation, velocity, angularVelocity, 0, time, dir, selectedCharacter, selectedItem, selectedSecondaryItem, targetMovement, animation, ignorePlatforms) { } - public CharacterStateInfo(Vector2 pos, float? rotation, UInt16 ID, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None) - : this(pos, rotation, Vector2.Zero, 0.0f, ID, 0.0f, dir, selectedCharacter, selectedItem, selectedSecondaryItem, animation) + public CharacterStateInfo(Vector2 pos, float? rotation, UInt16 ID, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, Vector2 targetMovement, AnimController.Animation animation = AnimController.Animation.None, bool ignorePlatforms = false) + : this(pos, rotation, Vector2.Zero, 0.0f, ID, 0.0f, dir, selectedCharacter, selectedItem, selectedSecondaryItem, targetMovement, animation, ignorePlatforms) { } - protected CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, UInt16 ID, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, AnimController.Animation animation = AnimController.Animation.None) + protected CharacterStateInfo(Vector2 pos, float? rotation, Vector2 velocity, float? angularVelocity, UInt16 ID, float time, Direction dir, Character selectedCharacter, Item selectedItem, Item selectedSecondaryItem, Vector2 targetMovement, AnimController.Animation animation = AnimController.Animation.None, bool ignorePlatforms = false) : base(pos, rotation, velocity, angularVelocity, ID, time) { Direction = dir; SelectedCharacter = selectedCharacter; SelectedItem = selectedItem; SelectedSecondaryItem = selectedSecondaryItem; - + IgnorePlatforms = ignorePlatforms; + TargetMovement = targetMovement; Animation = animation; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 69aa033cc6..4bc7de6030 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -398,7 +398,7 @@ public readonly struct AppliedStatValue public readonly float MinValue; /// - /// Minimum value to apply + /// Maximum value to apply /// public readonly float MaxValue; @@ -764,6 +764,12 @@ public override void Dispose() { } /// public readonly float TreatmentThreshold; + /// + /// How strong the affliction needs to be for treatment suggestions to be shown in the health interface. + /// Defaults to . + /// + public readonly float TreatmentSuggestionThreshold; + /// /// Bots will not try to treat the affliction if the character has any of these afflictions /// @@ -941,6 +947,7 @@ public AfflictionPrefab(ContentXElement element, AfflictionsFile file, Type type ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold), Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold)); TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 10.0f)); + TreatmentSuggestionThreshold = element.GetAttributeFloat(nameof(TreatmentSuggestionThreshold), TreatmentThreshold); DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f); BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 8e276723cf..051f94389a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -1184,7 +1184,11 @@ private List GetAllAfflictions(bool mergeSameAfflictions, Func /// A dictionary where the key is the identifier of the item and the value the suitability /// If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds. - public void GetSuitableTreatments(Dictionary treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) + /// Should the method check whether the afflictions are above (whether they're severe enough for AI to treat)? + /// Should the method check whether the afflictions are above (whether treatment suggestions are shown in the health interface)? + public void GetSuitableTreatments(Dictionary treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, + bool checkTreatmentThreshold = true, bool checkTreatmentSuggestionThreshold = true, + float predictFutureDuration = 0.0f) { //key = item identifier //float = suitability @@ -1235,7 +1239,14 @@ public void GetSuitableTreatments(Dictionary treatmentSuitabi //if this a suitable treatment, ignore it if the affliction isn't severe enough to treat //if the suitability is negative though, we need to take it into account! //otherwise we may end up e.g. giving too much opiates to someone already close to overdosing - if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; } + if (checkTreatmentThreshold) + { + if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; } + } + if (checkTreatmentSuggestionThreshold) + { + if (totalAfflictionStrength < affliction.Prefab.TreatmentSuggestionThreshold) { continue; } + } } if (treatment.Value > strength) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index baf34fdaa8..c6148d1ed5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -33,6 +33,12 @@ class HumanPrefab : PrefabWithUintIdentifier [Serialize(0, IsPropertySaveable.No)] public int ExperiencePoints { get; private set; } + [Serialize(0, IsPropertySaveable.No)] + public int BaseSalary { get; private set; } + + [Serialize(1f, IsPropertySaveable.No)] + public float SalaryMultiplier { get; private set; } + private readonly HashSet tags = new HashSet(); [Serialize("", IsPropertySaveable.Yes)] @@ -247,8 +253,8 @@ public CharacterInfo CreateCharacterInfo(Rand.RandSync randSync = Rand.RandSync. float newSkill = skill.Level * SkillMultiplier; skill.IncreaseSkill(newSkill - skill.Level, increasePastMax: false); } - characterInfo.Salary = characterInfo.CalculateSalary(); } + characterInfo.Salary = characterInfo.CalculateSalary(BaseSalary, SalaryMultiplier); characterInfo.HumanPrefabIds = (NpcSetIdentifier, Identifier); characterInfo.GiveExperience(ExperiencePoints); return characterInfo; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs index cdf076761e..cf5ba43b35 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs @@ -19,7 +19,7 @@ class SkillPrefab public SkillPrefab(ContentXElement element) { Identifier = element.GetAttributeIdentifier("identifier", ""); - PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 25.0f); + PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 15.0f); levelRange = GetSkillRange("level", element, defaultValue: new Range(0, 0)); levelRangePvP = GetSkillRange("pvplevel", element, defaultValue: levelRange); IsPrimarySkill = element.GetAttributeBool("primary", false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 4bcf6b50ce..1e9755e5a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -213,7 +213,9 @@ partial class Limb : ISerializableEntity, ISpatialEntity public readonly Ragdoll ragdoll; public readonly LimbParams Params; - //the physics body of the limb + /// + /// The physics body of the limb + /// public PhysicsBody body; public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale; @@ -528,6 +530,9 @@ public float Alpha public readonly List WearingItems = new List(); + /// + /// Other wearables attached to the head. I.e. husk sprite, hair, beard, moustache, and face attachments. + /// public readonly List OtherWearables = new List(); public bool PullJointEnabled @@ -721,7 +726,7 @@ public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams) var attackElement = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("attack"); if (attackElement != null) { - attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f); + attack.SetInitialDamageMultiplier(attackElement.GetAttributeFloat("damagemultiplier", 1f)); attack.RangeMultiplier = attackElement.GetAttributeFloat("rangemultiplier", 1f); attack.ImpactMultiplier = attackElement.GetAttributeFloat("impactmultiplier", 1f); } @@ -1014,7 +1019,6 @@ public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable dama float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos)); bool wasRunning = attack.IsRunning; attack.UpdateAttackTimer(deltaTime, character); - attack.DamageMultiplier = 1.0f + character.GetStatValue(attack.Ranged ? StatTypes.NaturalRangedAttackMultiplier : StatTypes.NaturalMeleeAttackMultiplier); if (attack.Blink) { @@ -1164,7 +1168,7 @@ public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable dama // Set the main collider where the body lands after the attack if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f) { - character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation); + character.AnimController.Collider.SetTransformIgnoreContacts(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation); } } return wasHit; @@ -1180,9 +1184,11 @@ public void ExecuteAttack(IDamageable damageTarget, Limb targetLimb, out AttackR LastAttackSoundTime = SoundInterval; } #endif - if (damageTarget is Character targetCharacter && targetLimb != null) - { - attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound, body, this); + attack.ResetDamageMultiplier(); + attack.DamageMultiplier *= 1.0f + character.GetStatValue(attack.Ranged ? StatTypes.NaturalRangedAttackMultiplier : StatTypes.NaturalMeleeAttackMultiplier); + if (damageTarget is Character && targetLimb != null) + { + attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, deltaTime: 1.0f, playSound, body, sourceLimb: this); } else { @@ -1192,7 +1198,7 @@ public void ExecuteAttack(IDamageable damageTarget, Limb targetLimb, out AttackR } else { - attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound, body, this); + attackResult = attack.DoDamage(character, damageTarget, WorldPosition, deltaTime: 1.0f, playSound, body, sourceLimb: this); } } /*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 2404f0b57d..3a2562d48d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -189,7 +189,7 @@ public static string GetFolder(Identifier speciesName) CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName); if (prefab?.ConfigElement == null) { - DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'"); + DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", contentPackage: prefab?.ContentPackage); return string.Empty; } return GetFolder(prefab.ConfigElement, prefab.FilePath.Value); @@ -414,7 +414,7 @@ public static AnimationParams Create(string fullPath, Identifier speciesName, An { if (animationType == AnimationType.NotDefined) { - throw new Exception("Cannot create an animation file of type " + animationType.ToString()); + throw new Exception("Cannot create an animation file of type " + animationType); } if (!allAnimations.TryGetValue(speciesName, out Dictionary anims)) { @@ -543,7 +543,7 @@ public static Type GetParamTypeFromAnimType(AnimationType type, bool isHumanoid) { if (doc == null) { - DebugConsole.ThrowError("[AnimationParams] The source XML Document is null!"); + DebugConsole.ThrowError("[AnimationParams] The source XML Document is null!", contentPackage: Path.ContentPackage); return; } Serialize(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 3dd5f9a7a9..44c4f6a8f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -193,25 +193,32 @@ public static XElement CreateVariantXml(ContentXElement variantXML, ContentXElem { return newXml; } - // CreateVariantXML seems to merge the ai targets so that in the new xml we have both the old and the new target definitions. + + // CreateVariantXML does not understand anything about targeting tags, it just replaces the elements in the order they're defined in. + // We can do better here by replacing the target with a matching tag, so let's clear the element and do that. var finalAiElement = newXml.GetChildElement("ai"); - var processedTags = new HashSet(); - foreach (var aiTarget in finalAiElement.Elements().ToArray()) + finalAiElement.Elements().Remove(); + + //add all the targets from the base character + baseAi.Elements().ForEach(e => finalAiElement.Add(e)); + + var processedTags = new List(); + foreach (var variantTargetElement in variantAi.Elements()) { - string tag = aiTarget.GetAttributeString("tag", null); - if (tag == null) { continue; } - if (processedTags.Contains(tag)) + Identifier tag = variantTargetElement.GetAttributeIdentifier("tag", Identifier.Empty); + var matchingElements = finalAiElement.Elements().Where(e => e.GetAttributeIdentifier("tag", Identifier.Empty) == tag); + int alreadyProcessed = processedTags.Count(t => t == tag); + if (matchingElements.Count() > alreadyProcessed) { - aiTarget.Remove(); - continue; + //more matching elements found, replace the first one that hasn't been processed yet + matchingElements.Skip(alreadyProcessed).First().ReplaceWith(variantTargetElement); } - processedTags.Add(tag); - var matchInSelf = variantAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag); - var matchInParent = baseAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag); - if (matchInSelf != null && matchInParent != null) + else { - aiTarget.ReplaceWith(new XElement(matchInSelf)); + //no more matching elements in the base XML, this must be a new target + finalAiElement.Add(variantTargetElement); } + processedTags.Add(tag); } return newXml; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 17d2210a07..f6094f8e05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -137,15 +137,14 @@ protected IEnumerable GetAllSubParams() => .Concat(Joints); public static string GetDefaultFileName(Identifier speciesName) => $"{speciesName.Value.CapitaliseFirstInvariant()}DefaultRagdoll"; - public static string GetDefaultFile(Identifier speciesName, ContentPackage contentPackage = null) - => IO.Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName)}.xml"); - - public static string GetFolder(Identifier speciesName, ContentPackage contentPackage = null) + public static string GetDefaultFile(Identifier speciesName) => IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName)}.xml"); + + public static string GetFolder(Identifier speciesName) { - CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage)); + CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName); if (prefab?.ConfigElement == null) { - DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", contentPackage: contentPackage); + DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'"); return string.Empty; } return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value); @@ -199,10 +198,10 @@ private static string GetFolder(ContentXElement root, string filePath) } } } - else if (!variantOf.IsEmpty && CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab prefab) + else if (!variantOf.IsEmpty && CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab parentPrefab) { - // Ragdoll element not defined -> use the ragdoll defined in the base definition file. - ragdollSpecies = prefab.GetBaseCharacterSpeciesName(variantOf); + //get the params from the parent prefab if this one doesn't re-define them + return GetDefaultRagdollParams(variantOf, parentPrefab.ConfigElement, parentPrefab.ContentPackage); } // Using a null file definition means we use the default animations found in the Ragdolls folder. return GetRagdollParams(speciesName, ragdollSpecies, file: null, contentPackage); @@ -245,7 +244,7 @@ private static string GetFolder(ContentXElement root, string filePath) } else { - DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {ragdollInstance} from {contentPath.Value} for the character {speciesName}. Using the default ragdoll.", contentPackage: contentPackage); + DebugConsole.ThrowError($"[RagdollParams] Failed to load a ragdoll {ragdollInstance} from {contentPath.Value} for the character {speciesName}. Using the default ragdoll.", contentPackage: contentPackage); } } // Seek the default ragdoll from the character's ragdoll folder. @@ -294,8 +293,30 @@ private static string GetFolder(ContentXElement root, string filePath) } else { - // Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harder to debug. It's better to fail early. - throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}."); + string error = $"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}."; + if (contentPackage == GameMain.VanillaContent) + { + // Check if the base character content package is vanilla too. + CharacterPrefab characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); + if (characterPrefab?.ParentPrefab == null || characterPrefab.ParentPrefab.ContentPackage == GameMain.VanillaContent) + { + // If the error is in the vanilla content, it's just better to crash early. + // If dodging with the solution below fails, we'll also get here. + throw new Exception(error); + } + } + // Try to dodge crashing on modded content. + DebugConsole.ThrowError(error, contentPackage: contentPackage); + if (typeof(T) == typeof(HumanRagdollParams)) + { + Identifier fallbackSpecies = CharacterPrefab.HumanSpeciesName; + r = GetRagdollParams(fallbackSpecies, fallbackSpecies, file: ContentPath.FromRaw(contentPackage, "Content/Characters/Human/Ragdolls/HumanDefaultRagdoll.xml"), contentPackage: GameMain.VanillaContent); + } + else + { + Identifier fallbackSpecies = "crawler".ToIdentifier(); + r = GetRagdollParams(fallbackSpecies, fallbackSpecies, file: ContentPath.FromRaw(contentPackage, "Content/Characters/Crawler/Ragdolls/CrawlerDefaultRagdoll.xml"), contentPackage: GameMain.VanillaContent); + } } return r; } @@ -869,6 +890,9 @@ public override string Name [Serialize(true, IsPropertySaveable.Yes, description: "Can the limb enter submarines? Only valid if the ragdoll's CanEnterSubmarine is set to Partial, otherwise the limb can enter if the ragdoll can."), Editable] public bool CanEnterSubmarine { get; private set; } + [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "When set to something else than None, this limb will be hidden if the limb of the specified type is hidden."), Editable] + public LimbType InheritHiding { get; set; } + public LimbParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { var spriteElement = element.GetChildElement("sprite"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 9d2ab1c13b..38ba7b64e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -100,7 +100,7 @@ public static CharacterAbility Load(ContentXElement abilityElement, CharacterAbi string type = abilityElement.Name.ToString().ToLowerInvariant(); try { - abilityType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true); + abilityType = ReflectionUtils.GetTypeWithBackwardsCompatibility(ToolBox.BarotraumaAssembly, "Barotrauma.Abilities", type, false, true); if (abilityType == null) { if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index 5acdb2a85c..210a852f91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -19,6 +19,13 @@ class CharacterAbilityApplyStatusEffects : CharacterAbility private bool effectBeingApplied; + /// + /// Should the character who has the ability be marked as the "user" of the status effect? + /// Means that e.g. enemies will consider damage from the effect to be coming from the character with the ability, and that the character will gain skills if the effect e.g. heals someone. + /// + + private readonly bool setUser; + public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); @@ -27,6 +34,7 @@ public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbility nearbyCharactersAppliesToSelf = abilityElement.GetAttributeBool("nearbycharactersappliestoself", true); nearbyCharactersAppliesToAllies = abilityElement.GetAttributeBool("nearbycharactersappliestoallies", true); nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true); + setUser = abilityElement.GetAttributeBool("setuser", true); } protected void ApplyEffectSpecific(Character targetCharacter, Limb targetLimb = null) @@ -44,7 +52,7 @@ protected void ApplyEffectSpecific(Character targetCharacter, Limb targetLimb = if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget)) { // currently used to spawn items on the targeted character - statusEffect.SetUser(targetCharacter); + if (setUser) { statusEffect.SetUser(targetCharacter); } statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter); } else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) @@ -63,22 +71,22 @@ protected void ApplyEffectSpecific(Character targetCharacter, Limb targetLimb = { targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character)); } - statusEffect.SetUser(Character); + if (setUser) { statusEffect.SetUser(Character); } statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); } else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) && targetLimb != null) { - statusEffect.SetUser(Character); + if (setUser) { statusEffect.SetUser(Character); } statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetLimb); } else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { - statusEffect.SetUser(Character); + if (setUser) { statusEffect.SetUser(Character); } statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); } else { - statusEffect.SetUser(Character); + if (setUser) { statusEffect.SetUser(Character); } statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs index c519be61ab..70e3c0db96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable namespace Barotrauma.Abilities { @@ -19,10 +19,30 @@ public CharacterAbilityReduceAffliction(CharacterAbilityGroup characterAbilityGr } } + protected override void ApplyEffect() + { + ApplyEffectToCharacter(Character); + } + protected override void ApplyEffect(AbilityObject abilityObject) { - if (abilityObject is not IAbilityCharacter character) { return; } - character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount, attacker: Character); + if (abilityObject is IAbilityCharacter characterData) + { + ApplyEffectToCharacter(characterData.Character); + } + } + + private void ApplyEffectToCharacter(Character character) + { + character?.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount, attacker: Character); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index aa8b701206..f53ab10d0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -140,7 +140,7 @@ private AbilityCondition ConstructCondition(CharacterTalent characterTalent, Con string type = conditionElement.Name.ToString().ToLowerInvariant(); try { - conditionType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true); + conditionType = ReflectionUtils.GetTypeWithBackwardsCompatibility(ToolBox.BarotraumaAssembly, "Barotrauma.Abilities", type, false, true); if (conditionType == null) { if (errorMessages) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index 736421c363..1a38a89d20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -22,6 +22,11 @@ class TalentPrefab : PrefabWithUintIdentifier public readonly Sprite Icon; + /// + /// When set to true, this talent will not be visible in the "Extra Talents" panel if it is not part of the character's job talent tree. + /// + public readonly bool IsHiddenExtraTalent; + /// /// When set to a value the talent tooltip will display a text showing the current value of the stat and the max value. /// For example "Progress: 37/100". @@ -62,6 +67,8 @@ public TalentPrefab(ContentXElement element, TalentsFile file) : base(file, elem DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value); } + IsHiddenExtraTalent = element.GetAttributeBool("ishiddenextratalent", false); + Description = string.Empty; #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index e1442dc700..600c17ded5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -738,7 +738,14 @@ void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name] [limb type] [use relative strength]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) => { - if (args.Length < 2) { return; } + if (args.Length < 2) + { + if (args.Length == 1) + { + ThrowError("Must give a strength value!"); + } + return; + } string affliction = args[0]; AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == affliction); if (afflictionPrefab == null) @@ -779,9 +786,9 @@ void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab { return new string[][] { - AfflictionPrefab.Prefabs.Select(a => a.Name.Value).ToArray(), + AfflictionPrefab.Prefabs.Select(a => a.Name.Value).ToArray().Concat(AfflictionPrefab.Prefabs.Select(a => a.Identifier.Value)).ToArray(), new string[] { "1" }, - Character.CharacterList.Select(c => c.Name).ToArray(), + ListCharacterNames(), Enum.GetNames(typeof(LimbType)).ToArray() }; }, isCheat: true)); @@ -827,7 +834,9 @@ void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab if (character != null) { Dictionary treatments = new Dictionary(); - character.CharacterHealth.GetSuitableTreatments(treatments, user: null); + character.CharacterHealth.GetSuitableTreatments(treatments, user: null, + checkTreatmentThreshold: true, + checkTreatmentSuggestionThreshold: false); foreach (var treatment in treatments.OrderByDescending(t => t.Value)) { Color color = Color.White; @@ -2782,6 +2791,7 @@ private static void TeleportCharacter(Vector2 cursorWorldPos, Character controll if (targetCharacter != null) { targetCharacter.TeleportTo(worldPosition); + targetCharacter.AnimController.BodyInRest = false; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/PerkBase.cs b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/PerkBase.cs index 7e534b6655..280672b983 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/PerkBase.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/PerkBase.cs @@ -54,7 +54,7 @@ public bool CanApplyWithoutSubmarine() public static bool TryLoadFromXml(ContentXElement element, DisembarkPerkPrefab prefab, [NotNullWhen(true)] out PerkBase? perk) { - Type? type = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.PerkBehaviors", element.Name.ToString(), throwOnError: false, ignoreCase: true); + Type? type = ReflectionUtils.GetTypeWithBackwardsCompatibility(ToolBox.BarotraumaAssembly, "Barotrauma.PerkBehaviors", element.Name.ToString(), throwOnError: false, ignoreCase: true); if (type is null) { DebugConsole.ThrowError($"Could not find a perk behavior of the type \"{element.Name}\".", contentPackage: element.ContentPackage); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 62d10ff206..5efea9ec11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -311,6 +311,11 @@ public enum StatTypes /// RangedAttackSpeed, + /// + /// Increases the damage dealt by ranged weapons held by the character by a percentage. + /// + RangedAttackMultiplier, + /// /// Decreases the reload time of submarine turrets operated by the character by a percentage. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AddScoreAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AddScoreAction.cs new file mode 100644 index 0000000000..ba59e5421b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AddScoreAction.cs @@ -0,0 +1,84 @@ +namespace Barotrauma +{ + /// + /// Modifies the win score of a team in the PvP mode. + /// + class AddScoreAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes, description: "Tag of a target (character) whose team the score should be given to.")] + public Identifier TargetTag { get; set; } + + [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes, description: $"Which team's score to add to? Ignored if {nameof(TargetTag)} is set.")] + public CharacterTeamType Team { get; set; } + + [Serialize(1, IsPropertySaveable.Yes, description: "How much to add to the score? Can also be negative.")] + public int Amount { get; set; } + + public AddScoreAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (Amount == 0) + { + DebugConsole.ThrowError($"Error in {nameof(AddScoreAction)}, event {parentEvent.Prefab.Identifier}: score set to 0, the action will do nothing.", contentPackage: element.ContentPackage); + } + if (TargetTag.IsEmpty && Team == CharacterTeamType.None) + { + DebugConsole.ThrowError($"Error in {nameof(AddScoreAction)}, event {parentEvent.Prefab.Identifier}: neither {nameof(Team)} or {nameof(TargetTag)} is set.", contentPackage: element.ContentPackage); + } + } + + private bool isFinished = false; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + CharacterTeamType targetTeam = CharacterTeamType.None; + if (TargetTag.IsEmpty) + { + targetTeam = Team; + } + else + { + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + if (target is Character character) + { + targetTeam = character.TeamID; + break; + } + } + } + if (targetTeam == CharacterTeamType.None) { return; } + +#if SERVER + if (GameMain.GameSession?.Missions is { } missions) + { + foreach (var mission in missions) + { + if (mission is CombatMission combatMission) + { + combatMission.AddToScore(targetTeam, Amount); + } + } + } +#endif + isFinished = true; + } + + public override string ToDebugString() + { + string target = TargetTag.IsEmpty ? $"team: {Team.ColorizeObject()}" : $"target: {TargetTag}"; + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(AddScoreAction)} -> ({target}, amount: {Amount.ColorizeObject()})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs index 13a217c0d9..54fb8f59b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs @@ -50,7 +50,7 @@ public CheckVisibilityAction(ScriptedEvent parentEvent, ContentXElement element) { if (!AllowSameEntity && entity == target) { continue; } if (Vector2.DistanceSquared(target.WorldPosition, entity.WorldPosition) > MaxDistance * MaxDistance) { continue; } - if (Character.IsTargetVisible(target, entity, seeThroughWindows: true, CheckFacing)) + if (ISpatialEntity.IsTargetVisible(target, entity, seeThroughWindows: true, CheckFacing)) { if (!ApplyTagToEntity.IsEmpty) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 4e5d7f065f..8e765c3d1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -240,7 +240,7 @@ private void ResetSpeaker() { humanAI.ClearForcedOrder(); if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); } - if (prevGotoObjective != null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); } + if (prevGotoObjective != null && !prevGotoObjective.Abandon) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); } humanAI.ObjectiveManager.SortObjectives(); } } @@ -402,7 +402,7 @@ private void TryStartConversation(Character speaker, Character targetCharacter = if (!targets.Any() || IsBlockedByAnotherConversation(targets, BlockOtherConversationsDuration)) { return; } } - if (targetCharacter != null && IsBlockedByAnotherConversation(targetCharacter.ToEnumerable(), 0.1f)) { return; } + if (IsBlockedByAnotherConversation(targetCharacter?.ToEnumerable(), BlockOtherConversationsDuration)) { return; } if (speaker?.AIController is HumanAIController humanAI) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs index dd33756578..f072202a47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Barotrauma.Extensions; using Barotrauma.Items.Components; @@ -12,6 +12,11 @@ namespace Barotrauma /// class WaitForItemUsedAction : EventAction { + /// + /// Counter used to ensure we have a unique identifier to use for the ItemComponent.OnUsed event + /// + private static int IdCounter; + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item that must be used. Note that the item needs to have been tagged by the event - this does not refer to the tags that can be set per-item in the sub editor.")] public Identifier ItemTag { get; set; } @@ -50,7 +55,8 @@ private Identifier OnUseEventIdentifier { if (onUseEventIdentifier.IsEmpty) { - onUseEventIdentifier = (ParentEvent.Prefab.Identifier + ParentEvent.Actions.IndexOf(this).ToString()).ToIdentifier(); + onUseEventIdentifier = (ParentEvent.Prefab.Identifier + ParentEvent.Actions.IndexOf(this).ToString() + IdCounter).ToIdentifier(); + IdCounter++; } return onUseEventIdentifier; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 4f62d0ec0f..c0d219e392 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -1018,8 +1018,8 @@ private void CalculateCurrentIntensity(float deltaTime) } else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1)) { - if (character.Submarine != null && - character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } && + if (character.Submarine != null && Submarine.MainSub != null && + character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } && Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange) { //we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 81e579c804..b79d4e13f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -160,7 +160,9 @@ protected override void UpdateMissionSpecific(float deltaTime) #if DEBUG || UNSTABLE if (State == 1 && !level.CheckBeaconActive()) { - DebugConsole.ThrowError("Beacon became inactive!"); + DebugConsole.ThrowError( + "Debug/unstable only error message: beacon became inactive mid-mission after it had been activated! If this happened unexpectedly while you were away from the beacon, it may be a sign of a bug."+ + " If possible, please try to check what caused the beacon to go inactive."); State = 2; } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 1303cded9f..f62efa2eff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -211,7 +211,7 @@ private void DetermineCargo() if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } } - public override int GetBaseReward(Submarine sub) + public override float GetBaseReward(Submarine sub) { // If we are not at the location of the mission, skip the calculation of the reward if (GameMain.GameSession?.StartLocation != Locations[0]) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index c1548aa1da..2edb4b83b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -61,7 +61,7 @@ private void CalculateReward() if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } } - public override int GetBaseReward(Submarine sub) + public override float GetBaseReward(Submarine sub) { if (sub != missionSub) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 856157a0b6..5a88ec3888 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -244,25 +244,23 @@ public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requi /// /// Calculates the base reward, can be overridden for different mission types /// - public virtual int GetBaseReward(Submarine sub) + public virtual float GetBaseReward(Submarine sub) { return Prefab.Reward; } /// - /// Calculates the available reward, taking into account universal modifiers such as campaign settings + /// Calculates the available monetary reward, taking into account universal modifiers such as campaign settings. /// public int GetReward(Submarine sub) { - int reward = GetBaseReward(sub); - + float reward = GetBaseReward(sub); // Some modifiers should apply universally to all implementations of GetBaseReward if (GameMain.GameSession?.Campaign is CampaignMode campaign) { - reward = (int)Math.Round(reward * campaign.Settings.MissionRewardMultiplier); + reward *= campaign.Settings.MissionRewardMultiplier; } - - return reward; + return (int)Math.Round(reward); } public void Start(Level level) @@ -428,15 +426,23 @@ private void CalculateFinalReward(Submarine sub) finalReward = (int)(reward * missionMoneyGainMultiplier.Value); } + private float CalculateDifficultyXPMultiplier() + { + const float minMissionDifficulty = 1; + const float maxMissionDifficulty = 4; + const float maxXpBonus = 1.3f; + float selectedMissionDifficulty = MathUtils.InverseLerp(minMissionDifficulty, maxMissionDifficulty, Prefab.Difficulty.GetValueOrDefault()); + float xpBonusMultiplier = MathHelper.Lerp(1.0f, maxXpBonus, selectedMissionDifficulty); + + return xpBonusMultiplier; + } + private void GiveReward() { if (GameMain.GameSession.GameMode is not CampaignMode campaign) { return; } - int reward = GetReward(Submarine.MainSub); - - float baseExperienceGain = reward * 0.09f; - float difficultyMultiplier = 1 + level.Difficulty / 100f; - baseExperienceGain *= difficultyMultiplier; + float xpReward = GetBaseReward(Submarine.MainSub) * Prefab.ExperienceMultiplier * campaign.Settings.ExperienceRewardMultiplier; + float xpGain = xpReward * level.LevelData.Biome.ExperienceFromMissionRewards * CalculateDifficultyXPMultiplier(); IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); @@ -444,7 +450,7 @@ private void GiveReward() var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f, character: null); crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); - DistributeExperienceToCrew(crewCharacters, (int)(baseExperienceGain * experienceGainMultiplier.Value)); + DistributeExperienceToCrew(crewCharacters, (int)(xpGain * experienceGainMultiplier.Value)); CalculateFinalReward(Submarine.MainSub); #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index b5cb31820c..b692d0725d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -101,6 +101,8 @@ public ReputationReward(XElement element) public readonly int Reward; + public readonly float ExperienceMultiplier; + // The titles and bodies of the popup messages during the mission, shown when the state of the mission changes. The order matters. public readonly ImmutableArray Headers; public readonly ImmutableArray Messages; @@ -218,6 +220,7 @@ LocalizedString GetText(string textTag, string textTagPrefix) } Reward = element.GetAttributeInt("reward", 1); + ExperienceMultiplier = element.GetAttributeFloat("experiencemultiplier", 1.0f); AllowRetry = element.GetAttributeBool("allowretry", false); ShowInMenus = element.GetAttributeBool("showinmenus", true); ShowStartMessage = element.GetAttributeBool("showstartmessage", true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 37736d6086..a7fd4c3c7f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -70,7 +70,7 @@ partial class PirateMission : Mission } } - public override int GetBaseReward(Submarine sub) + public override float GetBaseReward(Submarine sub) { return alternateReward; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index 8ee06a07d1..288e37b922 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -1,3 +1,4 @@ +using System; using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.RuinGeneration; @@ -20,23 +21,14 @@ partial class ScanMission : Mission private readonly Dictionary scanTargets = new Dictionary(); private readonly HashSet newTargetsScanned = new HashSet(); private readonly float minTargetDistance; - - + private Ruin TargetRuin { get; set; } - private bool AllTargetsScanned - { - get - { - return scanTargets.Any() && scanTargets.All(kvp => kvp.Value); - } - } - public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { - if (State > 0 || scanTargets.None()) + if (AllTargetsScanned()) { return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } @@ -234,24 +226,19 @@ private static bool IsValidScanPosition(Scanner scanner, KeyValuePair kvp.Value)); } - - protected override bool DetermineCompleted() => State > 0; + + private bool AllTargetsScanned() => State >= targetsToScan; + + protected override bool DetermineCompleted() => AllTargetsScanned(); protected override void EndMissionSpecific(bool completed) { foreach (var scanner in scanners) { - if (scanner.Item != null && !scanner.Item.Removed) + if (scanner.Item is { Removed: false }) { scanner.OnScanStarted -= OnScanStarted; scanner.OnScanCompleted -= OnScanCompleted; @@ -259,7 +246,7 @@ protected override void EndMissionSpecific(bool completed) } } Reset(); - failed = !completed && state > 0; + failed = !completed; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index a517c23175..e4d8f4c580 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -510,6 +510,17 @@ private bool IsItemSellable(Item item, IEnumerable confirmedItems) } return false; } + //can't sell items in hidden inventories + Item rootContainer = item.Container; + while (rootContainer != null) + { + if (rootContainer.OwnInventory?.Container is { } containerComponent) + { + if (!containerComponent.DrawInventory) { return false; } + if (!containerComponent.IsAccessible()) { return false; } + } + rootContainer = rootContainer.Container; + } if (item.OwnInventory?.Container is ItemContainer itemContainer) { var containedItems = item.ContainedItems; @@ -672,7 +683,8 @@ public static void DeliverItemsToCharacter(IEnumerable itemsToSpa { foreach (Item containedItem in character.Inventory.AllItemsMod) { - if (containedItem.OwnInventory != null && + //only put into containers that draw the inventory (not ones with a hidden inventory like circuit boxes!) + if (containedItem.OwnInventory?.Container is { DrawInventory: true } && containedItem.OwnInventory.TryPutItem(item, user: null, item.AllowedSlots)) { break; @@ -703,14 +715,23 @@ private static void ItemSpawned(PurchasedItem purchased, Item item, CargoManager public static void ItemSpawned(Item item) { - Submarine sub = item.Submarine ?? item.RootContainer?.Submarine; - if (sub != null) + CharacterTeamType teamID = CharacterTeamType.Team1; + if (item.ParentInventory?.Owner is Character character) { - foreach (WifiComponent wifiComponent in item.GetComponents()) + teamID = character.TeamID; + } + else + { + Submarine sub = item.Submarine ?? item.RootContainer?.Submarine; + if (sub != null) { - wifiComponent.TeamID = sub.TeamID; + teamID = sub.TeamID; } } + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = teamID; + } } private readonly List<(PurchasedItem purchaseInfo, IdCard idCard)> purchasedIDCards = new List<(PurchasedItem purchaseInfo, IdCard idCard)>(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 418cfeae0a..e83b8bba29 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -81,6 +81,7 @@ public bool AddOrder(Order order, float? fadeOutTime) // Ignore orders work a bit differently since the "unignore" order counters the "ignore" order var isUnignoreOrder = order.Identifier == Tags.UnignoreThis; + var isIgnoreOrder = order.Identifier == Tags.IgnoreThis; var orderPrefab = !isUnignoreOrder ? order.Prefab : OrderPrefab.Prefabs[Tags.IgnoreThis]; ActiveOrder existingOrder = ActiveOrders.Find(o => o.Order.Prefab == orderPrefab && MatchesTarget(o.Order.TargetEntity, order.TargetEntity) && @@ -96,6 +97,14 @@ public bool AddOrder(Order order, float? fadeOutTime) else { ActiveOrders.Remove(existingOrder); + if (isIgnoreOrder && order.TargetEntity is Item targetItem) + { + foreach (var stackedItem in targetItem.GetStackedItems()) + { + ActiveOrders.RemoveAll(o => o.Order.Prefab == orderPrefab && o.Order.TargetEntity == stackedItem); + stackedItem.OrderedToBeIgnored = false; + } + } return true; } } @@ -124,7 +133,18 @@ public bool AddOrder(Order order, float? fadeOutTime) } } } - ActiveOrders.Add(new ActiveOrder(order, fadeOutTime)); + if (isIgnoreOrder && order.TargetEntity is Item targetItem) + { + foreach (var stackedItem in targetItem.GetStackedItems()) + { + ActiveOrders.Add(new ActiveOrder(order.WithTargetEntity(stackedItem), fadeOutTime)); + stackedItem.OrderedToBeIgnored = true; + } + } + else + { + ActiveOrders.Add(new ActiveOrder(order, fadeOutTime)); + } #if CLIENT HintManager.OnActiveOrderAdded(order); #endif @@ -554,7 +574,7 @@ public static Character GetCharacterForQuickAssignment(Order order, Character co public static IEnumerable GetCharactersSortedForOrder(Order order, IEnumerable characters, Character controlledCharacter, bool includeSelf, IEnumerable extraCharacters = null) { - var filteredCharacters = characters.Where(c => controlledCharacter == null || ((includeSelf || c != controlledCharacter) && c.TeamID == controlledCharacter.TeamID)); + var filteredCharacters = characters.Where(c => c.Info != null && (controlledCharacter == null || ((includeSelf || c != controlledCharacter) && c.TeamID == controlledCharacter.TeamID))); if (extraCharacters != null) { filteredCharacters = filteredCharacters.Union(extraCharacters); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 9cac8c186c..174e3cb702 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -18,6 +18,7 @@ public readonly record struct SaveInfo( string FilePath, Option SaveTime, string SubmarineName, + RespawnMode RespawnMode, ImmutableArray EnabledContentPackageNames) : INetSerializableStruct; public const int MaxMoney = int.MaxValue / 2; //about 1 billion @@ -33,6 +34,20 @@ public readonly record struct SaveInfo( public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Upgrade, PurchaseSub, MedicalClinic, Cargo } + /// + /// Should the interaction be disabled if the character's faction is hostile towards the players? + /// + public static bool HostileFactionDisablesInteraction(InteractionType interactionType) + { + return + interactionType != InteractionType.None && + //allow interacting with stores, otherwise you could get softlocked + //(no way to get enough resources from a hostile outpost to make it to the next one?) + interactionType != InteractionType.Store && + //examining is triggered by events, and there may be events that are intended to allow interaction with a hostile NPC. + interactionType != InteractionType.Examine; + } + public static bool BlocksInteraction(InteractionType interactionType) { return interactionType != InteractionType.None && interactionType != InteractionType.Cargo; @@ -1099,6 +1114,7 @@ public bool CanAffordNewCharacter(CharacterInfo characterInfo) private void NPCInteract(Character npc, Character interactor) { if (!npc.AllowCustomInteract) { return; } + if (npc.AIController is HumanAIController humanAi && !humanAi.AllowCampaignInteraction()) { return; } NPCInteractProjSpecific(npc, interactor); string coroutineName = "DoCharacterWait." + (npc?.ID ?? Entity.NullEntityID); if (!CoroutineManager.IsCoroutineRunning(coroutineName)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index ffafbc5426..a05c964022 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -140,13 +140,14 @@ public float LevelDifficultyMultiplier private static readonly Dictionary _multiplierSettings = new Dictionary { - { "default", new MultiplierSettings { Min = 0.2f, Max = 2.0f, Step = 0.1f } }, - { nameof(CrewVitalityMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } }, - { nameof(NonCrewVitalityMultiplier),new MultiplierSettings { Min = 0.5f, Max = 3.0f, Step = 0.1f } }, - { nameof(MissionRewardMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } }, - { nameof(RepairFailMultiplier), new MultiplierSettings { Min = 0.5f, Max = 5.0f, Step = 0.5f } }, - { nameof(ShopPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } }, - { nameof(ShipyardPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } } + { "default", new MultiplierSettings { Min = 0.2f, Max = 2.0f, Step = 0.1f } }, + { nameof(CrewVitalityMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } }, + { nameof(NonCrewVitalityMultiplier), new MultiplierSettings { Min = 0.5f, Max = 3.0f, Step = 0.1f } }, + { nameof(MissionRewardMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } }, + { nameof(ExperienceRewardMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } }, + { nameof(RepairFailMultiplier), new MultiplierSettings { Min = 0.5f, Max = 5.0f, Step = 0.5f } }, + { nameof(ShopPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } }, + { nameof(ShipyardPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } } // Add overrides for default values here }; @@ -165,6 +166,9 @@ public float LevelDifficultyMultiplier [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] public float MissionRewardMultiplier { get; set; } + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float ExperienceRewardMultiplier { get; set; } + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] public float ShopPriceMultiplier { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 86ea3991a9..00e294e68c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -482,6 +482,7 @@ public void StartRound(string levelSeed, float? difficulty = null, LevelGenerati { Random rand = new MTRandom(ToolBox.StringToInt(levelSeed)); LocationType locationType = LocationType.Prefabs + .OrderBy(lt => lt.UintIdentifier) .Where(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)) .GetRandom(rand)!; dummyLocations = CreateDummyLocations(levelSeed, locationType); @@ -1563,13 +1564,15 @@ public void Save(string filePath, bool isSavingOnLoading) if (kvp.Key.TryUnwrap(out AccountId? accountId)) { permadeathsElement.Add( - new XElement("account"), + new XElement("account", new XAttribute("id", accountId.StringRepresentation), - new XAttribute("permadeathcount", kvp.Value)); + new XAttribute("permadeathcount", kvp.Value))); } } rootElement.Add(permadeathsElement); + rootElement.Add(new XAttribute("respawnmode", GameMain.NetworkMember?.ServerSettings?.RespawnMode ?? RespawnMode.None)); + ((CampaignMode)GameMode).Save(doc.Root, isSavingOnLoading); doc.SaveSafe(filePath, throwExceptions: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index 6de40b6cea..2b8758fdd6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -1,4 +1,4 @@ -namespace Barotrauma +namespace Barotrauma { public enum InputType { @@ -7,7 +7,7 @@ public enum InputType Aim, Up, Down, Left, Right, Attack, - Run, Crouch, + Run, ToggleRun, Crouch, InfoTab, Chat, RadioChat, CrewOrders, Ragdoll, Health, Grab, DropItem, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 7ed39f1c4c..65b00c67c9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -21,7 +21,7 @@ partial class Door : Pickable, IDrawableComponent, IServerSerializable private Gap linkedGap; private bool isOpen; - private float openState; + private float openState, lastOpenState; private readonly Sprite doorSprite, weldedSprite, brokenSprite; private readonly bool scaleBrokenSprite, fadeBrokenSprite; private readonly bool autoOrientGap; @@ -218,6 +218,7 @@ public float OpenState get { return openState; } set { + lastOpenState = openState; openState = MathHelper.Clamp(value, 0.0f, 1.0f); #if CLIENT float size = IsHorizontal ? item.Rect.Width : item.Rect.Height; @@ -329,13 +330,24 @@ public override void Move(Vector2 amount, bool ignoreContacts = false) private readonly LocalizedString cannotOpenText = TextManager.Get("DoorMsgCannotOpen"); public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { - Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar"; + if (IsBroken) + { + return false; + } + if (isOpen) + { + Msg = HasAccess(character) ? "ItemMsgClose" : "ItemMsgForceCloseCrowbar"; + } + else + { + Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar"; + } ParseMsg(); if (addMessage) { - msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value; + msg ??= (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value; } - return isBroken || base.HasRequiredItems(character, addMessage, msg); + return base.HasRequiredItems(character, addMessage, msg); } public override bool Pick(Character picker) @@ -461,12 +473,12 @@ public override void Update(float deltaTime, Camera cam) if (PredictedState == null) { OpenState += deltaTime * (isOpen ? OpeningSpeed : -ClosingSpeed); - isClosing = openState > 0.0f && openState < 1.0f && !isOpen; + isClosing = openState is > 0.0f and < 1.0f && !isOpen; } else { - OpenState += deltaTime * ((bool)PredictedState ? OpeningSpeed : -ClosingSpeed); - isClosing = openState > 0.0f && openState < 1.0f && !(bool)PredictedState; + OpenState += deltaTime * (PredictedState.Value ? OpeningSpeed : -ClosingSpeed); + isClosing = openState is > 0.0f and < 1.0f && !PredictedState.Value; resetPredictionTimer -= deltaTime; if (resetPredictionTimer <= 0.0f) @@ -479,7 +491,11 @@ public override void Update(float deltaTime, Camera cam) if (isClosing) { - if (OpenState < 0.9f) { PushCharactersAway(); } + //server gives the clients more leeway on moving through closing doors + //latency can often otherwise make a client get blocked by a closing door server-side even if it seemed like they made it through client-side + float pushCharactersAwayThreshold = GameMain.NetworkMember is { IsServer: true } ? 0.1f : 0.9f; + + if (OpenState < pushCharactersAwayThreshold) { PushCharactersAway(); } if (CheckSubmarinesInDoorWay()) { PredictedState = null; @@ -771,11 +787,11 @@ private bool PushBodyOutOfDoorway(Character c, PhysicsBody body, int dir, Vector { if (IsHorizontal) { - body.SetTransform(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation); + body.SetTransformIgnoreContacts(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation); } else { - body.SetTransform(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation); + body.SetTransformIgnoreContacts(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 2d1d5228d3..5c77cca7f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -14,13 +14,15 @@ namespace Barotrauma.Items.Components { partial class Holdable : Pickable, IServerSerializable, IClientSerializable { - private readonly struct EventData : IEventData + private readonly struct AttachEventData : IEventData { public readonly Vector2 AttachPos; - - public EventData(Vector2 attachPos) + public readonly Character Attacher; + + public AttachEventData(Vector2 attachPos, Character attacher) { AttachPos = attachPos; + Attacher = attacher; } } @@ -225,6 +227,44 @@ public float SpriteDepthWhenDropped set; } + /// + /// For setting the handle positions using status effects + /// + public Vector2 Handle1 + { + get { return ConvertUnits.ToDisplayUnits(handlePos[0]); } + set + { + handlePos[0] = ConvertUnits.ToSimUnits(value); + if (item.FlippedX) + { + handlePos[0].X = -handlePos[0].X; + } + if (!secondHandlePosDefined) + { + Handle2 = value; + } + } + } + + /// + /// For setting the handle positions using status effects + /// + public Vector2 Handle2 + { + get { return ConvertUnits.ToDisplayUnits(handlePos[1]); } + set + { + handlePos[1] = ConvertUnits.ToSimUnits(value); + if (item.FlippedX) + { + handlePos[1].X = -handlePos[1].X; + } + } + } + + private bool secondHandlePosDefined; + public Holdable(Item item, ContentXElement element) : base(item, element) { @@ -254,9 +294,14 @@ public Holdable(Item item, ContentXElement element) { int index = i - 1; string attributeName = "handle" + i; - var attribute = element.GetAttribute(attributeName); // If no value is defind for handle2, use the value of handle1. - var value = attribute != null ? ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)) : previousValue; + Vector2 value = previousValue; + var attribute = element.GetAttribute(attributeName); + if (attribute != null) + { + secondHandlePosDefined = i > 1; + value = ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)); + } handlePos[index] = value; previousValue = value; } @@ -755,21 +800,14 @@ public override bool Use(float deltaTime, Character character = null) if (GameMain.NetworkMember != null) { - if (character != Character.Controlled) - { - return false; - } - else if (GameMain.NetworkMember.IsServer) - { - return false; - } - else - { #if CLIENT + if (character == Character.Controlled) + { Vector2 attachPos = ConvertUnits.ToSimUnits(GetAttachPosition(character)); - item.CreateClientEvent(this, new EventData(attachPos)); -#endif + item.CreateClientEvent(this, new AttachEventData(attachPos, character)); } +#endif + //don't attach at this point in MP: instead rely on the network events created above return false; } else @@ -824,9 +862,13 @@ private Vector2 GetAttachPosition(Character user, bool useWorldCoordinates = fal if (user.Submarine != null) { + //we must add some "padding" to the raycast to ensure it reaches all the way to a wall + //otherwise the cursor might be outside a wall, but the grid cell it's in might be partially inside + Vector2 padding = Submarine.GridSize * new Vector2(Math.Sign(mouseDiff.X), Math.Sign(mouseDiff.Y)); + if (Submarine.PickBody( ConvertUnits.ToSimUnits(user.Position), - ConvertUnits.ToSimUnits(user.Position + mouseDiff), collisionCategory: Physics.CollisionWall) != null) + ConvertUnits.ToSimUnits(user.Position + mouseDiff + padding), collisionCategory: Physics.CollisionWall) != null) { attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index edcfef1846..791148997c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -420,7 +420,7 @@ private void HandleImpact(Fixture targetFixture) 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 ?? targetFixture.UserData as Item; + Item targetItem = target.UserData is Holdable h ? h.Item : target.UserData as Item ?? targetFixture.UserData as Item; Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity; if (Attack != null) { @@ -460,10 +460,9 @@ private void HandleImpact(Fixture targetFixture) } #endif } - else if (target.UserData is Holdable holdable && holdable.CanPush) + else if (target.UserData is Holdable { CanPush: true } holdable) { if (holdable.Item.Removed) { return; } - Attack.DoDamage(user, holdable.Item, item.WorldPosition, 1.0f); RestoreCollision(); hitting = false; User = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 07299eb0eb..b6a70a3f34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -202,12 +202,26 @@ private IEnumerable WaitForPick(Character picker, float require #if CLIENT if (requiredTime < float.MaxValue && picker == Character.Controlled) { + string text = string.Empty; + if (!string.IsNullOrWhiteSpace(PickingMsg)) + { + text = PickingMsg; + } + else if (this is Door door) + { + text = door.IsClosed ? "progressbar.opening" : "progressbar.closing"; + } + else + { + text = "progressbar.deattaching"; + } + Character.Controlled?.UpdateHUDProgressBar( this, item.WorldPosition, pickTimer / requiredTime, GUIStyle.Red, GUIStyle.Green, - !string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching"); + text); } #endif picker.AnimController.UpdateUseItem(!picker.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 9db5152b48..4c2936d4cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -296,7 +296,9 @@ public override bool Use(float deltaTime, Character character = null) //which doesn't support multiple attached ropes (see Holdable.GetRope and the references to it) lastProjectile?.Item.GetComponent()?.Snap(); } - float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier; + + float rangedAttackMultiplier = character?.GetStatValue(StatTypes.RangedAttackMultiplier) ?? 0; + float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier) + rangedAttackMultiplier) * WeaponDamageModifier; projectile.Launcher = item; ignoredBodies.Clear(); @@ -306,6 +308,9 @@ public override bool Use(float deltaTime, Character character = null) { if (l.IsSevered) { continue; } ignoredBodies.Add(l.body.FarseerBody); +#if SERVER + ignoredBodies.Add(l.LagCompensatedBody.FarseerBody); +#endif } foreach (Item heldItem in character.HeldItems) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 6a7c65d83d..1825bf9666 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -320,7 +320,7 @@ public override bool Use(float deltaTime, Character character = null) private readonly List fireSourcesInRange = new List(); private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List ignoredBodies) { - var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall; + var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall | Physics.CollisionItemBlocking; if (!IgnoreCharacters) { collisionCategories |= Physics.CollisionCharacter; @@ -654,8 +654,9 @@ private bool FixBody(Character user, Vector2 hitPosition, float deltaTime, float FixCharacterProjSpecific(user, deltaTime, targetLimb.character); return true; } - else if (targetBody.UserData is Item targetItem) + else if (targetBody.UserData is Barotrauma.Item or Holdable) { + Item targetItem = targetBody.UserData is Holdable holdable ? holdable.Item : (Item)targetBody.UserData; if (!HitItems || !targetItem.IsInteractable(user)) { return false; } var levelResource = targetItem.GetComponent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index d39bb8a13a..d4b27fa439 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -75,6 +75,9 @@ public ItemComponent Parent public readonly ContentXElement originalElement; + /// + /// The default delay for delayed client-side corrections (see . + /// protected const float CorrectionDelay = 1.0f; protected CoroutineHandle delayedCorrectionCoroutine; @@ -667,8 +670,7 @@ protected virtual void RemoveComponentSpecific() #endif } - protected string GetTextureDirectory(ContentXElement subElement) - => subElement.DoesAttributeReferenceFileNameAlone("texture") ? Path.GetDirectoryName(item.Prefab.FilePath) : string.Empty; + protected string GetTextureDirectory(ContentXElement subElement) => item.Prefab.GetTexturePath(subElement, item.Prefab.ParentPrefab); public bool HasRequiredSkills(Character character) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index f8860ee1bc..2d696dae17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -108,7 +108,7 @@ public bool HideItems [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 inventory of this item be visible when the item is selected.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected. Note that this does not prevent dragging and dropping items to the item.")] public bool DrawInventory { get; @@ -923,6 +923,8 @@ public override void ReceiveSignal(Signal signal, Connection connection) #warning There's some code duplication here and in DrawContainedItems() method, but it's not straightforward to get rid of it, because of slightly different logic and the usage of draw positions vs. positions etc. Should probably be splitted into smaller methods. public void SetContainedItemPositions() { + if (containedItems.Count == 0) { return; } + var rootBody = item.RootContainer?.body ?? item.body; Vector2 transformedItemPos = GetContainedPosition( @@ -989,8 +991,7 @@ public void SetContainedItemPositions() rotation += -item.RotationRad; } contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); - contained.Item.body.SetPrevTransform(contained.Item.body.SimPosition, contained.Item.body.Rotation); - contained.Item.body.UpdateDrawPosition(); + contained.Item.body.UpdateDrawPosition(interpolate: false); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 267a730a41..a41b9e10c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -220,6 +220,10 @@ public Controller(Item item, ContentXElement element) /// private bool forceSelectNextFrame; + private float userCanInteractCheckTimer; + + private const float UserCanInteractCheckInterval = 1.0f; + public override void Update(float deltaTime, Camera cam) { this.cam = cam; @@ -238,13 +242,15 @@ public override void Update(float deltaTime, Camera cam) } forceSelectNextFrame = false; + userCanInteractCheckTimer -= deltaTime; + if (user == null || user.Removed || !user.IsAnySelectedItem(item) || (item.ParentInventory != null && !IsAttachedUser(user)) - || !user.CanInteractWith(item) || (UsableIn == UseEnvironment.Water && !user.AnimController.InWater) - || (UsableIn == UseEnvironment.Air && user.AnimController.InWater)) + || (UsableIn == UseEnvironment.Air && user.AnimController.InWater) + || !CheckUserCanInteract()) { if (user != null) { @@ -368,6 +374,22 @@ public override void Update(float deltaTime, Camera cam) } } + private bool CheckUserCanInteract() + { + //optimization: CanInteractWith is relatively heavy (can involve visibility checks for example), let's not do it every frame + if (user != null) + { + if (userCanInteractCheckTimer <= 0.0f) + { + userCanInteractCheckTimer = UserCanInteractCheckInterval; + return user.CanInteractWith(item); + } + } + //we only do the actual check every UserCanInteractCheckInterval seconds + //can mean the component can stay selected for <1s after the user no longer has access to it + return true; + } + private double lastUsed; public override bool Use(float deltaTime, Character activator = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 0b310a0da4..67a1cff734 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -499,12 +499,13 @@ private void Fabricate() { GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); } + InvSlotType invSlot = fabricatedItem.MoveToSlot; if (i < amountFittingContainer) { Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, onSpawned: (Item spawnedItem) => { - onItemSpawned(spawnedItem, tempUser); + onItemSpawned(spawnedItem, tempUser, invSlot); spawnedItem.Quality = quality; spawnedItem.StolenDuringRound = ingredientsStolen; spawnedItem.AllowStealing = ingredientsAllowStealing; @@ -517,7 +518,7 @@ private void Fabricate() Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality, onSpawned: (Item spawnedItem) => { - onItemSpawned(spawnedItem, tempUser); + onItemSpawned(spawnedItem, tempUser, invSlot); spawnedItem.Quality = quality; spawnedItem.StolenDuringRound = ingredientsStolen; spawnedItem.AllowStealing = ingredientsAllowStealing; @@ -527,15 +528,28 @@ private void Fabricate() } } - void onItemSpawned(Item spawnedItem, Character user) + void onItemSpawned(Item spawnedItem, Character user, InvSlotType slot) { - if (user != null && user.TeamID != CharacterTeamType.None) + CharacterTeamType teamID = CharacterTeamType.None; + if (user != null) + { + teamID = user.TeamID; + } + else if (item.Submarine != null) + { + teamID = item.Submarine.TeamID; + } + if (teamID != CharacterTeamType.None) { foreach (WifiComponent wifiComponent in spawnedItem.GetComponents()) { - wifiComponent.TeamID = user.TeamID; + wifiComponent.TeamID = teamID; } } + if (slot != InvSlotType.None) + { + user?.Inventory.TryPutItem(spawnedItem, user, slot.ToEnumerable()); + } OnItemFabricated?.Invoke(spawnedItem, user); } if (user?.Info != null && !user.Removed) @@ -562,7 +576,6 @@ void onItemSpawned(Item spawnedItem, Character user) StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false); } } - } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index ed97faf0ea..fd50424b01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -80,6 +80,7 @@ public Character LastUser private set { if (lastUser == value) { return; } + if (Screen.Selected.IsEditor) { return; } lastUser = value; if (lastUser == null) { @@ -246,6 +247,13 @@ public override void Update(float deltaTime, Camera cam) } } + //rapidly adjust the reactor in the first few seconds of the round to prevent overvoltages if the load changed between rounds + //(unless the reactor is being operated by a player) + if (GameMain.GameSession is { RoundDuration: <5 } && lastUser is not { IsPlayer: true }) + { + UpdateAutoTemp(100.0f, (float)(Timing.Step * 10.0f)); + } + #if CLIENT if (PowerOn && AvailableFuel < 1) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 72c8eb339c..bfd4aaafe1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -8,6 +8,8 @@ namespace Barotrauma.Items.Components { partial class Sonar : Powered, IServerSerializable, IClientSerializable { + public static List SonarList = new List(); + public enum Mode { Active, @@ -167,6 +169,7 @@ public Sonar(Item item, ContentXElement element) IsActive = true; InitProjSpecific(element); CurrentMode = Mode.Passive; + SonarList.Add(this); } partial void InitProjSpecific(ContentXElement element); @@ -379,6 +382,29 @@ public override void ReceiveSignal(Signal signal, Connection connection) } } + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); +#if CLIENT + sonarBlip?.Remove(); + pingCircle?.Remove(); + directionalPingCircle?.Remove(); + screenOverlay?.Remove(); + screenBackground?.Remove(); + lineSprite?.Remove(); + + foreach (var t in targetIcons.Values) + { + t.Item1.Remove(); + } + targetIcons.Clear(); + + MineralClusters = null; +#endif + SonarList.Remove(this); + } + + public void ServerEventRead(IReadMessage msg, Client c) { bool isActive = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 63e6404229..8473a50de0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -232,7 +232,7 @@ public override void Update(float deltaTime, Camera cam) float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f); - Overload = Voltage > maxOverVoltage; + Overload = Voltage > maxOverVoltage && GameMain.GameSession is not { RoundDuration: < 5 }; if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 1b45e89f8a..13db3297ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -157,6 +157,13 @@ public float Voltage { if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; } } + + if (this is PowerTransfer && item.Condition <= 0.0f) + { + //if the junction box or other power transfer device is broken, + //it cannot be supplying any power (voltage = 0) + return 0.0f; + } return PowerConsumption <= 0.0f ? 1.0f : voltage; } set diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 97b890bd1f..203213d19b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -440,6 +440,14 @@ public bool Use(Character character = null, float launchImpulseModifier = 0f) //can't launch if already launched if (StickTarget != null || IsActive) { return false; } +#if SERVER + var owner = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == User); + if (owner != null) + { + Limb.SetLagCompensatedBodyPositions(owner); + } +#endif + float initialRotation = item.body.Rotation; //if the item is being launched from an inventory, assume it's being fired by a gun that handles setting the rotation correctly //but if the item is e.g. being thrown by a character, we need to take the direction into account @@ -461,10 +469,10 @@ public bool Use(Character character = null, float launchImpulseModifier = 0f) spreadIndex++; Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle)); + Vector2 prevSimpos = item.SimPosition; + item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle); if (Hitscan) { - Vector2 prevSimpos = item.SimPosition; - item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle); DoHitscan(launchDir); if (i < HitScanCount - 1) { @@ -473,7 +481,6 @@ public bool Use(Character character = null, float launchImpulseModifier = 0f) } else { - item.body.SetTransform(item.body.SimPosition, launchAngle); float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread)); DoLaunch(launchDir * modifiedLaunchImpulse); } @@ -670,8 +677,6 @@ private List DoRayCast(Vector2 rayStart, Vector2 rayEnd, Submarin } if (fixture.Body.UserData is VineTile) { return true; } if (fixture.CollidesWith == Category.None) { return true; } - //only collides with characters = probably an "outsideCollisionBlocker" created by a gap - if (fixture.CollidesWith == Physics.CollisionCharacter) { return true; } if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; } @@ -690,6 +695,11 @@ private List DoRayCast(Vector2 rayStart, Vector2 rayEnd, Submarin if (item.Condition <= 0) { return true; } if (!item.Prefab.DamagedByProjectiles && item.GetComponent() == null) { return true; } } + else if (fixture.Body.UserData is Gap) + { + //an "outsideCollisionBlocker" created by a gap, should never collide + return true; + } else if (fixture.Body.UserData is Holdable { CanPush: false }) { // Ignore holdables that can't push -> shouldn't block @@ -724,14 +734,17 @@ private List DoRayCast(Vector2 rayStart, Vector2 rayEnd, Submarin return -1; } if (fixture.Body.UserData is VineTile) { return -1; } - if (fixture.CollidesWith == Category.None) { return -1; } - //only collides with characters = probably an "outsideCollisionBlocker" created by a gap - if (fixture.CollidesWith == Physics.CollisionCharacter) { return -1; } + if (fixture.CollidesWith == Category.None && fixture.CollisionCategories != Physics.CollisionLagCompensationBody) { return -1; } if (fixture.Body.UserData is Item item) { if (item.Condition <= 0) { return -1; } if (!item.Prefab.DamagedByProjectiles && item.GetComponent() == null) { return -1; } } + else if (fixture.Body.UserData is Gap) + { + //an "outsideCollisionBlocker" created by a gap, should never collide + return -1; + } if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; } //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub @@ -779,7 +792,7 @@ private List DoRayCast(Vector2 rayStart, Vector2 rayEnd, Submarin hits.Add(new HitscanResult(fixture, point, normal, fraction)); return 1; - }, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile); + }, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile | Physics.CollisionLagCompensationBody); return hits; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index e4d654db50..8985556c91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -15,6 +15,9 @@ partial class MotionSensor : ItemComponent private Vector2 detectOffset; private float updateTimer; + + [Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] + public bool MotionDetected { get; set; } [Flags] public enum TargetType @@ -26,14 +29,25 @@ public enum TargetType Any = Human | Monster | Wall | Pet, } - [Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] - public bool MotionDetected { get; set; } - + private bool triggerFromHumans = true; + private bool triggerFromPets = true; + private bool triggerFromMonsters = true; + private TargetType _target; + [InGameEditable, Serialize(TargetType.Any, IsPropertySaveable.Yes, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)] public TargetType Target { - get; - set; + get => _target; + set + { + if (_target != value) + { + _target = value; + triggerFromHumans = Target.HasFlag(TargetType.Human); + triggerFromPets = Target.HasFlag(TargetType.Pet); + triggerFromMonsters = Target.HasFlag(TargetType.Monster); + } + } } [Editable, Serialize("", IsPropertySaveable.Yes, description: "Does the sensor react only to certain characters (species names, groups or tags)? Doesn't have an effect, if the Target Type is incorrect.", alwaysUseInstanceValues: true)] @@ -263,10 +277,7 @@ public override void Update(float deltaTime, Camera cam) } } } - - bool triggerFromHumans = Target.HasFlag(TargetType.Human); - bool triggerFromPets = Target.HasFlag(TargetType.Pet); - bool triggerFromMonsters = Target.HasFlag(TargetType.Monster); + bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters; if (!hasTriggers) { return; } foreach (Character character in Character.CharacterList) @@ -299,9 +310,6 @@ public override void Update(float deltaTime, Camera cam) public bool TriggersOn(Character character) { - bool triggerFromHumans = Target.HasFlag(TargetType.Human); - bool triggerFromPets = Target.HasFlag(TargetType.Pet); - bool triggerFromMonsters = Target.HasFlag(TargetType.Monster); bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters; if (!hasTriggers) { return false; } return TriggersOn(character, triggerFromHumans, triggerFromPets, triggerFromMonsters); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index c86f30ef20..ea7c81e904 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -80,7 +80,6 @@ public bool IsOn set { isOn = value; - CanTransfer = value; if (!isOn) { currPowerConsumption = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 4c9d0347df..edf388da8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -96,8 +96,20 @@ public Color TextColor [Editable, Serialize("> ", IsPropertySaveable.Yes)] public string LineStartSymbol { get; set; } - [Editable, Serialize(false, IsPropertySaveable.No)] - public bool Readonly { get; set; } + private bool _readonly; + + [Editable, Serialize(false, IsPropertySaveable.Yes)] + public bool Readonly + { + get => _readonly; + set + { + _readonly = value; +#if CLIENT + RefreshInputElements(); +#endif + } + } [Serialize(true, IsPropertySaveable.No)] public bool AutoScrollToBottom { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index bd88cc3bd3..55c232587c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -229,14 +229,20 @@ public void SetChannelMemory(int index, int value) public void TransmitSignal(Signal signal, bool sentFromChat) { + bool chatMsgSent = false; + + var receivers = GetReceiversInRange(); if (sentFromChat) { + //if sent from chat, we need to reset the "signal chain" at this point + //so we can correctly detect which components the signal has already passed through to avoid infinite loops + //only relevant for signals originating from the chat - normally this is handled in Item.SendSignal item.LastSentSignalRecipients.Clear(); + foreach (WifiComponent receiver in receivers) + { + receiver.item.LastSentSignalRecipients.Clear(); + } } - - bool chatMsgSent = false; - - var receivers = GetReceiversInRange(); foreach (WifiComponent wifiComp in receivers) { if (sentFromChat && !wifiComp.LinkToChat) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 866679a714..a7bafa5225 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -1051,7 +1051,7 @@ public void UpdateAutoOperate(float deltaTime, bool ignorePower, Identifier frie } if (TargetItems) { - foreach (Item targetItem in Item.ItemList) + foreach (Item targetItem in Item.TurretTargetItems) { if (!IsValidTarget(targetItem)) { continue; } float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority; @@ -1395,7 +1395,7 @@ void CheckRemainingAmmo() closestDistance = dist / priority; currentTarget = closestEnemy; } - foreach (Item targetItem in Item.ItemList) + foreach (Item targetItem in Item.TurretTargetItems) { if (!IsValidTarget(targetItem)) { continue; } float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority; @@ -1767,8 +1767,15 @@ private bool CanShoot(Body targetBody, Character user = null, Identifier friendl Submarine sub = e.Submarine ?? e as Submarine; if (sub == null) { return true; } if (sub == Item.Submarine) { return false; } - if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; } - if (sub.TeamID == Item.Submarine.TeamID) { return false; } + if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon || sub.Info.IsRuin) { return false; } + if (item.Submarine == null) + { + if (sub.TeamID == FriendlyTeam) { return false; } + } + else + { + if (sub.TeamID == Item.Submarine.TeamID) { return false; } + } } else if (targetBody.UserData is not Voronoi2.VoronoiCell { IsDestructible: true }) { @@ -1786,6 +1793,8 @@ private Body CheckLineOfSight(Vector2 start, Vector2 end) customPredicate: (Fixture f) => { if (f.UserData is Item i && i.GetComponent() != null) { return false; } + if (f.CollidesWith == Physics.CollisionNone) { return false; } + if (f.Body.UserData == item) { return false; } if (f.UserData is Hull) { return false; } return !item.StaticFixtures.Contains(f); }); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 6b0ec41f29..ded04cd5ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -165,10 +165,11 @@ private ContentPath ParseSpritePath(ContentXElement element) { if (element.DoesAttributeReferenceFileNameAlone("texture")) { + var basePrefab = WearableComponent.Item.Prefab.ParentPrefab ?? WearableComponent.Item.Prefab; string textureName = element.GetAttributeString("texture", ""); return ContentPath.FromRaw( element.ContentPackage, - $"{Path.GetDirectoryName(WearableComponent.Item.Prefab.FilePath)}/{textureName}"); + $"{Path.GetDirectoryName(basePrefab.FilePath)}/{textureName}"); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 35d3248505..ce4da3bda2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -331,13 +331,16 @@ public Inventory(Entity owner, int capacity, int slotsPerRow = 5) } #endif } - + public IEnumerable GetAllItems(bool checkForDuplicates) { for (int i = 0; i < capacity; i++) { - foreach (var item in slots[i].Items) + var items = slots[i].Items; + // ReSharper disable once ForCanBeConvertedToForeach, because this is performance-sensitive code. + for (int j = 0; j < items.Count; j++) { + var item = items[j]; if (item == null) { #if DEBUG @@ -349,9 +352,9 @@ public IEnumerable GetAllItems(bool checkForDuplicates) if (checkForDuplicates) { bool duplicateFound = false; - for (int j = 0; j < i; j++) + for (int s = 0; s < i; s++) { - if (slots[j].Items.Contains(item)) + if (slots[s].Items.Contains(item)) { duplicateFound = true; break; @@ -364,7 +367,7 @@ public IEnumerable GetAllItems(bool checkForDuplicates) yield return item; } } - } + } } private void NotifyItemComponentsOfChange() @@ -420,12 +423,17 @@ public Item LastOrDefault() return null; } + private bool IsIndexInRange(int index) + { + return index >= 0 && index < slots.Length; + } + /// /// Get the item stored in the specified inventory slot. If the slot contains a stack of items, returns the first item in the stack. /// public Item GetItemAt(int index) { - if (index < 0 || index >= slots.Length) { return null; } + if (!IsIndexInRange(index)) { return null; } return slots[index].FirstOrDefault(); } @@ -434,14 +442,13 @@ public Item GetItemAt(int index) /// public IEnumerable GetItemsAt(int index) { - if (index < 0 || index >= slots.Length) { return Enumerable.Empty(); } + if (!IsIndexInRange(index)) { return Enumerable.Empty(); } return slots[index].Items; } public int GetItemStackSlotIndex(Item item, int index) { - if (index < 0 || index >= slots.Length) { return -1; } - + if (!IsIndexInRange(index)) { return -1; } return slots[index].Items.IndexOf(item); } @@ -476,7 +483,7 @@ public List FindIndices(Item item) public virtual bool ItemOwnsSelf(Item item) { if (Owner == null) { return false; } - if (!(Owner is Item)) { return false; } + if (Owner is not Item) { return false; } Item ownerItem = Owner as Item; if (ownerItem == item) { return true; } if (ownerItem.ParentInventory == null) { return false; } @@ -519,7 +526,7 @@ public bool CanBePut(Item item) public virtual bool CanBePutInSlot(Item item, int i, bool ignoreCondition = false) { if (ItemOwnsSelf(item)) { return false; } - if (i < 0 || i >= slots.Length) { return false; } + if (!IsIndexInRange(i)) { return false; } return slots[i].CanBePut(item, ignoreCondition); } @@ -539,7 +546,7 @@ public bool CanProbablyBePut(ItemPrefab itemPrefab, float? condition = null, int public virtual bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition = null, int? quality = null) { - if (i < 0 || i >= slots.Length) { return false; } + if (!IsIndexInRange(i)) { return false; } return slots[i].CanProbablyBePut(itemPrefab, condition, quality); } @@ -555,7 +562,7 @@ public int HowManyCanBePut(ItemPrefab itemPrefab, float? condition = null) public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition, bool ignoreItemsInSlot = false) { - if (i < 0 || i >= slots.Length) { return 0; } + if (!IsIndexInRange(i)) { return 0; } return slots[i].HowManyCanBePut(itemPrefab, condition: condition, ignoreItemsInSlot: ignoreItemsInSlot); } @@ -573,7 +580,7 @@ public virtual bool TryPutItem(Item item, Character user, IEnumerable= slots.Length) + if (!IsIndexInRange(i)) { string thisItemStr = item?.Prefab.Identifier.Value ?? "null"; string ownerStr = "null"; @@ -637,7 +644,7 @@ public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowC protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true) { - if (i < 0 || i >= slots.Length) + if (!IsIndexInRange(i)) { string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); @@ -1087,10 +1094,16 @@ public void ForceRemoveFromSlot(Item item, int index) public bool IsInSlot(Item item, int index) { - if (index < 0 || index >= slots.Length) { return false; } + if (!IsIndexInRange(index)) { return false; } return slots[index].Contains(item); } + public bool IsSlotEmpty(int index) + { + if (!IsIndexInRange(index)) { return false; } + return slots[index].Empty(); + } + public void SharedRead(IReadMessage msg, List[] receivedItemIds, out bool readyToApply) { byte start = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index caac0ea37a..5aec5e3cfb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -23,42 +23,66 @@ namespace Barotrauma { partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable { + #region Lists + + /// + /// A list of every item that exists somewhere in the world. Note that there can be a huge number of items in the list, + /// and you probably shouldn't be enumerating it to find some that match some specific criteria (unless that's done very, very sparsely or during initialization). + /// public static readonly List ItemList = new List(); - private static readonly HashSet dangerousItems = new HashSet(); + private static readonly HashSet _dangerousItems = new HashSet(); - public static IReadOnlyCollection DangerousItems { get { return dangerousItems; } } + public static IReadOnlyCollection DangerousItems => _dangerousItems; - private static readonly List repairableItems = new List(); + private static readonly List _repairableItems = new List(); /// /// Items that have one more more Repairable component /// - public static IReadOnlyCollection RepairableItems => repairableItems; + public static IReadOnlyCollection RepairableItems => _repairableItems; - private static readonly List cleanableItems = new List(); + private static readonly List _cleanableItems = new List(); /// /// Items that may potentially need to be cleaned up (pickable, not attached to a wall, and not inside a valid container) /// - public static IReadOnlyCollection CleanableItems => cleanableItems; + public static IReadOnlyCollection CleanableItems => _cleanableItems; - private static readonly HashSet deconstructItems = new HashSet(); + private static readonly HashSet _deconstructItems = new HashSet(); /// /// Items that have been marked for deconstruction /// - public static HashSet DeconstructItems => deconstructItems; + public static HashSet DeconstructItems => _deconstructItems; - private static readonly List sonarVisibleItems = new List(); + private static readonly List _sonarVisibleItems = new List(); /// /// Items whose is larger than 0 /// - public static IReadOnlyCollection SonarVisibleItems => sonarVisibleItems; + public static IReadOnlyCollection SonarVisibleItems => _sonarVisibleItems; + + private static readonly List _turretTargetItems = new List(); + + /// + /// Items whose is true. + /// + public static IReadOnlyCollection TurretTargetItems => _turretTargetItems; + + private static readonly List _chairItems = new List(); + + /// + /// Items that have the tag . Which is an oddly specific thing, but useful as an optimization for NPC AI. + /// + public static IReadOnlyCollection ChairItems => _chairItems; + + #endregion public new ItemPrefab Prefab => base.Prefab as ItemPrefab; + public override ContentPackage ContentPackage => Prefab?.ContentPackage; + public static bool ShowLinks = true; private HashSet tags; @@ -103,8 +127,9 @@ public void AssignCampaignInteractionType(CampaignMode.InteractionType interacti #endif //components that determine the functionality of the item - private readonly Dictionary componentsByType = new Dictionary(); + private readonly Dictionary> componentsByType = new Dictionary>(); private readonly List components; + /// /// Components that are Active or need to be updated for some other reason (status effects, sounds) /// @@ -495,6 +520,22 @@ public override float Scale Rect = new Rectangle(rect.X, rect.Y, newWidth, newHeight); } + //need to update to get the position of the physics body to match the new center of the item + if (body != null) + { + if (FullyInitialized) + { + //fully intialized = scaling after the item has been created + //if this happens in the editor, refresh the transform to get the rect to match the position of the physics body + if (Screen.Selected is { IsEditor: true }) { UpdateTransform(); } + } + else + { + //scaling during loading -> move the body to the new center of the rect + body.SetTransformIgnoreContacts(ConvertUnits.ToSimUnits(base.Position), body.Rotation); + } + } + if (components != null) { foreach (ItemComponent component in components) @@ -1302,9 +1343,11 @@ public Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool InsertToList(); ItemList.Add(this); - if (Prefab.IsDangerous) { dangerousItems.Add(this); } - if (Repairables.Any()) { repairableItems.Add(this); } - if (Prefab.SonarSize > 0.0f) { sonarVisibleItems.Add(this); } + if (Prefab.IsDangerous) { _dangerousItems.Add(this); } + if (Repairables.Any()) { _repairableItems.Add(this); } + if (Prefab.SonarSize > 0.0f) { _sonarVisibleItems.Add(this); } + if (Prefab.IsAITurretTarget) { _turretTargetItems.Add(this); } + if (Prefab.Tags.Contains(Barotrauma.Tags.ChairItem)) { _chairItems.Add(this); } CheckCleanable(); DebugConsole.Log("Created " + Name + " (" + ID + ")"); @@ -1484,17 +1527,24 @@ public void AddComponent(ItemComponent component) }; Type type = component.GetType(); - if (!componentsByType.ContainsKey(type)) + CacheComponent(type); + Type baseType = type.BaseType; + while (baseType != null) { - componentsByType.Add(type, component); - Type baseType = type.BaseType; - while (baseType != null && baseType != typeof(ItemComponent)) + CacheComponent(baseType); + baseType = baseType.BaseType; + } + + void CacheComponent(Type t) + { + if (!componentsByType.TryGetValue(t, out List cachedComponents)) { - if (!componentsByType.ContainsKey(baseType)) - { - componentsByType.Add(baseType, component); - } - baseType = baseType.BaseType; + cachedComponents = new List(); + componentsByType.Add(t, cachedComponents); + } + if (!cachedComponents.Contains(component)) + { + cachedComponents.Add(component); } } } @@ -1531,15 +1581,11 @@ public int GetComponentIndex(ItemComponent component) public T GetComponent() where T : ItemComponent { - if (componentsByType.TryGetValue(typeof(T), out ItemComponent component)) + if (componentsByType.TryGetValue(typeof(T), out List matchingComponents)) { - return (T)component; + return (T)matchingComponents.First(); } - if (typeof(T) == typeof(ItemComponent)) - { - return (T)components.FirstOrDefault(); - } - return default; + return null; } public IEnumerable GetComponents() @@ -1548,8 +1594,11 @@ public IEnumerable GetComponents() { return components.Cast(); } - if (!componentsByType.ContainsKey(typeof(T))) { return Enumerable.Empty(); } - return components.Where(c => c is T).Cast(); + if (componentsByType.TryGetValue(typeof(T), out List matchingComponents)) + { + return matchingComponents.Cast(); + } + return Enumerable.Empty(); } public float GetQualityModifier(Quality.StatType statType) @@ -1560,7 +1609,7 @@ public float GetQualityModifier(Quality.StatType statType) public void RemoveContained(Item contained) { ownInventory?.RemoveItem(contained); - contained.Container = null; + contained.Container = null; } public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = true, bool setPrevTransform = true) @@ -1585,14 +1634,7 @@ public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = try { #endif - if (!body.PhysEnabled || Submarine.Unloading) - { - body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform); - } - else - { - body.SetTransform(simPosition, rotation, setPrevTransform); - } + body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform); #if DEBUG } catch (Exception e) @@ -1648,14 +1690,14 @@ public void CheckCleanable() Prefab.PreferredContainers.Any() && (container == null || container.HasTag(Barotrauma.Tags.AllowCleanup))) { - if (!cleanableItems.Contains(this)) + if (!_cleanableItems.Contains(this)) { - cleanableItems.Add(this); + _cleanableItems.Add(this); } } else { - cleanableItems.Remove(this); + _cleanableItems.Remove(this); } } @@ -1844,9 +1886,9 @@ public Inventory FindParentInventory(Func predicate) public void SetContainedItemPositions() { - foreach (ItemComponent component in components) + foreach (var ownInventory in OwnInventories) { - (component as ItemContainer)?.SetContainedItemPositions(); + ownInventory.Container.SetContainedItemPositions(); } } @@ -2505,15 +2547,15 @@ public void UpdateTransform() if (Submarine == null && prevSub != null) { - body.SetTransform(body.SimPosition + prevSub.SimPosition, body.Rotation); + body.SetTransformIgnoreContacts(body.SimPosition + prevSub.SimPosition, body.Rotation); } else if (Submarine != null && prevSub == null) { - body.SetTransform(body.SimPosition - Submarine.SimPosition, body.Rotation); + body.SetTransformIgnoreContacts(body.SimPosition - Submarine.SimPosition, body.Rotation); } else if (Submarine != null && prevSub != null && Submarine != prevSub) { - body.SetTransform(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation); + body.SetTransformIgnoreContacts(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation); } if (Submarine != prevSub) @@ -3404,7 +3446,7 @@ public void Drop(Character dropper, bool createNetworkEvent = true, bool setTran } else if (setTransform) { - body.SetTransform(dropper.SimPosition, 0.0f); + body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f); } } } @@ -4075,7 +4117,7 @@ public static Item Load(ContentXElement element, Submarine submarine, bool creat } } - if (element.GetAttributeBool("markedfordeconstruction", false)) { deconstructItems.Add(item); } + if (element.GetAttributeBool("markedfordeconstruction", false)) { _deconstructItems.Add(item); } float prevRotation = item.Rotation; if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); } @@ -4363,7 +4405,7 @@ public override XElement Save(XElement parentElement) new XAttribute("name", Prefab.OriginalName), new XAttribute("identifier", Prefab.Identifier), new XAttribute("ID", ID), - new XAttribute("markedfordeconstruction", deconstructItems.Contains(this))); + new XAttribute("markedfordeconstruction", _deconstructItems.Contains(this))); if (PendingItemSwap != null) { @@ -4563,11 +4605,13 @@ public override void Remove() private void RemoveFromLists() { ItemList.Remove(this); - dangerousItems.Remove(this); - repairableItems.Remove(this); - sonarVisibleItems.Remove(this); - cleanableItems.Remove(this); - deconstructItems.Remove(this); + _dangerousItems.Remove(this); + _repairableItems.Remove(this); + _sonarVisibleItems.Remove(this); + _cleanableItems.Remove(this); + _deconstructItems.Remove(this); + _turretTargetItems.Remove(this); + _chairItems.Remove(this); RemoveFromDroppedStack(allowClientExecute: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index b24f012be6..64401aa076 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -223,6 +223,7 @@ public LocalizedString DisplayName public readonly int Amount; public readonly int? Quality; public readonly bool HideForNonTraitors; + public readonly InvSlotType MoveToSlot; /// /// How many of this item the fabricator can create (< 0 = unlimited) @@ -257,6 +258,7 @@ public FabricationRecipe(ContentXElement element, Identifier itemPrefab) FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault); HideForNonTraitors = element.GetAttributeBool(nameof(HideForNonTraitors), false); + MoveToSlot = element.GetAttributeEnum(nameof(MoveToSlot), InvSlotType.None); if (element.GetAttribute(nameof(Quality)) != null) { @@ -1000,7 +1002,7 @@ public ItemPrefab(ContentXElement element, ItemFile file) : base(element, file) ParseConfigElement(variantOf: null); } - private string GetTexturePath(ContentXElement subElement, ItemPrefab variantOf) + public string GetTexturePath(ContentXElement subElement, ItemPrefab variantOf) => subElement.DoesAttributeReferenceFileNameAlone("texture") ? Path.GetDirectoryName(variantOf?.ContentFile.Path ?? ContentFile.Path) : ""; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 4bdd1678f0..3798ce08b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -86,7 +86,12 @@ public bool InDetectable public readonly UInt64 CreationIndex; public string ErrorLine => $"- {ID}: {this} ({Submarine?.Info?.Name ?? "[null]"} {Submarine?.ID ?? 0}) {CreationStackTrace}"; - + + /// + /// Which content package is this entity from (if it's something like an item or a character that's loaded from a package, otherwise we assume it's the vanilla package). + /// + public virtual ContentPackage ContentPackage => GameMain.VanillaContent; + public Entity(Submarine submarine, ushort id) { this.Submarine = submarine; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index c7dbe941ea..179355407a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -317,9 +317,9 @@ public void Explode(Vector2 worldPosition, Entity damageSource, Character attack Color flashColor = Color.Lerp(Color.Transparent, screenColor, Math.Max((screenColorRange - cameraDist) / screenColorRange, 0.0f)); Screen.Selected.ColorFade(flashColor, Color.Transparent, screenColorDuration); } - foreach (Item item in Item.ItemList) + foreach (Sonar sonar in Sonar.SonarList) { - item.GetComponent()?.RegisterExplosion(this, worldPosition); + sonar.RegisterExplosion(this, worldPosition); } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 643a228edf..5f047f018f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -34,6 +34,18 @@ public bool IsHorizontal public readonly float GlowEffectT; + private readonly List overlappingGaps = new List(); + + /// + /// Do we need to recheck which gaps are overlapping with this one, and how much they should reduce this gap's flow? + /// + private bool overlappingGapsDirty; + + /// + /// How much overlapping gaps reduce the flow rate of this one? + /// + private float overlappingGapFlowRateReduction; + //a value between 0.0f-1.0f (0.0 = closed, 1.0f = open) private float open; @@ -67,22 +79,29 @@ public float Open set { if (float.IsNaN(value)) { return; } + float prevValue = open; if (value > open) { openedTimer = 1.0f; } - if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull)) + + open = MathHelper.Clamp(value, 0.0f, 1.0f); + if (!MathUtils.NearlyEqual(open, prevValue)) { - if (value > open && value >= 1.0f) - { - InformWaypointsAboutGapState(this, open: true); - } - else if (value < open && open >= 1.0f) + overlappingGapsDirty = true; + FlagOverlappingGapsDirty(); + if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull)) { - InformWaypointsAboutGapState(this, open: false); + if (open > prevValue && open >= 1.0f) + { + InformWaypointsAboutGapState(this, open: true); + } + else if (open < prevValue && prevValue >= 1.0f) + { + InformWaypointsAboutGapState(this, open: false); + } } } - open = MathHelper.Clamp(value, 0.0f, 1.0f); static void InformWaypointsAboutGapState(Gap gap, bool open) { @@ -205,7 +224,7 @@ public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, bool isDiagon Physics.CollisionWall, Physics.CollisionCharacter, findNewContacts: false); - outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})"; + outsideCollisionBlocker.UserData = this; outsideCollisionBlocker.Enabled = false; #if CLIENT Resized += newRect => IsHorizontal = newRect.Width < newRect.Height; @@ -338,7 +357,11 @@ private void FindHulls() { if (hulls[i] == null) { continue; } linkedTo.Add(hulls[i]); - if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this); + if (!hulls[i].ConnectedGaps.Contains(this)) { hulls[i].ConnectedGaps.Add(this); } + foreach (var gap in hulls[i].ConnectedGaps) + { + gap.overlappingGapsDirty = true; + } } } @@ -364,6 +387,12 @@ public override void Update(float deltaTime, Camera cam) deltaTime *= updateCount; updateCount = 0; + if (overlappingGapsDirty) + { + RefreshOverlappingGaps(); + overlappingGapsDirty = false; + } + flowForce = Vector2.Zero; outsideColliderRaycastTimer -= deltaTime; @@ -431,7 +460,7 @@ void UpdateRoomToRoom(float deltaTime, Hull hull1, Hull hull2) //a variable affecting the water flow through the gap //the larger the gap is, the faster the water flows - float sizeModifier = Size / 100.0f * open; + float sizeModifier = Size / 100.0f * open * (1.0f - overlappingGapFlowRateReduction); //horizontal gap (such as a regular door) if (IsHorizontal) @@ -597,7 +626,7 @@ void UpdateRoomToOut(float deltaTime, Hull hull1) { //a variable affecting the water flow through the gap //the larger the gap is, the faster the water flows - float sizeModifier = Size * open * open; + float sizeModifier = Size * open * open * (1.0f - overlappingGapFlowRateReduction); float delta = 500.0f * sizeModifier * deltaTime; @@ -790,6 +819,52 @@ public static Gap FindAdjacent(IEnumerable gaps, Vector2 worldPos, float al return null; } + private void RefreshOverlappingGaps() + { + overlappingGapFlowRateReduction = 0.0f; + overlappingGaps.Clear(); + foreach (var linked in linkedTo) + { + if (linked is not Hull hull) { continue; } + foreach (var connectedGap in hull.ConnectedGaps) + { + if (connectedGap == this) { continue; } + //let the "more open" gap reduce this gap's flow rate + //or if they're both equally open, let the one that was created first handle it + //(note that we can't use Entity.ID here because gaps on walls don't have IDs) + if (connectedGap.open > open || + (connectedGap.open == open && connectedGap.CreationIndex < CreationIndex)) + { + Rectangle intersection = Rectangle.Intersect(rect, connectedGap.rect); + if (intersection.Width > 0 && intersection.Height > 0) + { + //reduce flow rate based on how much of this gap is covered by the connected one, and how open the connected one is + float relativeOverlap = IsHorizontal ? + intersection.Height / (float)rect.Height : + intersection.Width / (float)rect.Width; + overlappingGapFlowRateReduction += relativeOverlap * connectedGap.open; + } + } + if (overlappingGapFlowRateReduction >= 1.0f) + { + overlappingGapFlowRateReduction = 1.0f; + break; + } + } + } + } + + /// + /// Mark all gaps that are currently known to overlap with this one as needing a refresh of overlapping gaps + /// + private void FlagOverlappingGapsDirty() + { + foreach (var overlappingGap in overlappingGaps) + { + overlappingGap.overlappingGapsDirty = true; + } + } + public override void ShallowRemove() { base.ShallowRemove(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 0a21b93309..4e87e96d9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -303,7 +303,11 @@ public float WaterVolume { if (!MathUtils.IsValid(value)) { return; } waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress); - if (waterVolume < Volume) { Pressure = rect.Y - rect.Height + waterVolume / rect.Width; } + if (waterVolume <= Volume) + { + //recalculate pressure, but only if there's less water than the volume, above that point the "overpressure" logic kicks in + Pressure = rect.Y - rect.Height + waterVolume / rect.Width; + } if (waterVolume > 0.0f) { update = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs index bbaad892ef..99dfc5e30a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs @@ -1,4 +1,8 @@ -using Microsoft.Xna.Framework; +using System; +using Barotrauma.Items.Components; +using FarseerPhysics; +using FarseerPhysics.Dynamics; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -8,6 +12,112 @@ interface ISpatialEntity Vector2 WorldPosition { get; } Vector2 SimPosition { get; } Submarine Submarine { get; } + + public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false) + { + if (seeingEntity is Character seeingCharacter) + { + return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing); + } + if (target is Character targetCharacter) + { + return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing); + } + else + { + return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing); + } + } + + public static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false) + { + System.Diagnostics.Debug.Assert(target != null); + if (target == null || target.Removed) { return false; } + if (seeingEntity == null) { return false; } + if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; } + if (!target.AnimController.SimplePhysicsEnabled) + { + //find the limbs that are furthest from the target's position (from the viewer's point of view) + Limb leftExtremity = null, rightExtremity = null; + float leftMostDot = 0.0f, rightMostDot = 0.0f; + Vector2 dir = target.WorldPosition - seeingEntity.WorldPosition; + Vector2 leftDir = new Vector2(dir.Y, -dir.X); + Vector2 rightDir = new Vector2(-dir.Y, dir.X); + foreach (Limb limb in target.AnimController.Limbs) + { + if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; } + if (limb.Hidden) { continue; } + Vector2 limbDir = limb.WorldPosition - seeingEntity.WorldPosition; + float leftDot = Vector2.Dot(limbDir, leftDir); + if (leftDot > leftMostDot) + { + leftMostDot = leftDot; + leftExtremity = limb; + continue; + } + float rightDot = Vector2.Dot(limbDir, rightDir); + if (rightDot > rightMostDot) + { + rightMostDot = rightDot; + rightExtremity = limb; + } + } + if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; } + if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; } + } + return false; + } + + public static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false) + { + System.Diagnostics.Debug.Assert(target != null); + if (target == null) { return false; } + if (seeingEntity == null) { return false; } + // TODO: Could we just use the method below? If not, let's refactor it so that we can. + Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition); + if (checkFacing && seeingEntity is Character seeingCharacter) + { + if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; } + } + //both inside the same sub (or both outside) + //OR the we're inside, the other character outside + if (target.Submarine == seeingEntity.Submarine || target.Submarine == null) + { + return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null; + } + //we're outside, the other character inside + else if (seeingEntity.Submarine == null) + { + return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null; + } + //both inside different subs + else + { + return + Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null && + Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null; + } + + bool IsBlocking(Fixture f) + { + var body = f.Body; + if (body == null) { return false; } + if (body.UserData is Structure wall) + { + if (!wall.CastShadow && seeThroughWindows) { return false; } + return wall != target; + } + else if (body.UserData is Item item) + { + if (item.GetComponent() is { HasWindow: true } door && seeThroughWindows) + { + if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; } + } + return item != target; + } + return true; + } + } } interface IIgnorable : ISpatialEntity diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs index 2cc1d7c279..8dbc714eae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -1,4 +1,4 @@ -using System; +using System; using Barotrauma.Extensions; using System.Collections.Generic; using System.Collections.Immutable; @@ -21,6 +21,8 @@ class Biome : PrefabWithUintIdentifier public float ActualMaxDifficulty => maxDifficulty; public float AdjustedMaxDifficulty => maxDifficulty - 0.1f; + public readonly float ExperienceFromMissionRewards; + public readonly ImmutableHashSet AllowedZones; @@ -50,6 +52,10 @@ public Biome(ContentXElement element, LevelGenerationParametersFile file) : base AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet(); MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0); maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100); + float baseExperience = 0.09f; + float difficultyRewardMultiplier = 0.25f; + float calculateDefaultExperience = baseExperience + MinDifficulty * difficultyRewardMultiplier / 100; + ExperienceFromMissionRewards = element.GetAttributeFloat("ExperienceFromMissionRewards", calculateDefaultExperience); var submarineAvailabilityOverrides = new HashSet(); if (element.GetChildElement("submarines") is ContentXElement availabilityElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 313b832eeb..d576dab256 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -217,8 +217,13 @@ public static List GeneratePath(List targetCells, List /// Makes the cell rounder by subdividing the edges and offsetting them at the middle /// /// How small the individual subdivided edges can be (smaller values produce rounder shapes, but require more geometry) - public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, float roundingAmount = 0.5f, float irregularity = 0.1f) + /// How "thin" irregularity is allowed to make parts of the cell. Very high irregularity values can lead to thin "spikes" or even parts where the "spike's" thickness becomes negative and wall segments intersect each other. + public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, float roundingAmount = 0.5f, float irregularity = 0.1f, float minThickness = 0.0f) { + //we need to make sure the vertices of the wall are still ordered counter-clockwise - + //if we deform some vertices so much the cell becomes concave, rendering the triangles will break (parts of the inside of the wall will render outside the edges) + var compareCCW = new CompareCCW(cell.Center); + List tempEdges = new List(); foreach (GraphEdge edge in cell.Edges) { @@ -231,8 +236,10 @@ public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, flo Vector2 edgeDiff = edge.Point2 - edge.Point1; Vector2 edgeDir = Vector2.Normalize(edgeDiff); + float maxExtrusion = float.PositiveInfinity; + const float minPassageWidth = 200.0f; //If the edge is next to an empty cell and there's another solid cell at the other side of the empty one, - //don't touch this edge. Otherwise we may end up closing off small passages between cells. + //we need to calculate how far we can extrude the edge so it doesn't end up closing off small passages between cells. var adjacentEmptyCell = edge.AdjacentCell(cell); if (adjacentEmptyCell?.CellType == CellType.Solid) { adjacentEmptyCell = null; } if (adjacentEmptyCell != null) @@ -252,8 +259,15 @@ public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, flo } if (adjacentEdge != null) { - tempEdges.Add(edge); - continue; + maxExtrusion = + new[] + { + Vector2.Distance(edge.Point1, adjacentEdge.Point1), + Vector2.Distance(edge.Point1, adjacentEdge.Point2), + Vector2.Distance(edge.Point1, adjacentEdge.Point2), + Vector2.Distance(edge.Point2, adjacentEdge.Point1), + }.Min(); + maxExtrusion = Math.Max(0, maxExtrusion - minPassageWidth); } } List edgePoints = new List(); @@ -274,12 +288,53 @@ public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, flo else { + //value that's 0 at edges, 0.5 at center float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount)); - float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.ServerAndClient); - Vector2 extrudedPoint = + //make the value "curve" from 0 to 1 at the center, instead of going linearly from 0 to 1 + centerF = MathF.Sin(centerF * MathHelper.Pi); + + //magic number intended to make old rounding values behave roughly the same with the new formula + //previously the extrusion increased linearly towards the center, forming a "spike" like "/\" + //now it follows a sine curve, which makes lower values produce rounder results + const float RoundingScale = 0.25f; + + //magic number intended to make old variance values behave roughly the same with the new formula + //previously a value of 1 would allow a maximum extrusion of 50% of the edge's length at the center, + //now we extrude by the variance at any point on the edge (not more in the center) + const float RandomVarianceScale = 0.25f; + float randomVariance = irregularity * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient); + + float extrusionAmount = edgeLength * ((roundingAmount * RoundingScale * centerF) + randomVariance * RandomVarianceScale); + extrusionAmount = Math.Min(extrusionAmount, maxExtrusion); + + Vector2 nonExtrudedPoint = + edge.Point1 + + edgeDiff * (i / (float)pointCount); + + Vector2 nextPoint = edge.Point1 + - edgeDiff * (i / (float)pointCount) + - edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF; + edgeDiff * ((i + 1) / (float)pointCount); + + //"extruding" inwards, need to make sure we don't make the edge poke through the cell from the other side + if (extrusionAmount < 0 && minThickness > 0.0f) + { + foreach (GraphEdge otherEdge in cell.Edges) + { + if (otherEdge == edge) { continue; } + float margin = minThickness * Math.Sign(extrusionAmount); + if (MathUtils.GetLineIntersection( + nonExtrudedPoint, nonExtrudedPoint + edgeNormal * (extrusionAmount + margin), + otherEdge.Point1, otherEdge.Point2, areLinesInfinite: false, out Vector2 intersection)) + { + extrusionAmount = Math.Min(extrusionAmount, Vector2.Distance(edge.Point1, intersection)) - margin; + //make sure we don't "overshoot", fix the inwards extrusion by instead extruding too much outwards + //(can happen on small cells in caves for example) + extrusionAmount = Math.Min(extrusionAmount, edge.Length / 2); + } + } + } + + Vector2 extrudedPoint = nonExtrudedPoint + edgeNormal * extrusionAmount; var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 2); bool isInside = false; @@ -306,14 +361,29 @@ public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, flo } if (isInside) { break; } } - - if (!isInside) - { - edgePoints.Add(extrudedPoint); - } + if (isInside) { continue; } + + //if adding the point would deform the edge so much that the normal of the new edge would point + //in the opposite direction from the undeformed edge's normal, don't allow adding the point + //(that would lead to the edge being "inside out", the wall texture and objects on the wall pointing inwards) + bool isNormalInverted = + Vector2.Dot(edgeNormal, GraphEdge.GetNormal(cell, edgePoints.Last(), extrudedPoint)) < 0 || + //check that the edge at the other side of the new point doesn't get inverted either + Vector2.Dot(edgeNormal, GraphEdge.GetNormal(cell, extrudedPoint, edge.Point2)) < 0; + if (isNormalInverted) { continue; } + + //make sure extruding the point doesn't change the vertex order + //(they're assumed to be sorted counter-clockwise, and if they're not, the triangles will generate incorrectly) + bool vertexOrderChanged = + compareCCW.Compare(edgePoints.Last(), nonExtrudedPoint) != compareCCW.Compare(edgePoints.Last(), extrudedPoint) || + compareCCW.Compare(nonExtrudedPoint, nextPoint) != compareCCW.Compare(extrudedPoint, nextPoint); + if (vertexOrderChanged) { continue; } + + edgePoints.Add(extrudedPoint); } } + for (int i = 0; i < edgePoints.Count - 1; i++) { tempEdges.Add(new GraphEdge(edgePoints[i], edgePoints[i + 1]) @@ -376,19 +446,7 @@ public static Body GeneratePolygons(List cells, Level level, out Li continue; } - Vector2 minVert = tempVertices[0]; - Vector2 maxVert = tempVertices[0]; - foreach (var vert in tempVertices) - { - minVert = new Vector2( - Math.Min(minVert.X, vert.X), - Math.Min(minVert.Y, vert.Y)); - maxVert = new Vector2( - Math.Max(maxVert.X, vert.X), - Math.Max(maxVert.Y, vert.Y)); - } - Vector2 center = (minVert + maxVert) / 2; - renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, center)); + renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, cell.Center)); if (bodyPoints.Count < 2) { continue; } @@ -411,7 +469,7 @@ public static Body GeneratePolygons(List cells, Level level, out Li if (cell.CellType == CellType.Empty) { continue; } cellBody.UserData = cell; - var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(center)); + var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(cell.Center)); for (int i = 0; i < triangles.Count; i++) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index ef426fdb44..394fc935e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -1205,7 +1205,8 @@ static void MarkEdges(VoronoiCell cell, TunnelType tunnelType) CaveGenerator.RoundCell(cell, minEdgeLength: GenerationParams.CellSubdivisionLength, roundingAmount: GenerationParams.CellRoundingAmount, - irregularity: GenerationParams.CellIrregularity); + irregularity: GenerationParams.CellIrregularity, + minThickness: GenerationParams.WallTextureExpandInwardsAmount); } } @@ -1278,12 +1279,22 @@ static void MarkEdges(VoronoiCell cell, TunnelType tunnelType) Debug.Assert(triangleLists.Count == cellBatches.Count); for (int i = 0; i < triangleLists.Count; i++) { + //the solid black inner part of the wall + var wallVerts = CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].cells, + expandOutwards: 0.0f, expandInwards: GenerationParams.WallTextureExpandInwardsAmount, + outerColor: GenerationParams.WallColor, innerColor: Color.Black, + this, zCoord: 0.9f, preventExpandThroughCell: true).ToArray(); + CaveGenerator.GenerateTextureCoordinates(wallVerts, GenerationParams.WallTextureSize); renderer.SetVertices( - CaveGenerator.GenerateWallVertices(triangleLists[i], GenerationParams, zCoord: 0.9f).ToArray(), - CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].cells, this, zCoord: 0.9f).ToArray(), + wallVerts, + CaveGenerator.GenerateWallEdgeVertices( + cellBatches[i].cells, + GenerationParams.WallEdgeExpandOutwardsAmount, GenerationParams.WallEdgeExpandInwardsAmount, + outerColor: GenerationParams.WallColor, innerColor: GenerationParams.WallColor, + this, zCoord: 0.9f).ToArray(), + CaveGenerator.GenerateWallVertices(triangleLists[i], Color.Black, zCoord: 0.9f).ToArray(), cellBatches[i].parentCave?.CaveGenerationParams?.WallSprite == null ? GenerationParams.WallSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallSprite.Texture, - cellBatches[i].parentCave?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallEdgeSprite.Texture, - GenerationParams.WallColor); + cellBatches[i].parentCave?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallEdgeSprite.Texture); } #endif @@ -2808,6 +2819,7 @@ private void GenerateItems() if (l.Cell == null || l.Edge == null) { return false; } if (resourceInfo.IsIslandSpecific && !l.Cell.Island) { return false; } if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > startPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; } + if (l.Edge.Length < itemPrefab.Size.X) { return false; } if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; } return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _); @@ -2839,6 +2851,7 @@ private void GenerateItems() { if (l.Cell == null || l.Edge == null) { return false; } if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; } + if (l.Edge.Length < selectedPrefab.Size.X) { return false; } l.InitializeResources(); return l.Resources.Count <= GetMaxResourcesOnEdge(selectedPrefab, l, out _); }, randSync: Rand.RandSync.ServerAndClient); @@ -3375,37 +3388,41 @@ bool IsBlockedByWall() private void PlaceResources(ItemPrefab resourcePrefab, int resourceCount, ClusterLocation location, out List placedResources, float? edgeLength = null, float maxResourceOverlap = 0.4f) { - edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2); + edgeLength ??= location.Edge.Length; Vector2 edgeDir = (location.Edge.Point2 - location.Edge.Point1) / edgeLength.Value; if (!MathUtils.IsValid(edgeDir)) { edgeDir = Vector2.Zero; } - var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X)); + float minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X)); minResourceOverlap = Math.Clamp(minResourceOverlap, 0, maxResourceOverlap); - var lerpAmounts = new float[resourceCount]; + float[] lerpAmounts = new float[resourceCount]; lerpAmounts[0] = 0.0f; - var lerpAmount = 0.0f; + float lerpAmount = 0.0f; for (int i = 1; i < resourceCount; i++) { - var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient); - lerpAmount += (1.0f - overlap) * resourcePrefab.Size.X / edgeLength.Value; - lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f); + float overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient); + lerpAmount = Math.Clamp(lerpAmount + (1.0f - overlap) * resourcePrefab.Size.X / edgeLength.Value, 0.0f, 1.0f); + lerpAmounts[i] = lerpAmount; } + var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.ServerAndClient); placedResources = new List(); for (int i = 0; i < resourceCount; i++) { - Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1 + edgeDir * resourcePrefab.Size.X / 2, location.Edge.Point2 - edgeDir * resourcePrefab.Size.X / 2, startOffset + lerpAmounts[i]); + Vector2 selectedPos = + location.Edge.Length < resourcePrefab.Size.X ? + location.Edge.Center : + Vector2.Lerp(location.Edge.Point1 + edgeDir * resourcePrefab.Size.X / 2, location.Edge.Point2 - edgeDir * resourcePrefab.Size.X / 2, startOffset + lerpAmounts[i]); var item = new Item(resourcePrefab, selectedPos, submarine: null); Vector2 edgeNormal = location.Edge.GetNormal(location.Cell); float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f)); moveAmount += (item.GetComponent()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient); item.Move(edgeNormal * moveAmount); + item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); if (item.GetComponent() is Holdable h) { h.AttachToWall(); - item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); } else if (item.body != null) { @@ -3914,7 +3931,7 @@ bool IsValidWaypoint(WayPoint wp) attempt++; spawnPoint = wayPoint.WorldPosition; success = TryPositionSub(subBorders, subName, placement, ref spawnPoint); - positionHistory.Add($"{info.Name}: {attempt}", positions.ToList()); + positionHistory.TryAdd($"{info.Name}: {attempt}", positions.ToList()); positions.Clear(); if (success) { @@ -3940,6 +3957,11 @@ bool IsValidWaypoint(WayPoint wp) PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: sub)); foreach (Hull hull in sub.GetHulls(false)) { + if (hull.WaterPercentage > 0) + { + // Don't override the water level set by the sub designer + continue; + } if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.WreckHullFloodingChance) { hull.WaterVolume = @@ -4296,7 +4318,7 @@ private void CreateWrecks() if (LevelData.ForceWreck != null) { //force the desired wreck to be chosen first - var matchingFile = placeableWrecks.FirstOrDefault(wreck => wreck.WreckFile.Path == LevelData.ForceWreck.FilePath); + PlaceableWreck matchingFile = placeableWrecks.FirstOrDefault(wreck => wreck.WreckFile.Path == LevelData.ForceWreck.FilePath); if (matchingFile.WreckFile != null) { placeableWrecks.Remove(matchingFile); @@ -4337,7 +4359,12 @@ private void CreateWrecks() { var placeableWreck = placeableWrecks.First(); var wreckFile = placeableWreck.WreckFile; - placeableWrecks.RemoveAt(0); + if (LevelData.ForceWreck == null) + { + // If a wreck is forced, don't remove it -> only spawns those wrecks (makes testing them in the editor easier). + // Normally we don't want two instances of the same wreck to spawn in the same level, but when we test or debug certain wrecks, we want only them. + placeableWrecks.RemoveAt(0); + } LevelData.ThalamusSpawn thalamusSpawn = requireThalamus ? LevelData.ThalamusSpawn.Forced : LevelData.ThalamusSpawn.Random; if (LevelData.ForceWreck != null) { thalamusSpawn = LevelData.ForceThalamus; } @@ -4783,40 +4810,13 @@ public void PrepareBeaconStation() #endif } } - else if (GameMain.NetworkMember is not { IsClient: true }) - { - bool allowDisconnectedWires = true; - bool allowDamagedDevices = true; - bool allowDamagedWalls = true; - if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info) - { - allowDisconnectedWires = info.AllowDisconnectedWires; - allowDamagedWalls = info.AllowDamagedWalls; - allowDamagedDevices = info.AllowDamagedDevices; - } - - //remove wires - float disconnectWireMinDifficulty = 20.0f; - float disconnectWireProbability = MathUtils.InverseLerp(disconnectWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f; - if (disconnectWireProbability > 0.0f && allowDisconnectedWires) - { - DisconnectBeaconStationWires(disconnectWireProbability); - } - - if (allowDamagedDevices) - { - DamageBeaconStationDevices(breakDeviceProbability: 0.5f); - } - if (allowDamagedWalls) - { - DamageBeaconStationWalls(damageWallProbability: 0.25f); - } - } SetLinkedSubCrushDepth(BeaconStation); } public void DisconnectBeaconStationWires(float disconnectWireProbability) { + if (BeaconStation?.Info?.BeaconStationInfo is { AllowDisconnectedWires: false }) { return; } + if (disconnectWireProbability <= 0.0f) { return; } List beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation); foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) @@ -4852,6 +4852,8 @@ public void DisconnectBeaconStationWires(float disconnectWireProbability) public void DamageBeaconStationDevices(float breakDeviceProbability) { + if (BeaconStation?.Info?.BeaconStationInfo is { AllowDamagedDevices: false }) { return; } + if (breakDeviceProbability <= 0.0f) { return; } //break powered items List beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation); @@ -4867,6 +4869,8 @@ public void DamageBeaconStationDevices(float breakDeviceProbability) public void DamageBeaconStationWalls(float damageWallProbability) { + if (BeaconStation?.Info?.BeaconStationInfo is { AllowDamagedWalls: false }) { return; } + if (damageWallProbability <= 0.0f) { return; } //poke holes in the walls foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 5ae23c43ab..264395a98b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -236,7 +236,7 @@ public int CellSubdivisionLength } - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the individual wall cells are rounded. " + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the individual wall cells are rounded. " + "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")] public float CellRoundingAmount { @@ -247,7 +247,7 @@ public float CellRoundingAmount } } - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, IsPropertySaveable.Yes, description: "How much random variance is applied to the edges of the cells. " + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.1f, IsPropertySaveable.Yes, description: "How much random variance is applied to the edges of the cells. " + "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")] public float CellIrregularity { @@ -525,19 +525,19 @@ public int MountainHeightMax [Serialize(5, IsPropertySaveable.Yes, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MaxCorpseCount { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a character set to be spawned as a corpse spawns as a human husk instead? Percentage from 0 to 1 per character."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a character set to be spawned as a corpse spawns as a human husk instead? Percentage from 0 to 1 per character."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float HuskProbability { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float ThalamusProbability { get; set; } - [Serialize(0.5f, IsPropertySaveable.Yes, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float WreckHullFloodingChance { get; set; } - [Serialize(0.1f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.1f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float WreckFloodingHullMinWaterPercentage { get; set; } - [Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float WreckFloodingHullMaxWaterPercentage { get; set; } #endregion @@ -602,6 +602,13 @@ public float WallEdgeExpandInwardsAmount private set; } + [Serialize(1000.0f, IsPropertySaveable.Yes, description: "How deep inside the walls the wall texture extends to before fading to black."), Editable(minValue: 0.0f, maxValue: 10000.0f)] + public float WallTextureExpandInwardsAmount + { + get; + private set; + } + [Header("Colors")] [Serialize("27,30,36", IsPropertySaveable.Yes), Editable] public Color AmbientLightColor diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 1aadaa3542..960aa818fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -339,11 +339,11 @@ public LevelObjectPrefab(ContentXElement element, LevelObjectPrefabsFile file, I InitProjSpecific(element); } - //use the maximum width of the sprite as the minimum surface width if no value is given - if (element != null && !element.Attributes("minsurfacewidth").Any()) + //use (a bit less than) the maximum width of the sprite as the minimum surface width if no value is given + if (element != null && element.GetAttribute("minsurfacewidth") == null) { - if (Sprites.Any()) MinSurfaceWidth = Sprites[0].size.X * MaxSize; - if (DeformableSprite != null) MinSurfaceWidth = Math.Max(MinSurfaceWidth, DeformableSprite.Size.X * MaxSize); + if (Sprites.Any()) { MinSurfaceWidth = Sprites[0].size.X * MaxSize * 0.8f; } + if (DeformableSprite != null) { MinSurfaceWidth = Math.Max(MinSurfaceWidth, DeformableSprite.Size.X * MaxSize * 0.8f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 0fb3927a49..21a71b22b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -609,6 +609,21 @@ public void Update(float deltaTime) return; } + if (PhysicsBody != null) + { + if (currentForceFluctuation <= 0.0f && statusEffects.None() && attacks.None()) + { + //no force atm, and no status effects or attacks the trigger could apply + // -> we can disable the collider and get a minor physics performance improvement + PhysicsBody.Enabled = false; + return; + } + else + { + PhysicsBody.Enabled = true; + } + } + foreach (Entity triggerer in triggerers) { if (triggerer.Removed) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index ad44567e62..c07c87098e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -124,8 +124,17 @@ public class StoreInfo /// public int PriceModifier { get; set; } public Location Location { get; } + + /// + /// The maximum effect positive reputation can have on store prices (e.g. 0.5 = 50% discount with max reputation). + /// private float MaxReputationModifier => Location.StoreMaxReputationModifier; + /// + /// The maximum effect negative reputation can have on store prices (e.g. 0.5 = 50% price increase with minimum reputation). + /// + private float MinReputationModifier => Location.StoreMinReputationModifier; + private StoreInfo(Location location) { Location = location; @@ -343,7 +352,7 @@ public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, if (characters.Any()) { price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false)); - price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag))); + price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValueWithAll(StatTypes.StoreSellMultiplier, tag))); } // Price should never go below 1 mk @@ -373,7 +382,7 @@ public float GetReputationModifier(bool buying) } else { - return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation); + return MathHelper.Lerp(1.0f, 1.0f + MinReputationModifier, reputation.Value / reputation.MinReputation); } } else @@ -384,7 +393,7 @@ public float GetReputationModifier(bool buying) } else { - return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation); + return MathHelper.Lerp(1.0f, 1.0f - MinReputationModifier, reputation.Value / reputation.MinReputation); } } } @@ -398,6 +407,7 @@ public override string ToString() public Dictionary Stores { get; set; } private float StoreMaxReputationModifier => Type.StoreMaxReputationModifier; + private float StoreMinReputationModifier => Type.StoreMinReputationModifier; private float StoreSellPriceModifier => Type.StoreSellPriceModifier; private float DailySpecialPriceModifier => Type.DailySpecialPriceModifier; private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 7a82719a2d..0b0de60657 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -118,6 +118,7 @@ public Color SpriteColor } public float StoreMaxReputationModifier { get; } = 0.1f; + public float StoreMinReputationModifier { get; } = 1.0f; public float StoreSellPriceModifier { get; } = 0.3f; public float DailySpecialPriceModifier { get; } = 0.5f; public float RequestGoodPriceModifier { get; } = 2f; @@ -264,6 +265,7 @@ public LocationType(ContentXElement element, LocationTypesFile file) : base(file break; case "store": StoreMaxReputationModifier = subElement.GetAttributeFloat("maxreputationmodifier", StoreMaxReputationModifier); + StoreMinReputationModifier = subElement.GetAttributeFloat("minreputationmodifier", StoreMaxReputationModifier); StoreSellPriceModifier = subElement.GetAttributeFloat("sellpricemodifier", StoreSellPriceModifier); DailySpecialPriceModifier = subElement.GetAttributeFloat("dailyspecialpricemodifier", DailySpecialPriceModifier); RequestGoodPriceModifier = subElement.GetAttributeFloat("requestgoodpricemodifier", RequestGoodPriceModifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 2fea7429ab..dbb226f117 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Xml.Linq; using Barotrauma.Extensions; namespace Barotrauma @@ -301,12 +302,13 @@ public bool IsLinkAllowed(MapEntityPrefab target) protected void LoadDescription(ContentXElement element) { - Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); - Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); - + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); string originalDescription = Description.Value; - if (descriptionIdentifier != Identifier.Empty) + const string descriptionIdentifierAttributeName = "descriptionidentifier"; + XAttribute descriptionIdenfifierAttribute = element.GetAttribute(descriptionIdentifierAttributeName); + if (descriptionIdenfifierAttribute != null) { + Identifier descriptionIdentifier = element.GetAttributeIdentifier(descriptionIdentifierAttributeName, Identifier.Empty); Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}"); } else if (nameIdentifier == Identifier.Empty) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index d218d83b58..8c11def4e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -115,7 +115,11 @@ private static Submarine Generate(OutpostGenerationParams generationParams, Loca { outpostInfos.Add(new SubmarineInfo(outpostFile.Path.Value)); } - if (!generationParams.OutpostTag.IsEmpty) + if (generationParams.OutpostTag.IsEmpty) + { + outpostInfos = outpostInfos.FindAll(o => o.OutpostTags.None()); + } + else { if (outpostInfos.Any(o => o.OutpostTags.Contains(generationParams.OutpostTag))) { @@ -448,6 +452,8 @@ List loadEntities(Submarine sub) entities[selectedModule] = moduleEntities; } + int maxMoveAmount = Math.Max(2000, selectedModules.Max(m => Math.Max(m.Bounds.Width, m.Bounds.Height))); + bool overlapsFound = true; int iteration = 0; while (overlapsFound) @@ -465,7 +471,7 @@ List loadEntities(Submarine sub) while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingTries > 0) { overlapsFound = true; - if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, out Dictionary solution)) + if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, maxMoveAmount, out Dictionary solution)) { foreach (KeyValuePair kvp in solution) { @@ -909,7 +915,12 @@ private static bool ModuleOverlapsWithModuleConnections(IEnumerableAll generated modules /// The solution to the overlap (if any). Key = placed module, value = distance to move the module /// Was a solution found for resolving the overlap. - private static bool FindOverlapSolution(IEnumerable movableModules, PlacedModule module1, PlacedModule module2, IEnumerable allmodules, out Dictionary solution) + private static bool FindOverlapSolution( + IEnumerable movableModules, + PlacedModule module1, PlacedModule module2, + IEnumerable allmodules, + int maxMoveAmount, + out Dictionary solution) { solution = new Dictionary(); foreach (PlacedModule module in movableModules) @@ -925,7 +936,6 @@ private static bool FindOverlapSolution(IEnumerable movableModules Vector2 moveDir = GetMoveDir(module.ThisGapPosition); Vector2 moveStep = moveDir * 50.0f; Vector2 currentMove = Vector2.Zero; - float maxMoveAmount = 2000.0f; List subsequentModules2 = new List(); GetSubsequentModules(module, movableModules, ref subsequentModules2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index ca974ed5c9..4abde1f099 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -53,6 +53,8 @@ partial class Structure : MapEntity, IDamageable, IServerSerializable, ISerializ const float LeakThreshold = 0.1f; const float BigGapThreshold = 0.7f; + public override ContentPackage ContentPackage => Prefab?.ContentPackage; + #if CLIENT public SpriteEffects SpriteEffects = SpriteEffects.None; #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index fc90a2efe7..2d90d017c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1522,7 +1522,14 @@ public bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs if (entity.Submarine == null) { return false; } if (includingConnectedSubs) { - return GetConnectedSubs().Any(s => s == entity.Submarine && (allowDifferentTeam || entity.Submarine.TeamID == TeamID) && (allowDifferentType || entity.Submarine.Info.Type == Info.Type)); + // Performance-sensitive code -> implemented without Linq. + foreach (Submarine s in connectedSubs) + { + if (s == entity.Submarine && (allowDifferentTeam || entity.Submarine.TeamID == TeamID) && (allowDifferentType || entity.Submarine.Info.Type == Info.Type)) + { + return true; + } + } } return false; } @@ -1938,8 +1945,8 @@ public void SaveToXElement(XElement element) { bool hasThalamus = false; - var wreckAiEntities = WreckAIConfig.Prefabs.Select(p => p.Entity).ToImmutableHashSet(); - var prefabsOnSub = GetItems(true).Select(i => i.Prefab).Distinct().ToImmutableHashSet(); + var wreckAiEntities = WreckAIConfig.Prefabs.Select(p => p.Entity); + var prefabsOnSub = GetItems(true).Select(i => i.Prefab).Distinct(); foreach (ItemPrefab prefab in prefabsOnSub) { @@ -2077,7 +2084,6 @@ public static void Unload() #if CLIENT RoundSound.RemoveAllRoundSounds(); GameMain.LightManager?.ClearLights(); - depthSortedDamageable.Clear(); #endif var _loaded = new List(loaded); foreach (Submarine sub in _loaded) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 1dac897cf7..0c20ed708c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -504,10 +504,8 @@ private void DisplaceCharacters(Vector2 subTranslation) foreach (Character c in Character.CharacterList) { - if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine != CanEnterSubmarine.True) - { - continue; - } + //character inside some sub, no need to displace + if (c.Submarine != null) { continue; } foreach (Limb limb in c.AnimController.Limbs) { @@ -525,13 +523,11 @@ private void DisplaceCharacters(Vector2 subTranslation) continue; } - //"+ translatedir" in order to move the character slightly away from the wall c.AnimController.SetPosition(ConvertUnits.ToSimUnits(c.WorldPosition + (intersection - limb.WorldPosition)) + translateDir); - return; + break; } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs index 8580b9ce5c..cdcc9f3514 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -93,6 +93,10 @@ public static Vector2 InterpolateCursorPositionError(Vector2 cursorPositionError return cursorPositionError *= 0.7f; } + /// + /// Quantizes the value so it's "as accurate as it can be" when the value is represented using the specified number of bits. + /// Relevant e.g. when writing float values into network messages using some specific number of bits. + /// public static Vector2 Quantize(Vector2 value, float min, float max, int numberOfBits) { return new Vector2( @@ -100,15 +104,21 @@ public static Vector2 Quantize(Vector2 value, float min, float max, int numberOf Quantize(value.Y, min, max, numberOfBits)); } + /// + /// Quantizes the value so it's "as accurate as it can be" when the value is represented using the specified number of bits. + /// Relevant e.g. when writing float values into network messages using some specific number of bits. + /// public static float Quantize(float value, float min, float max, int numberOfBits) { - float step = (max - min) / (1 << (numberOfBits + 1)); + value = MathHelper.Clamp(value, min, max); + + float step = (max - min) / ((1 << numberOfBits) - 1); if (Math.Abs(value) < step + 0.00001f) { return 0.0f; } - return MathUtils.RoundTowardsClosest(MathHelper.Clamp(value, min, max), step); + return MathUtils.RoundTowardsClosest(value - min, step) + min; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs index c88d1c2675..8b481c1e9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -1,9 +1,18 @@ using System; using System.Collections.Generic; -using System.Linq; namespace Barotrauma.Networking { + class EntityEventException : Exception + { + public readonly Entity Entity; + + public EntityEventException(string errorMessage, Entity causingEntity, Exception innerException = null) : base(errorMessage, innerException) + { + Entity = causingEntity; + } + } + abstract class NetEntityEventManager { public const int MaxEventBufferLength = 1024; @@ -29,7 +38,7 @@ protected void Write(IWriteMessage msg, List eventsToSync, out L } catch (Exception exception) { - DebugConsole.ThrowError("Failed to write an event for the entity \"" + e.Entity + "\"", exception); + DebugConsole.ThrowError($"Failed to write an event (ID: {e.ID}) for the entity \"{e.Entity}\"", exception, contentPackage: e.Entity?.ContentPackage); GameAnalyticsManager.AddErrorEventOnce("NetEntityEventManager.Write:WriteFailed" + e.Entity.ToString(), GameAnalyticsManager.ErrorSeverity.Error, "Failed to write an event for the entity \"" + e.Entity + "\"\n" + exception.StackTrace.CleanupStackTrace()); @@ -37,7 +46,8 @@ protected void Write(IWriteMessage msg, List eventsToSync, out L //write an empty event to avoid messing up IDs //(otherwise the clients might read the next event in the message and think its ID //is consecutive to the previous one, even though we skipped over this broken event) - tempBuffer.WriteUInt16(Entity.NullEntityID); + tempBuffer.WriteUInt16(Entity.NullEntityID); + tempBuffer.WriteVariableUInt32(0); //size of the event eventCount++; continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index 1b6c15a17e..a126c446c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -31,7 +31,7 @@ partial class OrderChatMessage : ChatMessage /// public OrderChatMessage(Order order, Character targetCharacter, Character sender, bool isNewOrder = true) : this(order, - order?.GetChatMessage(targetCharacter?.Name, + order?.GetChatMessage(targetCharacter?.DisplayName, (order.TargetEntity as Hull ?? sender?.CurrentHull)?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), targetCharacter, sender, isNewOrder) @@ -51,7 +51,7 @@ public static string NameFromEntityOrNull(Entity entity) => entity switch { null => null, - Character character => character.Name, + Character character => character.DisplayName, Item it => it.Name, _ => throw new ArgumentException("Entity is not a character or item", nameof(entity)) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index c34bd35b71..10d6fb065a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -35,6 +35,7 @@ public enum PlayStyle public enum RespawnMode { + None, MidRound, BetweenRounds, Permadeath, @@ -416,6 +417,19 @@ public int TickRate set { tickRate = MathHelper.Clamp(value, 1, 60); } } + private int maxLagCompensation = 150; + [Serialize(150, IsPropertySaveable.Yes, description: + "Maximum amount of lag compensation for firing weapons, in milliseconds. " + + "E.g. when a client fires a gun, the server will be notified about it with some latency, and checks if it hit anything in the past (at the time the shot was taken), up to this limit. " + + "The largest allowed lag compensation is 500 milliseconds.")] + public int MaxLagCompensation + { + get { return maxLagCompensation; } + set { maxLagCompensation = MathHelper.Clamp(value, 0, 500); } + } + + public float MaxLagCompensationSeconds => maxLagCompensation / 1000.0f; + [Serialize(true, IsPropertySaveable.Yes, description: "Do clients need to be authenticated (e.g. based on Steam ID or an EGS ownership token). Can be disabled if you for example want to play the game in a local network without a connection to external services.")] public bool RequireAuthentication { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs index c94691e3a8..d2fe8c378d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs @@ -15,6 +15,7 @@ static class Physics public const Category CollisionProjectile = Category.Cat7; public const Category CollisionLevel = Category.Cat8; public const Category CollisionRepairableWall = Category.Cat9; + public const Category CollisionLagCompensationBody = Category.Cat10; public const Category DefaultItemCollidesWith = CollisionWall | CollisionLevel | CollisionPlatform | CollisionRepairableWall; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index e60885cbcb..e695ebd745 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -897,10 +897,11 @@ public void UpdateDrawPosition(bool interpolate = true) } else { - drawPosition = prevPosition = ConvertUnits.ToDisplayUnits(FarseerBody.Position); + prevPosition = FarseerBody.Position; + drawPosition = ConvertUnits.ToDisplayUnits(FarseerBody.Position); drawRotation = prevRotation = FarseerBody.Rotation; drawOffset = Vector2.Zero; - drawRotation = 0.0f; + rotationOffset = 0.0f; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index c4e8b814e8..c528fca789 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -1,4 +1,4 @@ -/* +/* * Created by SharpDevelop. * User: Burhan * Date: 17/06/2014 @@ -212,16 +212,41 @@ public bool IsPointInside(Vector2 point) public bool IsPointInsideAABB(Vector2 point2, float margin) { + // Note: Previously used Linq.All() queries, which were simpler and more readable, but based on DPA, this caused major memory allocations due to captured variables in the lambdas, as the method is used a lot when generating levels. + // So the code was refactored to use a custom local implementation instead. Vector2 transformedPoint = point2 - Translation; Vector2 max = transformedPoint + Vector2.One * margin; Vector2 min = transformedPoint - Vector2.One * margin; - - if (Edges.All(e => e.Point1.X < min.X && e.Point2.X < min.X)) { return false; } - if (Edges.All(e => e.Point1.Y < min.Y && e.Point2.Y < min.Y)) { return false; } - if (Edges.All(e => e.Point1.X > max.X && e.Point2.X > max.X)) { return false; } - if (Edges.All(e => e.Point1.Y > max.Y && e.Point2.Y > max.Y)) { return false; } - + if (AllOutsideBounds(static (e, t) => e.Point1.X < t && e.Point2.X < t, bounds: min.X)) { return false; } + if (AllOutsideBounds(static (e, t) => e.Point1.Y < t && e.Point2.Y < t, bounds: min.Y)) { return false; } + if (AllOutsideBounds(static (e, t) => e.Point1.X > t && e.Point2.X > t, bounds: max.X)) { return false; } + if (AllOutsideBounds(static (e, t) => e.Point1.Y > t && e.Point2.Y > t, bounds: max.Y)) { return false; } return true; + + bool AllOutsideBounds(Func predicate, float bounds) + { + foreach (GraphEdge edge in Edges) + { + if (!predicate(edge, bounds)) + { + return false; + } + } + return true; + } + } + + public void GetBounds(out Vector2 min, out Vector2 max) + { + min = Center; + max = Center; + foreach (GraphEdge edge in Edges) + { + min = new Vector2(Math.Min(edge.Point1.X, min.X), Math.Min(edge.Point1.Y, min.Y)); + min = new Vector2(Math.Min(edge.Point2.X, min.X), Math.Min(edge.Point2.Y, min.Y)); + max = new Vector2(Math.Max(edge.Point1.X, max.X), Math.Max(edge.Point1.Y, max.Y)); + max = new Vector2(Math.Max(edge.Point2.X, max.X), Math.Max(edge.Point2.Y, max.Y)); + } } } @@ -240,6 +265,8 @@ public Vector2 Center get { return (Point1 + Point2) / 2.0f; } } + public float Length => Vector2.Distance(Point1, Point2); + public GraphEdge(Vector2 point1, Vector2 point2) { this.Point1 = point1; @@ -265,9 +292,18 @@ public VoronoiCell AdjacentCell(VoronoiCell cell) /// public Vector2 GetNormal(VoronoiCell cell) { - Vector2 dir = Vector2.Normalize(Point1 - Point2); + return GetNormal(cell, Point1, Point2); + } + + /// + /// Returns the normal of the edge between point1 to point2 that points outwards from the specified cell + /// + public static Vector2 GetNormal(VoronoiCell cell, Vector2 point1, Vector2 point2) + { + Vector2 center = (point1 + point2) / 2; + Vector2 dir = Vector2.Normalize(point1 - point2); Vector2 normal = new Vector2(dir.Y, -dir.X); - if (cell != null && Vector2.Dot(normal, Vector2.Normalize(Center - (cell.Center - cell.Translation))) < 0) + if (cell != null && Vector2.Dot(normal, Vector2.Normalize(center - (cell.Center - cell.Translation))) < 0) { normal = -normal; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index ceb11f4cf3..866d4db16d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -135,7 +135,10 @@ public override void Update(double deltaTime) foreach (PhysicsBody body in PhysicsBody.List) { - if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) { body.Update(); } + if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) + { + body.Update(); + } } MapEntity.ClearHighlightedEntities(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs index 00a77cdf5d..f3979fd503 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs @@ -573,6 +573,11 @@ private bool TryGetFloatValueWithoutReflection(object parentObject, out float va if (parentObject is PowerContainer powerContainer) { value = powerContainer.Charge; return true; } } break; + case nameof(Repairable.StressDeteriorationMultiplier): + { + if (parentObject is Repairable repairable) { value = repairable.StressDeteriorationMultiplier; return true; } + } + break; case nameof(PowerContainer.ChargePercentage): { if (parentObject is PowerContainer powerContainer) { value = powerContainer.ChargePercentage; return true; } @@ -583,6 +588,11 @@ private bool TryGetFloatValueWithoutReflection(object parentObject, out float va if (parentObject is PowerContainer powerContainer) { value = powerContainer.RechargeRatio; return true; } } break; + case nameof(ItemContainer.ContainedNonBrokenItemCount): + { + if (parentObject is ItemContainer itemContainer) { value = itemContainer.ContainedNonBrokenItemCount; return true; } + } + break; case nameof(Reactor.AvailableFuel): { if (parentObject is Reactor reactor) { value = reactor.AvailableFuel; return true; } } break; @@ -622,6 +632,15 @@ private bool TryGetFloatValueWithoutReflection(object parentObject, out float va case nameof(Item.Condition): { if (parentObject is Item item) { value = item.Condition; return true; } } break; + case nameof(Item.ConditionPercentage): + { if (parentObject is Item item) { value = item.ConditionPercentage; return true; } } + break; + case nameof(Item.SightRange): + { if (parentObject is Item item) { value = item.SightRange; return true; } } + break; + case nameof(Item.SoundRange): + { if (parentObject is Item item) { value = item.SoundRange; return true; } } + break; case nameof(Character.SpeedMultiplier): { if (parentObject is Character character) { value = character.SpeedMultiplier; return true; } } break; @@ -631,6 +650,9 @@ private bool TryGetFloatValueWithoutReflection(object parentObject, out float va case nameof(Character.LowPassMultiplier): { if (parentObject is Character character) { value = character.LowPassMultiplier; return true; } } break; + case nameof(Character.ObstructVisionAmount): + { if (parentObject is Character character) { value = character.ObstructVisionAmount; return true; } } + break; case nameof(Character.HullOxygenPercentage): { if (parentObject is Character character) @@ -666,12 +688,21 @@ private bool TryGetBoolValueWithoutReflection(object parentObject, out bool valu case nameof(PowerTransfer.Overload): if (parentObject is PowerTransfer powerTransfer) { value = powerTransfer.Overload; return true; } break; + case nameof(PowerContainer.OutputDisabled): + if (parentObject is PowerContainer powerContainer) { value = powerContainer.OutputDisabled; return true; } + break; case nameof(MotionSensor.MotionDetected): if (parentObject is MotionSensor motionSensor) { value = motionSensor.MotionDetected; return true; } break; case nameof(Character.IsDead): { if (parentObject is Character character) { value = character.IsDead; return true; } } break; + case nameof(Character.NeedsAir): + { if (parentObject is Character character) { value = character.NeedsAir; return true; } } + break; + case nameof(Character.NeedsOxygen): + { if (parentObject is Character character) { value = character.NeedsOxygen; return true; } } + break; case nameof(Character.IsHuman): { if (parentObject is Character character) { value = character.IsHuman; return true; } } break; @@ -695,6 +726,9 @@ private bool TryGetBoolValueWithoutReflection(object parentObject, out bool valu case nameof(Controller.State): if (parentObject is Controller controller) { value = controller.State; return true; } break; + case nameof(Holdable.Attached): + if (parentObject is Holdable holdable) { value = holdable.Attached; return true; } + break; case nameof(Character.InWater): { if (parentObject is Character character) @@ -779,6 +813,12 @@ private bool TrySetFloatValueWithoutReflection(object parentObject, float value) case nameof(Item.Scale): { if (parentObject is Item item) { item.Scale = value; return true; } } break; + case nameof(Item.SightRange): + { if (parentObject is Item item) { item.SightRange = value; return true; } } + break; + case nameof(Item.SoundRange): + { if (parentObject is Item item) { item.SoundRange = value; return true; } } + break; } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 77d6bb96be..dd727145f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -736,9 +736,9 @@ public static Range GetAttributeRange(this XElement element, string name, R public static string ElementInnerText(this XElement el) { StringBuilder str = new StringBuilder(); - foreach (XNode element in el.DescendantNodes().Where(x => x.NodeType == XmlNodeType.Text)) + foreach (XText textNode in el.DescendantNodes().OfType()) { - str.Append(element.ToString()); + str.Append(textNode.Value); } return str.ToString(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index ce18a13900..0a2e707216 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -188,6 +188,7 @@ public static GraphicsSettings GetDefault() { GraphicsSettings gfxSettings = new GraphicsSettings { + Display = 0, RadialDistortion = true, InventoryScale = 1.0f, LightMapScale = 1.0f, @@ -218,6 +219,7 @@ public static GraphicsSettings FromElements(IEnumerable elements, in G return retVal; } + public int Display; public int Width; public int Height; public bool VSync; @@ -308,6 +310,7 @@ public struct KeyMapping new Dictionary() { { InputType.Run, Keys.LeftShift }, + { InputType.ToggleRun, Keys.None }, { InputType.Attack, Keys.R }, { InputType.Crouch, Keys.LeftControl }, { InputType.Grab, Keys.G }, @@ -566,7 +569,8 @@ public static void SetCurrentConfig(in Config newConfig) bool setGraphicsMode = resolutionChanged || currentConfig.Graphics.VSync != newConfig.Graphics.VSync || - currentConfig.Graphics.DisplayMode != newConfig.Graphics.DisplayMode; + currentConfig.Graphics.DisplayMode != newConfig.Graphics.DisplayMode || + currentConfig.Graphics.Display != newConfig.Graphics.Display; #if CLIENT bool keybindsChanged = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 84a77ba6a3..316f52f74f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -656,6 +656,16 @@ public IEnumerable Explosions private readonly List spawnItems; + + /// + /// If set, the character targeted by this effect will send the corresponding localized text that has the specified identifier in the chat. + /// + private readonly Identifier forceSayIdentifier = Identifier.Empty; + /// + /// If set to true, the character targeted by this effect's "forcesay" command will send their message in the radio. + /// + private readonly bool forceSayInRadio; + /// /// If enabled, one of the items this effect is configured to spawn is selected randomly, as opposed to spawning all of them. /// @@ -1183,6 +1193,10 @@ protected StatusEffect(ContentXElement element, string parentDebugName) animationsToTrigger ??= new List(); animationsToTrigger.Add(new AnimLoadInfo(animType, file, priority, expectedSpeciesNames.ToImmutableArray())); + break; + case "forcesay": + forceSayIdentifier = subElement.GetAttributeIdentifier("message", Identifier.Empty); + forceSayInRadio = subElement.GetAttributeBool("sayinradio", false); break; } } @@ -1948,6 +1962,27 @@ protected void Apply(float deltaTime, Entity entity, IReadOnlyList(); + + CharacterTeamType? inheritedTeam = null; + if (characterSpawnInfo.InheritTeam) + { + bool isPvP = GameMain.GameSession?.GameMode?.Preset == GameModePreset.PvP; + inheritedTeam = entity switch + { + Character c => c.TeamID, + Item it => it.GetRootInventoryOwner() is Character owner ? owner.TeamID : GetTeamFromSubmarine(it), + MapEntity e => GetTeamFromSubmarine(e), + _ => null + // Default to Team1, when we can't deduce the team (for example when spawning outside the sub AND character inventory). + } ?? (isPvP ? CharacterTeamType.None : CharacterTeamType.Team1); + + CharacterTeamType? GetTeamFromSubmarine(MapEntity e) + { + if (e.Submarine == null) { return null; } + // Don't allow team FriendlyNPC in outposts, because if you buy a spawner item (such as husk container) from the store and choose to get it immediately, it will be spawned in the outpost. + return !isPvP && e.Submarine.Info.IsOutpost && e.Submarine.TeamID == CharacterTeamType.FriendlyNPC ? + CharacterTeamType.Team1 : e.Submarine.TeamID; + } + } + for (int i = 0; i < characterSpawnInfo.Count; i++) { Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, onSpawn: newCharacter => { - if (characterSpawnInfo.InheritTeam) + if (inheritedTeam.HasValue) { - newCharacter.TeamID = entity switch - { - Character c => c.TeamID, - Item it => it.GetRootInventoryOwner() is Character owner ? owner.TeamID : it.Submarine?.TeamID ?? newCharacter.TeamID, - MapEntity e => e.Submarine?.TeamID ?? newCharacter.TeamID, - _ => newCharacter.TeamID - }; + newCharacter.SetOriginalTeamAndChangeTeam(inheritedTeam.Value, processImmediately: true); } if (characterSpawnInfo.TotalMaxCount > 0) { @@ -2429,16 +2481,14 @@ void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo, Entity entity, PhysicsBody sou { ignoredBodies = user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(); } - float damageMultiplier = 1f; - - if (sourceEntity is Limb attackLimb) + if (entity is Character character && sourceEntity is Limb { attack: Attack attack }) { - damageMultiplier = attackLimb.attack?.DamageMultiplier ?? 1.0f; + attack.ResetDamageMultiplier(); + attack.DamageMultiplier *= 1.0f + character.GetStatValue(StatTypes.NaturalRangedAttackMultiplier); + damageMultiplier = attack.DamageMultiplier; } - - projectile.Shoot(user, spawnPos, spawnPos, rotation, - ignoredBodies: ignoredBodies, createNetworkEvent: true, damageMultiplier: damageMultiplier); + projectile.Shoot(user, spawnPos, spawnPos, rotation, ignoredBodies: ignoredBodies, createNetworkEvent: true, damageMultiplier: damageMultiplier); projectile.Item.Submarine = projectile.LaunchSub = sourceEntity?.Submarine; } else if (newItem.body != null) @@ -2794,18 +2844,21 @@ private void RegisterTreatmentResults(Character user, Item item, Limb limb, Affl if (limb == null) { return; } foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions()) { - if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && + if (result.Afflictions != null && + /* "affliction" is the affliction directly defined in the status effect (e.g. "5 internal damage (per second / per frame / however the effect is defined to run)"), + * "result" is how much we actually applied of that affliction right now (taking into account the elapsed time, resistances and such) */ + result.Afflictions.FirstOrDefault(a => a.Prefab == limbAffliction.Prefab) is Affliction resultAffliction && (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb)) { if (type == ActionType.OnUse || type == ActionType.OnSuccess) { limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; - limb.character.TryAdjustHealerSkill(user, affliction: affliction); + limb.character.TryAdjustHealerSkill(user, affliction: resultAffliction); } else if (type == ActionType.OnFailure) { limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime; - limb.character.TryAdjustHealerSkill(user, affliction: affliction); + limb.character.TryAdjustHealerSkill(user, affliction: resultAffliction); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index 6467d4e54e..18f992415d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -230,6 +230,18 @@ public static bool TryGetUnlockedAchievements(out List achievements) + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) + { + achievements = null; + return false; + } + achievements = Steamworks.SteamUserStats.Achievements.ToList(); + return true; + } + public static void Update(float deltaTime) { //this should be run even if SteamManager is uninitialized diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index d78b151319..147e7256e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Barotrauma.IO; using Barotrauma.Extensions; @@ -87,13 +87,34 @@ private static readonly ImmutableDictionary SpeciallyHandledCategoriesCache = new Dictionary(); + public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(LocalizedString text) => GetSpeciallyHandledCategories(text.Value); - + public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(string text) { if (string.IsNullOrEmpty(text)) { return SpeciallyHandledCharCategory.None; } + if (SpeciallyHandledCategoriesCache.TryGetValue(text, out var cachedCategory)) + { + SpeciallyHandledCategoriesCache[text] = new CachedCategory(cachedCategory.Category); + return cachedCategory.Category; + } + var retVal = SpeciallyHandledCharCategory.None; for (int i = 0; i < text.Length; i++) { @@ -101,7 +122,7 @@ public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(string foreach (var category in SpeciallyHandledCharCategories) { - if (retVal.HasFlag(category)) { continue; } + if (retVal.AlreadyHasCategoryFlag(category)) { continue; } for (int j = 0; j < SpeciallyHandledCharacterRanges[category].Length; j++) { @@ -126,17 +147,42 @@ public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(string // Input contains characters from all // specially handled categories, there's // no need to inspect the string further - return SpeciallyHandledCharCategory.All; + break; } } + SpeciallyHandledCategoriesCache[text] = new CachedCategory(retVal); + TrimSpeciallyHandledCategoriesCache(); return retVal; } + private static void TrimSpeciallyHandledCategoriesCache() + { + if (SpeciallyHandledCategoriesCache.Count > SpeciallyHandledCategoriesCacheSize) + { + //drop half of the cache, starting from the strings that haven't been used in the longest time + + //this is relatively expensive (instantiates a big new list), + //but the cache should get cleared so infrequently (when 5000 unique texts have been visible, which may not even happen in normal gameplay) + //it should not have much effect in practice + foreach (var cachedVal in SpeciallyHandledCategoriesCache.OrderBy(static c => c.Value.LastAccessed).Take(SpeciallyHandledCategoriesCacheSize / 2).ToList()) + { + SpeciallyHandledCategoriesCache.Remove(cachedVal.Key); + } + } + } + public static bool IsCJK(LocalizedString text) => IsCJK(text.Value); public static bool IsCJK(string text) - => GetSpeciallyHandledCategories(text).HasFlag(SpeciallyHandledCharCategory.CJK); + => GetSpeciallyHandledCategories(text).AlreadyHasCategoryFlag(SpeciallyHandledCharCategory.CJK); + + // This is a local optimized version of HasFlag/HasAnyFlag, which makes sense here because the loop using this is big enough + // to have made 8GB worth of memory allocations with HasFlag and still several dozen MB with the generic HasAnyFlag. + private static bool AlreadyHasCategoryFlag(this SpeciallyHandledCharCategory existingFlags, SpeciallyHandledCharCategory categoryFlag) + { + return (existingFlags & categoryFlag) != 0; + } /// /// Check if the currently selected language is available, and switch to English if not diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 96199e0594..8d2f3b49ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -11,6 +11,7 @@ using Microsoft.Xna.Framework; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using Barotrauma.Networking; namespace Barotrauma { @@ -149,17 +150,23 @@ public static void SaveGame(CampaignDataPath filePath, bool isSavingOnLoading = } catch (Exception e) { - DebugConsole.ThrowError("Failed to clear folder", e); + LogErrorAndSendToClients("Failed to clear folder", e); return; } try { GameMain.GameSession.Save(Path.Combine(TempPath, GameSessionFileName), isSavingOnLoading); + + if (!isSavingOnLoading) + { + // Reset the campaign data path, since if we had a different load path, it would be invalid now + GameMain.GameSession.DataPath = CampaignDataPath.CreateRegular(filePath.SavePath); + } } catch (Exception e) { - DebugConsole.ThrowError("Error saving gamesession", e); + LogErrorAndSendToClients("Error saving gamesession", e); return; } @@ -192,7 +199,7 @@ public static void SaveGame(CampaignDataPath filePath, bool isSavingOnLoading = } catch (Exception e) { - DebugConsole.ThrowError("Error saving submarine", e); + LogErrorAndSendToClients("Error saving submarine", e); return; } @@ -202,7 +209,21 @@ public static void SaveGame(CampaignDataPath filePath, bool isSavingOnLoading = } catch (Exception e) { - DebugConsole.ThrowError("Error compressing save file", e); + LogErrorAndSendToClients("Error compressing save file", e); + } + + void LogErrorAndSendToClients(string errorMsg, Exception e) + { + DebugConsole.ThrowError(errorMsg, e); +#if SERVER + if (GameMain.Server != null) + { + foreach (var client in GameMain.Server.ConnectedClients) + { + GameMain.Server.SendDirectChatMessage(errorMsg + '\n' + e.StackTrace.CleanupStackTrace(), client, ChatMessageType.Error); + } + } +#endif } } @@ -378,6 +399,7 @@ public static string GetSaveFolder(SaveType saveType) FilePath: file, SaveTime: Option.None, SubmarineName: "", + RespawnMode: RespawnMode.None, EnabledContentPackageNames: ImmutableArray.Empty)); } else @@ -413,6 +435,7 @@ public static string GetSaveFolder(SaveType saveType) FilePath: file, SaveTime: docRoot.GetAttributeDateTime("savetime"), SubmarineName: docRoot.GetAttributeStringUnrestricted("submarine", ""), + RespawnMode: docRoot.GetAttributeEnum("respawnmode", RespawnMode.None), EnabledContentPackageNames: enabledContentPackageNames.ToImmutableArray())); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index f5a75b51c8..a963760478 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -54,6 +54,13 @@ internal enum LineType static partial class ToolBox { + /// + /// Returns the Barotrauma.dll assembly. + /// Used with + /// + public static Assembly BarotraumaAssembly + => Assembly.GetAssembly(typeof(GameMain)); + 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 39ff218e00..965237fcb7 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,4 +1,164 @@ ------------------------------------------------------------------------------------------------------------------------------------------------- +v1.7.7.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed damage modifiers not working on creature variants (meaning that e.g. mudraptor hatchlings and veterans did the same amount of damage as normal mudraptors). +- Fixed crew list appearing empty when freecaming in single player. +- Fixed alien coils blocking the boss at the end of the campaign. +- Fixed a waypoint issue in Camel. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.7.6.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Changes: +- Added a "toggle run" input (so you no longer have to keep holding shift to run). Not bound to anything by default, but can be set in the settings menu. +- Improvements to environment visuals: the inside of level walls fades to black to make the texture tiling less obvious when zoomed far out, improvements to the shapes of the level walls (more natural shapes, less long and straight wall segments). +- Ruin scan missions show a progress bar indicating the status of the scans. +- Made dry or partially dry rooms more common in wrecks. +- Option to change the sound of the alarm sirens to a claxon horn sound (the "traditional" AWOOGA-AWOOGA submarine alarm sound). +- "giveaffliction" console command now auto-completes identifiers as well as names. +- Reworked Berilia (kudos to WJohnston): layout improvements, added a bottom docking port, more windows, replaced cameras with searchlights, simplified battery setup and increased reactor output, minor visual improvements and a lot of miscellaneous tweaks. +- Reworked Kastrull drone (less ugly, a little sturdier, the ballast can be accessed through a hatch). +- New playstyle banners. +- Added an option in the settings to launch the game on a specific display. +- Option to refresh the available audio devices (both playback and input devices) in the game settings. The game should now also automatically attempt switch to another device if the current one is disconnected. However, whether this can be done automatically can depend on the audio device, driver and the operating system, and in some cases it may be necessary to choose a new device manually from the game settings. +- Made electrician's goggles easier to get (spawns as a part of the sub's initial supplies, can be purchased from outposts, doesn't require fulgurium to fabricate). The goggles are intended to help less experienced engineers get a hang of wiring in particular, so it doesn't make sense for them to be so difficult to get. +- "Safe rooms" are no longer indestructible in beacon stations (felt confusing, especially when it wasn't apparent from the look of the walls). +- One new research outpost events to foreshadow the longer ruin event chain. + +Optimization: +- Rendering optimizations that should give a major performance boost to situations in which there's lots of structures visible. +- Changes to the physics engine to improve collision detection performance. In technical terms, the game now disregards potential collisions between things that can never collide (such as two characters and most items) much earlier in the collision detection logic. +- Significant optimizations to memory usage. These will mainly reduce load times and lag spikes as the game needs to do less cleanup to free memory that's no longer needed by the game. +- Optimized character ragdolls (building them out of simpler collision shapes). +- Optimized LevelTriggers (things in the level that can react to objects and apply effects to them, such as water currents). +- Optimized the logic for refreshing the positions of items that are inside a moving inventory (such as a character's inventory). +- Miscellaneous optimizations to things such as explosions, AI logic when a bot is operating a turret, psychosis effects, idling NPCs' AI. +- Text rendering optimizations. +- Optimizations to periscopes, chairs and other items that force the character to a certain pose. +- Optimizations to piezo crystals. +- Optimized lighting in the nightclub module. +- Minor miscellaneous optimizations to the physics logic, such as simplifying math and the way positions of entities are set. + +AI: +- Fixed bandits occasionally failing to power up the beacon station they reside in. +- Fixed non-humans enemies not firing ranged weapons if there's a dropped riot shield (or other "blocking" item) in the way. +- Fixed bots sometimes getting stuck walking towards a door if the button/mechanism for opening the door is somewhere far away the bot can't reach. +- Fixed bots and husks being unable to enter Barsuk, Camel and Remora through the airlock. +- Fixed bots being unable to enter through Camel's bottom hatch. +- Fixed bots spotting characters that should be indetectable by AI (e.g. camouflaged Mantises). +- Fixed some bot dialog using the "technical" names (e.g. Mudraptor_passive) of characters instead of the proper display name. + +Multiplayer/networking fixes: +- Implemented lag compensation for hitscan weapons. Should make cases of shots seemingly landing client-side just for the health to rubberband back shortly after significantly less likely. The issue had to do with the lack of lag compensation by the game: a client might fire exactly at a monster, but when the server found out about that shot some fraction of a second later, the monster might've moved and the server would consider the shot to have missed. Now the server takes the latency into account, and checks if the shot would've hit at the point when the client fired the gun. The maximum amount of lag compensation is currently 150 milliseconds, but this can be changed in the server config with the setting "MaxLagCompensation". +- Improvements to syncing character positions (further reducing issues such as shots missing): fixed a bug that caused minor inaccuraries in character velocities between the server and clients, and made the clients better at extrapolating positions while waiting for the next positional update from the server. +- Fixes to syncing ragdolled characters' positions. Should fix corpses and unconscious characters often getting stuck in a glitchy state around platforms, rubberbanding up and down. +- Fixed inability to disable Steam authentication for LAN play if the server is connected to Steam. +- Fixed clients sometimes choosing an incorrect biome in the sandbox, mission and PvP modes, leading to a "level equality check failed" error at the start of the round. +- Fixed character occasionally teleporting out of the sub for a split-second in multiplayer, particularly when the sub was travelling fast during bad networking conditions. +- Fixed occasional crashes with an error message about "SpamFilter.IsFiltered" after getting disconnected from a server and kicked back to the server list. +- Fixed "herpes threshold" server setting doing nothing (should determine how low a player's karma needs to drop for them to contract space herpes). +- Fixed networking errors when multiple clients have died during a permadeath round, and you attempt to continue to the next level. +- Fixed loot that can only go in specific kinds of inventory slots (e.g. mudraptor shell) failing to spawn in the monsters' inventories if they don't have slots of the correct type. +- Fixed occasional "entity event data exceeds the size of the buffer" errors. These happened when the server failed to write a network event for some item (in which case the server should've just skipped that event), which can be caused by issues in mods for example. +- Fixed other clients not seeing creature attacks if the creature is being controlled by a player. +- Fix to clients often getting stuck behind closing doors in MP despite the character seeming to make it through in time client-side. +- Fixed server lobby sometimes appearing empty/nonfunctional when clients join a server. In technical terms, this happened when the lobby's update ID had reached a very large value, which could happen if the server has run for a very long time or if there's been some issues that have caused the server to create a very large number of lobby updates. +- Fixed PvP outpost setting causing unnecessary server lobby updates (even when the PvP mode was not selected), potentially leading to the lobby becoming unusable due to the issue described above. +- Fixed flipping monsters behaving inconsistently in multiplayer when controlling a monster: other clients saw the monster facing in the position of the cursor, even if it hadn't turned at all at the end of the client controlling it. +- Fixed "ignore" and "unignore" orders not working on stacks of items in multiplayer (just marking the first item in the stack). +- Fixed components inside a circuit box losing their settings when saving the game while the box is in a player's inventory. +- Fixed moving an item UI potentially moving it outside the screen on other clients' screens when using different resolutions. +- Fixed clients syncing the selected outpost PvP setting unnecessarily to the server when a non-PvP mode was changed. When there were no outposts to select from, the clients would think the setting has changed (since the selected value of "nothing" doesn't match the server setting), and send the value. This could potentially have caused the reported issue of multiple admins constantly changing settings and messing up the lobby. +- Fixed clients sending the value of the win condition slider too eagerly (whenever it moves, as opposed to when it's released). Also changed it adjust in steps of 10 for a little nicer values. +- Fixed occasional "Unexpected error" console errors when the game attempts to unlock an achiement on the Epic Store or with EOS crossplay enabled. + +Extra logging for diagnosing networking errors: +- Mention the entity that caused an error to be thrown when reading a network event, and the content package that entity is from (makes it easier to tell when the error is caused by some specific mod). +- Improved logging of the "event count very high" errors: mention the content package the events are from if they're non-vanilla, log the errors client-side too. We are suspecting the occasional "expected old/removed event" disconnects could be due to the server creating so many events the clients can't keep up, and this should give us some more clues for diagnosing the issue. +- Added more info to the "component event creation failed" error messages, the errors are logged into GameAnalytics. + +Balance: +- Each successive use of the Mindwipe item increases the penalty to talent points, making it more costly to use it to "farm talents" by repeatedly unlocking talents that unlock other talents or give extra talent points. +- XP gain balancing: the XP is no longer directly tied to mission rewards, but adjusted based on the difficulty of the mission. +- Lowered cost of high-skilled NPCs to hire. High reputation with Coalition or Separatists results in more cost-effective hires. +- Tweaks to monster nest missions: higher reward (as they're some of the riskiest missions in the game) and less monsters in SP (in MP it's the same amount). +- Campaign setting for adjusting the XP gain rates. +- Higher XP gains in later biomes. +- Concussions wears off faster. +- Beds are a bit more effective for some afflictions like nausea and drunkness. +- Added chem addiction and chem withdrawal reduction to beds/bunks. +- Reduced how fast chem addiction and chem withdrawal build up. +- Reduced the amount of devices you need to repair to get the effects of the "Machine Maniac" talent. +- Health scanners spawn as a part of the sub's initial supplies, not given to every respawning medic (a too easy source of free resources). +- Deconstructing headsets no longer yields materials (a too easy source of free resources). + +Fixes: +- Fixed choosing "retry" in the pause menu after you've started playing from a backup save loading that same backup save instead of the most recent save. +- Fixed relay components not passing power until they've been toggled on and off. +- Fixed ability to sell the 2nd gene of a combined genetic material in stores. +- Fixed combined genes appearing untainted if the 2nd gene is tainted instead of the 1st one. +- Fixed NPCs still offering services (stores, submarine upgrades, etc) despite their faction being hostile to you, leading to weird situations where you could be trading with a merchant who's actively trying to run away from you. +- Fixed Artie Dolittle continuing to follow you if you refuse to hire him. +- Fixed "Miracle Worker" talent keeping husks from dying if you're friendly with husks (e.g. because you're wearing Zealot Robes). +- Fixed alien turrets not working properly in mirrored ruins (they were using circuits with hard-coded turret rotation angles, which broke in mirrored ruins). +- Fixed alien turrets set to auto-operate failing to fire inside hulls. +- Fixed auto-operated turrets trying to fire at monsters inside ruins. +- Fixed signal components being automatically placed inside circuit boxes in your inventory when you purchase them while there's no free slots in your inventory. +- Fixed holes on overlapping walls multiplying flooding rates (a hole was created on all of the walls, and they'd act independently of each other). +- Fixed lights being visible on contained items (e.g. handheld sonars in cabinets) in the sub editor. +- Fixed crashing when attempting to give contextual orders to a defense bot (i.e. when middle-mouse-clicking a defense bot). +- Reactors attempt to rapidly adjust to the load in the first 5 seconds of a round, during which time junction boxes don't take damage from overvoltage. Intended to address overvoltage in cases where the reactor is outputting a lot of power, and the load suddenly drops when a new round starts (e.g. due to the engines powering down). +- Fixed alien devices (or more specifically, items with a static physics body) shifting from their original position when loaded for the first time if they're used in a normal sub. +- Fixed "Crusty Seaman" talent giving characters medical skill as it heals the character's bleeding. +- Fixed certain level resources (e.g. piezo crystals) facing the wrong way on level walls. +- Fixed certain elements in the debugdraw view (submarine borders, damage texts on walls, gaps, water level indicators on hulls, lines pointing to walls enemies are targeting) "twitching" when the sub moves. +- Fixed headsets purchased with immediate item delivery getting assigned to an incorrect team, preventing them from communicating with the headsets of the rest of the crew. +- Fixed headsets whose fabrication was started on the previous round getting assigned to an incorrect team, preventing them from communicating with the headsets of the rest of the crew. +- Fixed pets (including defense bots) spawned in an outpost being assigned to the "friendly NPC" team, which would e.g. mean that they wouldn't attack the enemy team in the PvP mode. +- Fixed PvP outposts sometimes spawning as alien ruins. +- Fixed multi-tools detaching detonators (the multi-tool is not intended to detach any items, because that would conflict with the repair functionality). +- Fixed unopenable hatch at the top of ResidentialModule01_Colony (rarely caused any issues in the vanilla game, because other modules almost never attach on top of that module). +- Fixed Terminal component's Readonly field not working when it's set in the editor (only if the item is by default set to readonly). +- Fixed large hardpoints not having the "set_auto_operate" and "toggle_auto_operate" inputs. +- Fixed components being attachable inside walls/floors in some subs. +- Fixed broken junction boxes sometimes zapping players despite seemingly not being powered. +- Improved thalamus brain spawning to prevent it from ending up in dry rooms, and dying if it does: + - Avoid spawning in rooms with doors, hatches or duct blocks. + - The brain no longer dies in dry rooms. +- Fixed mudraptor beak from mudraptor genes showing through exosuits and other wearable items that should hide the character's head. +- Fixed bandoliers not going in cabinets' "container slots". +- Fixed "man and his raptor" mission failing if you don't speak to the man and spawn the mudraptor. +- Fixed wrecked doors and hatches not being weldable. +- Fixed the hover text saying that you can "open" or "force open" a door when it's already opened. +- Waypoint fixes in Camel. +- Fixed 'Traveling Tradesman' sell bonus not working. +- Fixed chat-linked wifi components sometimes failing to receive messages from the chat (most often in circuit boxes?). Had to do with the order in which the headsets and wifi components are created. +- Fixed conversation prompts not blocking conversations from other events if you've moved past the initial prompt (e.g. answered the first question and gotten a follow-up). +- Fixed treatment suggestions not being shown on some afflictions (e.g opiate addiction). Happened because we used the same thresholds to determine if bots should treat the affliction and to determine if the suggestion should be shown. + +Modding: +- Added AddScoreAction: can be used to make scripted events modify a team's score in the PvP mode, which should open up a lot of new possibilities for custom PvP mission types. +- Added RangedAttackMultiplier stat type. +- Beacon stations are no longer automatically damaged, but instead are only damaged through the DamageBeaconStationAction in ScriptedEvent. Recommended (and default) setting for beacon stations is to enable all three: Allow damaged walls, Allow damaged devices, Allow disconnected wires. +- Fixed ragdolls failing to load when inheriting creatures whose ragdolls are not in the default path ("Ragdolls" folder inside the character's folder), but e.g. defined using a direct path to a ragdoll file in the character's or some other character's folder. +- Added option to set the handle positions (Handle1 and Handle2) of a holdable item using status effects. +- Fixed character variants failing to load the correct texture for limbs that use a different texture than the rest of the ragdoll (e.g. the alien bits on a variant of the Cyborgworm). +- Fixed WaitForItemUsedAction not working if there's multiple instances of the same scripted event active at the same time. +- Fixed characters' damage overlays not being affected by the ragdoll's texture scale. +- Fixed stacking ability resistances past 100% making the resistance negative. +- Fixed texts with color tags not working in some UI elements: fabricator, sub editor, store, speech bubbles. +- Made the game load the vanilla human ragdoll (or in the case of monsters, crawler ragdoll) if loading a modded character's ragdoll fails. Should make it easier to diagnose and address issues in the character configuration. +- Fixed hair and other "wearables" in character portraits (in the bottom-right corner and the health interface) getting misaligned if they use a different origin or sourcerect than the head sprite. +- Fixed health multipliers defined in a HumanPrefab not working. +- Fixed crashes when a PvP outpost contains shuttles or other moving parts. +- If an item variant inherits a sprite without the full texture path from the base item, it uses the texture path of the base item instead of that of the variant. Fixes mods being unable to create variants of things like diving suits without reconfiguring all the sprite paths. +- Fixed character variants overriding the targeting parameters of the parent character incorrectly: just overriding the parameters in the order they're defined in, as opposed to overriding a parameter with a matching tag. +- Status effects can make a character say a line in the chat: used by adding a subelement called forcesay, with the attributes "message" and optionally "sayinradio". +- Fixed OnUse sounds not playing when attaching an item to a wall in MP. + +------------------------------------------------------------------------------------------------------------------------------------------------- v1.6.19.1 ------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/EnumExtensions.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/EnumExtensions.cs index 9d7872e1f9..5ce200614f 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/EnumExtensions.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Extensions/EnumExtensions.cs @@ -1,18 +1,27 @@ using System; using System.Collections.Generic; +// NOTE: We should use struct in addition to Enum in all the type constraints (at the end of the method signatures), as it +// tells the compiler that we're only ever using value types, which enums always are anyway. +// This avoids a lot of allocations caused by the compiler preparing for anything, which in turn happens because despite +// how it works in practice, Enum is counted as a reference type in C#... for historical reasons. + +// We use the (int)(object) cast because generic types can't be cast directly to int, so we box into object and unbox into int instead. +// It avoids some memory allocations that Convert.ToInt32() seems to do. +// NOTE: This should work fine as long as the enum values stay within - to + 2^31, so let's not use uint or long for them. + namespace Barotrauma.Extensions { public static class EnumExtensions { /// - /// Enum.HasFlag() checks if all flags matches. This method checks if any of them matches. + /// Enum.HasFlag() checks if all flags matches. This method checks if any of them matches. It also avoids boxing allocations that the built-in version might still sometimes cause. /// E.g. when myEnum = SomeEnum.First | SomeEnum.Second, myEnum.HasFlag(SomeEnum.First | SomeEnum.Third) returns false, because not all of the flags match, but HasAnyFlag(SomeEnum.First | SomeEnum.Third) returns true, because some of the flags match. /// - public static bool HasAnyFlag(this T type, T value) where T : Enum + public static bool HasAnyFlag(this T type, T value) where T : struct, Enum { - int typeValue = Convert.ToInt32(type); - int flagValue = Convert.ToInt32(value); + int typeValue = (int)(object)type; + int flagValue = (int)(object)value; return (typeValue & flagValue) != 0; } @@ -20,10 +29,10 @@ public static bool HasAnyFlag(this T type, T value) where T : Enum /// Adds a flag value to an enum. /// Note that enums are value types, so you need to use the value returned from this method. /// - public static T AddFlag(this T @enum, T flag) where T : Enum + public static T AddFlag(this T @enum, T flag) where T : struct, Enum { - int enumValue = Convert.ToInt32(@enum); - int flagValue = Convert.ToInt32(flag); + int enumValue = (int)(object)@enum; + int flagValue = (int)(object)flag; return (T)(object)(enumValue | flagValue); } @@ -31,18 +40,18 @@ public static T AddFlag(this T @enum, T flag) where T : Enum /// Removes a flag value from an enum. /// Note that enums are value types, so you need to use the value returned from this method. /// - public static T RemoveFlag(this T @enum, T flag) where T : Enum + public static T RemoveFlag(this T @enum, T flag) where T : struct, Enum { - int enumValue = Convert.ToInt32(@enum); - int flagValue = Convert.ToInt32(flag); + int enumValue = (int)(object)@enum; + int flagValue = (int)(object)flag; return (T)(object)(enumValue & ~flagValue); } - public static IEnumerable GetIndividualFlags(T flagsEnum) where T : Enum + public static IEnumerable GetIndividualFlags(T flagsEnum) where T : struct, Enum { foreach (T value in Enum.GetValues(typeof(T))) { - if (flagsEnum.HasFlag(value)) { yield return value; } + if (flagsEnum.HasAnyFlag(value)) { yield return value; } } } } diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/MathUtils.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/MathUtils.cs index 357a3335a1..f2340cb031 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/MathUtils.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/MathUtils.cs @@ -678,7 +678,7 @@ public static Vector2[] GetPointsOnCircumference(Vector2 center, float radius, i } /// - /// divide a convex hull into triangles + /// Divide a convex hull into triangles /// /// List of triangle vertices (sorted counter-clockwise) public static List TriangulateConvexHull(List vertices, Vector2 center) @@ -1156,4 +1156,5 @@ public static int Compare(Vector2 a, Vector2 b, Vector2 center) return -CompareCW.Compare(a, b, center); } } + } diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs index 1dcc8e4b4f..e3c88f2e3e 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs @@ -113,10 +113,9 @@ public static string NameWithGenerics(this Type t) /// Gets a type by its name, with backwards compatibility for types that have been renamed. /// /// - public static Type? GetTypeWithBackwardsCompatibility(string nameSpace, string typeName, bool throwOnError, bool ignoreCase) + public static Type? GetTypeWithBackwardsCompatibility(Assembly assembly, string nameSpace, string typeName, bool throwOnError, bool ignoreCase) { - if (Assembly.GetEntryAssembly() is not { } entryAssembly) { return null; } - var types = entryAssembly + var types = assembly .GetTypes() .Where(t => NameMatches(t.Namespace, nameSpace, ignoreCase)); diff --git a/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs b/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs index 7b496a52aa..fac9982bde 100644 --- a/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs +++ b/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs @@ -53,7 +53,7 @@ internal struct TreeNode internal int Height; internal int ParentOrNext; - public object Body; + public Body Body; internal T UserData; @@ -319,7 +319,7 @@ public AABB GetFatAABB(int proxyId) return _nodes[proxyId].AABB; } - public object GetBody(int proxyId) + public Body GetBody(int proxyId) { Debug.Assert(0 <= proxyId && proxyId < _nodeCapacity); return _nodes[proxyId].Body; @@ -344,7 +344,7 @@ public bool TestFatAABBOverlap(int proxyIdA, int proxyIdB) /// /// The callback. /// The aabb. - public void Query(Func callback, ref AABB aabb, ref object body) + public void Query(Func callback, ref AABB aabb, ref Body body) { _queryStack.Clear(); _queryStack.Push(_root); @@ -357,22 +357,38 @@ public void Query(Func callback, ref AABB aabb, ref object body) continue; } - //TreeNode* node = &_nodes[nodeId]; - if (!ReferenceEquals(_nodes[nodeId].Body, body) && AABB.TestOverlap(ref _nodes[nodeId].AABB, ref aabb)) + TreeNode node = _nodes[nodeId]; + if (node.Body != body && AABB.TestOverlap(ref node.AABB, ref aabb)) { - if (_nodes[nodeId].IsLeaf()) + if (node.IsLeaf()) { + if (node.Body.CollidesWithMatchesBetweenFixtures && + body.CollisionCategoriesMatchBetweenFixtures) + { + //equivalent to + //collide = node.Body.CollidesWith.HasAnyFlag(body.CollisionCategories) && body.CollidesWith.HasAnyFlag(node.Body.CollisionCategories) + //same check as in ContactManager.ShouldCollide + //inlined here using binary operations because this is performance critical code + bool collide = + (node.Body.CollidesWith & body.CollisionCategories) != 0 && + (body.CollidesWith & node.Body.CollisionCategories) != 0; + if (!collide) + { + continue; + } + } bool proceed = callback(nodeId); if (proceed == false) { return; } + } else { - _queryStack.Push(_nodes[nodeId].Child1); - _queryStack.Push(_nodes[nodeId].Child2); - } + _queryStack.Push(node.Child1); + _queryStack.Push(node.Child2); + } } } } diff --git a/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTreeBroadPhase.cs b/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTreeBroadPhase.cs index 9bdbe10736..d71fd04015 100644 --- a/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTreeBroadPhase.cs +++ b/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTreeBroadPhase.cs @@ -263,7 +263,7 @@ public void UpdatePairs(BroadphaseDelegate callback) // we don't fail to create a pair that may touch later. AABB fatAABB = _tree.GetFatAABB(_queryProxyId); - object body = _tree.GetBody(_queryProxyId); + Body body = _tree.GetBody(_queryProxyId); // Query tree, create pairs and add them pair buffer. _tree.Query(_queryCallback, ref fatAABB, ref body); diff --git a/Libraries/Farseer Physics Engine 3.5/Collision/Shapes/CircleShape.cs b/Libraries/Farseer Physics Engine 3.5/Collision/Shapes/CircleShape.cs index 987f25b775..d90463a1f5 100644 --- a/Libraries/Farseer Physics Engine 3.5/Collision/Shapes/CircleShape.cs +++ b/Libraries/Farseer Physics Engine 3.5/Collision/Shapes/CircleShape.cs @@ -115,7 +115,7 @@ public override bool RayCast(out RayCastOutput output, ref RayCastInput input, r } // Find the point of intersection of the line with the circle. - float a = -(c + (float)Math.Sqrt(sigma)); + float a = -(c + MathF.Sqrt(sigma)); // Is the intersection point on the segment? if (0.0f <= a && a <= input.MaxFraction * rr) @@ -177,8 +177,8 @@ public override float ComputeSubmergedArea(ref Vector2 normal, float offset, ref //Magic float l2 = l * l; - float area = _2radius * (float)((Math.Asin(l / Radius) + MathHelper.Pi / 2) + l * Math.Sqrt(_2radius - l2)); - float com = -2.0f / 3.0f * (float)Math.Pow(_2radius - l2, 1.5f) / area; + float area = _2radius * ((MathF.Asin(l / Radius) + MathHelper.Pi / 2) + l * MathF.Sqrt(_2radius - l2)); + float com = -2.0f / 3.0f * MathF.Pow(_2radius - l2, 1.5f) / area; sc.X = p.X + normal.X * com; sc.Y = p.Y + normal.Y * com; diff --git a/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/Seidel/MonotoneMountain.cs b/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/Seidel/MonotoneMountain.cs index 3166856177..6abe954675 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/Seidel/MonotoneMountain.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/Seidel/MonotoneMountain.cs @@ -150,7 +150,7 @@ private float Angle(Point p) { Point a = (p.Next - p); Point b = (p.Prev - p); - return (float)Math.Atan2(a.Cross(b), a.Dot(b)); + return MathF.Atan2(a.Cross(b), a.Dot(b)); } private bool AngleSign() diff --git a/Libraries/Farseer Physics Engine 3.5/Common/Math.cs b/Libraries/Farseer Physics Engine 3.5/Common/Math.cs index b3f8df8d77..70e2f3e8a0 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/Math.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/Math.cs @@ -709,7 +709,7 @@ public void Advance(float alpha) /// public void Normalize() { - float d = MathHelper.TwoPi * (float)Math.Floor(A0 / MathHelper.TwoPi); + float d = MathHelper.TwoPi * MathF.Floor(A0 / MathHelper.TwoPi); A0 -= d; A -= d; } diff --git a/Libraries/Farseer Physics Engine 3.5/Common/Maths/Complex.cs b/Libraries/Farseer Physics Engine 3.5/Common/Maths/Complex.cs index f6cc992821..ed0cec83b2 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/Maths/Complex.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/Maths/Complex.cs @@ -18,7 +18,7 @@ public struct Complex public float Phase { - get { return (float)Math.Atan2(Imaginary, Real); } + get { return MathF.Atan2(Imaginary, Real); } set { if (value == 0) @@ -26,14 +26,14 @@ public float Phase this = Complex.One; return; } - this.Real = (float)Math.Cos(value); - this.Imaginary = (float)Math.Sin(value); + this.Real = MathF.Cos(value); + this.Imaginary = MathF.Sin(value); } } public float Magnitude { - get { return (float)Math.Round(Math.Sqrt(MagnitudeSquared())); } + get { return MathF.Round(MathF.Sqrt(MagnitudeSquared())); } } @@ -49,8 +49,8 @@ public static Complex FromAngle(float angle) return Complex.One; return new Complex( - (float)Math.Cos(angle), - (float)Math.Sin(angle)); + MathF.Cos(angle), + MathF.Sin(angle)); } public void Conjugate() diff --git a/Libraries/Farseer Physics Engine 3.5/Common/Path.cs b/Libraries/Farseer Physics Engine 3.5/Common/Path.cs index 88b5fe87bf..57c5726f3f 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/Path.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/Path.cs @@ -315,7 +315,7 @@ public List SubdivideEvenly(int divisions) for (int i = 1; i < divisions; i++) { Vector2 normal = GetPositionNormal(t); - float angle = (float)Math.Atan2(normal.Y, normal.X); + float angle = MathF.Atan2(normal.Y, normal.X); verts.Add(new Vector3(end, angle)); diff --git a/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/RealExplosion.cs b/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/RealExplosion.cs index 490d6dd0d7..5f446c4102 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/RealExplosion.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/RealExplosion.cs @@ -1,4 +1,4 @@ -/* Original source Farseer Physics Engine: +/* Original source Farseer Physics Engine: * Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com * Microsoft Permissive License (Ms-PL) v1.1 */ @@ -14,7 +14,7 @@ namespace FarseerPhysics.Common.PhysicsLogic { // Original Code by Steven Lu - see http://www.box2d.org/forum/viewtopic.php?f=3&t=1688 - // Ported to Farseer 3.0 by Nicolás Hormazábal + // Ported to Farseer 3.0 by Nicolás Hormazábal internal struct ShapeData { @@ -185,7 +185,7 @@ public Dictionary Activate(Vector2 pos, float radius, float ma if ((shapes[i].Body.BodyType == BodyType.Dynamic) && ps != null) { Vector2 toCentroid = shapes[i].Body.GetWorldPoint(ps.MassData.Centroid) - pos; - float angleToCentroid = (float)Math.Atan2(toCentroid.Y, toCentroid.X); + float angleToCentroid = MathF.Atan2(toCentroid.Y, toCentroid.X); float min = float.MaxValue; float max = float.MinValue; float minAbsolute = 0.0f; @@ -194,7 +194,7 @@ public Dictionary Activate(Vector2 pos, float radius, float ma for (int j = 0; j < ps.Vertices.Count; ++j) { Vector2 toVertex = (shapes[i].Body.GetWorldPoint(ps.Vertices[j]) - pos); - float newAngle = (float)Math.Atan2(toVertex.Y, toVertex.X); + float newAngle = MathF.Atan2(toVertex.Y, toVertex.X); float diff = (newAngle - angleToCentroid); diff = (diff - MathHelper.Pi) % (2 * MathHelper.Pi); @@ -253,7 +253,7 @@ public Dictionary Activate(Vector2 pos, float radius, float ma midpt = midpt / 2; Vector2 p1 = pos; - Vector2 p2 = radius * new Vector2((float)Math.Cos(midpt), (float)Math.Sin(midpt)) + pos; + Vector2 p2 = radius * new Vector2(MathF.Cos(midpt), MathF.Sin(midpt)) + pos; // RaycastOne bool hitClosest = false; @@ -343,7 +343,7 @@ public Dictionary Activate(Vector2 pos, float radius, float ma j += offset) { Vector2 p1 = pos; - Vector2 p2 = pos + radius * new Vector2((float)Math.Cos(j), (float)Math.Sin(j)); + Vector2 p2 = pos + radius * new Vector2(MathF.Cos(j), MathF.Sin(j)); Vector2 hitpoint = Vector2.Zero; float minlambda = float.MaxValue; @@ -371,7 +371,7 @@ public Dictionary Activate(Vector2 pos, float radius, float ma float impulse = (arclen / (MinRays + insertedRays)) * maxForce * 180.0f / MathHelper.Pi * (1.0f - Math.Min(1.0f, minlambda)); // We Apply the impulse!!! - Vector2 vectImp = Vector2.Dot(impulse * new Vector2((float)Math.Cos(j), (float)Math.Sin(j)), -ro.Normal) * new Vector2((float)Math.Cos(j), (float)Math.Sin(j)); + Vector2 vectImp = Vector2.Dot(impulse * new Vector2(MathF.Cos(j), MathF.Sin(j)), -ro.Normal) * new Vector2(MathF.Cos(j), MathF.Sin(j)); _data[i].Body.ApplyLinearImpulse(ref vectImp, ref hitpoint); // We gather the fixtures for returning them diff --git a/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/SimpleExplosion.cs b/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/SimpleExplosion.cs index 72c7e00752..2e00f10ffd 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/SimpleExplosion.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/PhysicsLogic/SimpleExplosion.cs @@ -70,7 +70,7 @@ private Dictionary ApplyImpulse(Vector2 pos, float radius, float float forcePercent = GetPercent(distance, radius); Vector2 forceVector = pos - overlappingBody.Position; - forceVector *= 1f / (float)Math.Sqrt(forceVector.X * forceVector.X + forceVector.Y * forceVector.Y); + forceVector *= 1f / MathF.Sqrt(forceVector.X * forceVector.X + forceVector.Y * forceVector.Y); forceVector *= MathHelper.Min(force * forcePercent, maxForce); forceVector *= -1; @@ -85,7 +85,7 @@ private Dictionary ApplyImpulse(Vector2 pos, float radius, float private float GetPercent(float distance, float radius) { //(1-(distance/radius))^power-1 - float percent = (float)Math.Pow(1 - ((distance - radius) / radius), Power) - 1; + float percent = MathF.Pow(1 - ((distance - radius) / radius), Power) - 1; if (float.IsNaN(percent)) return 0f; diff --git a/Libraries/Farseer Physics Engine 3.5/Common/PolygonManipulation/SimplifyTools.cs b/Libraries/Farseer Physics Engine 3.5/Common/PolygonManipulation/SimplifyTools.cs index 56d0b682d9..d714a4cee5 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/PolygonManipulation/SimplifyTools.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/PolygonManipulation/SimplifyTools.cs @@ -137,8 +137,8 @@ public static Vertices MergeParallelEdges(Vertices vertices, float tolerance) float dy0 = vertices[middle].Y - vertices[lower].Y; float dx1 = vertices[upper].Y - vertices[middle].X; float dy1 = vertices[upper].Y - vertices[middle].Y; - float norm0 = (float)Math.Sqrt(dx0 * dx0 + dy0 * dy0); - float norm1 = (float)Math.Sqrt(dx1 * dx1 + dy1 * dy1); + float norm0 = MathF.Sqrt(dx0 * dx0 + dy0 * dy0); + float norm1 = MathF.Sqrt(dx1 * dx1 + dy1 * dy1); if (!(norm0 > 0.0f && norm1 > 0.0f) && newNVertices > 3) { diff --git a/Libraries/Farseer Physics Engine 3.5/Common/PolygonTools.cs b/Libraries/Farseer Physics Engine 3.5/Common/PolygonTools.cs index 7c65dd32a1..6952e18f1a 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/PolygonTools.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/PolygonTools.cs @@ -112,8 +112,8 @@ public static Vertices CreateRoundedRectangle(float width, float height, float x phase--; } - vertices.Add(posOffset + new Vector2(xRadius * (float)Math.Cos(stepSize * -(i + phase)), - -yRadius * (float)Math.Sin(stepSize * -(i + phase)))); + vertices.Add(posOffset + new Vector2(xRadius * MathF.Cos(stepSize * -(i + phase)), + -yRadius * MathF.Sin(stepSize * -(i + phase)))); } } @@ -160,8 +160,8 @@ public static Vertices CreateEllipse(float xRadius, float yRadius, int numberOfE vertices.Add(new Vector2(xRadius, 0)); for (int i = numberOfEdges - 1; i > 0; --i) - vertices.Add(new Vector2(xRadius * (float)Math.Cos(stepSize * i), - -yRadius * (float)Math.Sin(stepSize * i))); + vertices.Add(new Vector2(xRadius * MathF.Cos(stepSize * i), + -yRadius * MathF.Sin(stepSize * i))); return vertices; } @@ -177,8 +177,8 @@ public static Vertices CreateArc(float radians, int sides, float radius) float stepSize = radians / sides; for (int i = sides - 1; i > 0; i--) { - vertices.Add(new Vector2(radius * (float)Math.Cos(stepSize * i), - radius * (float)Math.Sin(stepSize * i))); + vertices.Add(new Vector2(radius * MathF.Cos(stepSize * i), + radius * MathF.Sin(stepSize * i))); } return vertices; @@ -252,8 +252,8 @@ public static Vertices CreateCapsule(float height, float topRadius, int topEdges float stepSize = MathHelper.Pi / topEdges; for (int i = 1; i < topEdges; i++) { - vertices.Add(new Vector2(topRadius * (float)Math.Cos(stepSize * i), - topRadius * (float)Math.Sin(stepSize * i) + newHeight)); + vertices.Add(new Vector2(topRadius * MathF.Cos(stepSize * i), + topRadius * MathF.Sin(stepSize * i) + newHeight)); } vertices.Add(new Vector2(-topRadius, newHeight)); @@ -264,8 +264,8 @@ public static Vertices CreateCapsule(float height, float topRadius, int topEdges stepSize = MathHelper.Pi / bottomEdges; for (int i = 1; i < bottomEdges; i++) { - vertices.Add(new Vector2(-bottomRadius * (float)Math.Cos(stepSize * i), - -bottomRadius * (float)Math.Sin(stepSize * i) - newHeight)); + vertices.Add(new Vector2(-bottomRadius * MathF.Cos(stepSize * i), + -bottomRadius * MathF.Sin(stepSize * i) - newHeight)); } vertices.Add(new Vector2(bottomRadius, -newHeight)); @@ -298,24 +298,24 @@ public static Vertices CreateGear(float radius, int numberOfTeeth, float tipPerc { vertices.Add( new Vector2(radius * - (float)Math.Cos(stepSize * i + toothAngleStepSize * 2f + toothTipStepSize), + MathF.Cos(stepSize * i + toothAngleStepSize * 2f + toothTipStepSize), -radius * - (float)Math.Sin(stepSize * i + toothAngleStepSize * 2f + toothTipStepSize))); + MathF.Sin(stepSize * i + toothAngleStepSize * 2f + toothTipStepSize))); vertices.Add( new Vector2((radius + toothHeight) * - (float)Math.Cos(stepSize * i + toothAngleStepSize + toothTipStepSize), + MathF.Cos(stepSize * i + toothAngleStepSize + toothTipStepSize), -(radius + toothHeight) * - (float)Math.Sin(stepSize * i + toothAngleStepSize + toothTipStepSize))); + MathF.Sin(stepSize * i + toothAngleStepSize + toothTipStepSize))); } vertices.Add(new Vector2((radius + toothHeight) * - (float)Math.Cos(stepSize * i + toothAngleStepSize), + MathF.Cos(stepSize * i + toothAngleStepSize), -(radius + toothHeight) * - (float)Math.Sin(stepSize * i + toothAngleStepSize))); + MathF.Sin(stepSize * i + toothAngleStepSize))); - vertices.Add(new Vector2(radius * (float)Math.Cos(stepSize * i), - -radius * (float)Math.Sin(stepSize * i))); + vertices.Add(new Vector2(radius * MathF.Cos(stepSize * i), + -radius * MathF.Sin(stepSize * i))); } return vertices; diff --git a/Libraries/Farseer Physics Engine 3.5/Common/Vertices.cs b/Libraries/Farseer Physics Engine 3.5/Common/Vertices.cs index 7ade504ddb..ca83c470d5 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/Vertices.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/Vertices.cs @@ -287,8 +287,8 @@ public void Rotate(float value) { Debug.Assert(!AttachedToBody, "Rotating vertices that are used by a Body can result in unstable behavior."); - float num1 = (float)Math.Cos(value); - float num2 = (float)Math.Sin(value); + float num1 = MathF.Cos(value); + float num2 = MathF.Sin(value); for (int i = 0; i < Count; i++) { diff --git a/Libraries/Farseer Physics Engine 3.5/Controllers/GravityController.cs b/Libraries/Farseer Physics Engine 3.5/Controllers/GravityController.cs index 728f4cfe22..bbb7352276 100644 --- a/Libraries/Farseer Physics Engine 3.5/Controllers/GravityController.cs +++ b/Libraries/Farseer Physics Engine 3.5/Controllers/GravityController.cs @@ -71,7 +71,7 @@ public override void Update(float dt) f = Strength / r2 * worldBody.Mass * controllerBody.Mass * d; break; case GravityType.Linear: - f = Strength / (float)Math.Sqrt(r2) * worldBody.Mass * controllerBody.Mass * d; + f = Strength / MathF.Sqrt(r2) * worldBody.Mass * controllerBody.Mass * d; break; } @@ -92,7 +92,7 @@ public override void Update(float dt) f = Strength / r2 * worldBody.Mass * d; break; case GravityType.Linear: - f = Strength / (float)Math.Sqrt(r2) * worldBody.Mass * d; + f = Strength / MathF.Sqrt(r2) * worldBody.Mass * d; break; } diff --git a/Libraries/Farseer Physics Engine 3.5/Controllers/VelocityLimitController.cs b/Libraries/Farseer Physics Engine 3.5/Controllers/VelocityLimitController.cs index 6e8d8c5457..a1855a3772 100644 --- a/Libraries/Farseer Physics Engine 3.5/Controllers/VelocityLimitController.cs +++ b/Libraries/Farseer Physics Engine 3.5/Controllers/VelocityLimitController.cs @@ -99,7 +99,7 @@ public override void Update(float dt) if (result > dt * _maxLinearSqared) { - float sq = (float)Math.Sqrt(result); + float sq = MathF.Sqrt(result); float ratio = _maxLinearVelocity / sq; body._linearVelocity.X *= ratio; diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs index 62a8d13c03..81faa01213 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs @@ -31,6 +31,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using FarseerPhysics.Collision; using FarseerPhysics.Collision.Shapes; using FarseerPhysics.Common; @@ -98,7 +99,7 @@ public Body() /// The revolutions. public float Revolutions { - get { return Rotation / (float)Math.PI; } + get { return Rotation / MathF.PI; } } /// @@ -614,6 +615,10 @@ public void Add(Fixture fixture, bool resetMassData = true) fixture.Body = this; this.FixtureList.Add(fixture); + + RefreshCollidesWithMatchesBetweenFixtures(); + RefreshCollisionCategoriesMatchBetweenFixtures(); + #if DEBUG if (fixture.Shape.ShapeType == ShapeType.Polygon) ((PolygonShape)fixture.Shape).Vertices.AttachedToBody = true; @@ -685,6 +690,8 @@ public virtual void Remove(Fixture fixture) fixture.Body = null; FixtureList.Remove(fixture); + RefreshCollidesWithMatchesBetweenFixtures(); + RefreshCollisionCategoriesMatchBetweenFixtures(); #if DEBUG if (fixture.Shape.ShapeType == ShapeType.Polygon) ((PolygonShape)fixture.Shape).Vertices.AttachedToBody = false; @@ -1241,29 +1248,89 @@ public void SetFriction(float friction) FixtureList[i].Friction = friction; } + public bool CollisionCategoriesMatchBetweenFixtures + { + get; private set; + } = false; + + private Category _collisionCategories; public Category CollisionCategories { + get { return _collisionCategories; } set { SetCollisionCategories(value); } } + + public bool CollidesWithMatchesBetweenFixtures + { + get; private set; + } = false; + + private Category _collidesWith; public Category CollidesWith { + get { return _collidesWith; } set { SetCollidesWith(value); } } + public void RefreshCollisionCategoriesMatchBetweenFixtures() + { + if (FixtureList.Count < 2) + { + if (FixtureList.Count > 0) { _collisionCategories = FixtureList[0].CollisionCategories; } + CollisionCategoriesMatchBetweenFixtures = true; + return; + } + for (int i = 1; i < FixtureList.Count; i++) + { + if (FixtureList[i].CollisionCategories != FixtureList[0].CollisionCategories) + { + CollisionCategoriesMatchBetweenFixtures = false; + return; + } + } + CollisionCategoriesMatchBetweenFixtures = true; + _collisionCategories = FixtureList[0].CollisionCategories; + } + /// /// Warning: This method applies the value on existing Fixtures. It's not a property of Body. /// public void SetCollisionCategories(Category category) { + CollisionCategoriesMatchBetweenFixtures = true; + _collisionCategories = category; for (int i = 0; i < FixtureList.Count; i++) FixtureList[i].CollisionCategories = category; } + public void RefreshCollidesWithMatchesBetweenFixtures() + { + if (FixtureList.Count < 2) + { + if (FixtureList.Count > 0) { _collidesWith = FixtureList[0].CollidesWith; } + CollidesWithMatchesBetweenFixtures = true; + return; + } + for (int i = 1; i < FixtureList.Count; i++) + { + if (FixtureList[i].CollidesWith != FixtureList[0].CollidesWith) + { + CollidesWithMatchesBetweenFixtures = false; + return; + } + } + CollidesWithMatchesBetweenFixtures = true; + _collidesWith = FixtureList[0].CollidesWith; + } + + /// /// Warning: This method applies the value on existing Fixtures. It's not a property of Body. /// public void SetCollidesWith(Category category) { + CollidesWithMatchesBetweenFixtures = true; + _collidesWith = category; for (int i = 0; i < FixtureList.Count; i++) FixtureList[i].CollidesWith = category; } diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/ContactManager.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/ContactManager.cs index e57f793c0b..1c47b8c055 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/ContactManager.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/ContactManager.cs @@ -172,7 +172,6 @@ private void AddPair(int proxyIdA, int proxyIdB) if (bodyB.ShouldCollide(bodyA) == false) return; - //Check default filter if (ShouldCollide(fixtureA, fixtureB) == false) return; diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs index d65d9a1b00..58f22c12eb 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Fixture.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Kastellanos Nikolaos +// Copyright (c) 2017 Kastellanos Nikolaos /* Original source Farseer Physics Engine: * Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com @@ -139,6 +139,10 @@ public Category CollidesWith _collidesWith = value; Refilter(); + if (Body != null) + { + Body.RefreshCollidesWithMatchesBetweenFixtures(); + } } } @@ -151,13 +155,17 @@ public Category CollisionCategories { get { return _collisionCategories; } - set + internal set { if (_collisionCategories == value) return; _collisionCategories = value; Refilter(); + if (Body != null) + { + Body.RefreshCollisionCategoriesMatchBetweenFixtures(); + } } } diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Joints/Joint.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Joints/Joint.cs index 2c2cdbced1..d40fc46225 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Joints/Joint.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Joints/Joint.cs @@ -1,4 +1,4 @@ -/* Original source Farseer Physics Engine: +/* Original source Farseer Physics Engine: * Copyright (c) 2014 Ian Qvist, http://farseerphysics.codeplex.com * Microsoft Permissive License (Ms-PL) v1.1 */ @@ -243,7 +243,7 @@ internal void Validate(float invDt) Enabled = false; if (Broke != null) - Broke(this, (float)Math.Sqrt(jointErrorSquared)); + Broke(this, MathF.Sqrt(jointErrorSquared)); } internal abstract void SolveVelocityConstraints(ref SolverData data); diff --git a/Libraries/Farseer Physics Engine 3.5/Fluids/1/FluidSystem1.cs b/Libraries/Farseer Physics Engine 3.5/Fluids/1/FluidSystem1.cs index 20fc53bd02..9e3f1faea4 100644 --- a/Libraries/Farseer Physics Engine 3.5/Fluids/1/FluidSystem1.cs +++ b/Libraries/Farseer Physics Engine 3.5/Fluids/1/FluidSystem1.cs @@ -107,7 +107,7 @@ private void ApplyViscosity(FluidParticle p, float timeStep) if (u > 0.0f) { - q = 1.0f - (float)Math.Sqrt(q) / Definition.InfluenceRadius; + q = 1.0f - MathF.Sqrt(q) / Definition.InfluenceRadius; float impulseFactor = 0.5f * timeStep * q * (u * (Definition.ViscositySigma + Definition.ViscosityBeta * u)); @@ -152,7 +152,7 @@ private void ApplyViscosity(FluidParticle p, float timeStep) // _distanceCache[_j] = _q; // if (_q < _influenceRadiusSquared && _q != 0) // { - // _q = (float)Math.Sqrt(_q); + // _q = MathF.Sqrt(_q); // _q /= Definition.InfluenceRadius; // _qq = ((1 - _q) * (1 - _q)); // _density += _qq; @@ -170,7 +170,7 @@ private void ApplyViscosity(FluidParticle p, float timeStep) // _q = _distanceCache[_j]; // if (_q < _influenceRadiusSquared && _q != 0) // { - // _q = (float)Math.Sqrt(_q); + // _q = MathF.Sqrt(_q); // _rij = p.Neighbours[_j].Position; // _rij -= p.Position; // _rij *= 1 / _q; @@ -209,7 +209,7 @@ private void DoubleDensityRelaxation(FluidParticle particle, float deltaTime2) if (q > _influenceRadiusSquared) continue; - q = 1.0f - (float)Math.Sqrt(q) / Definition.InfluenceRadius; + q = 1.0f - MathF.Sqrt(q) / Definition.InfluenceRadius; float densityDelta = q * q; _density += densityDelta; @@ -237,7 +237,7 @@ private void DoubleDensityRelaxation(FluidParticle particle, float deltaTime2) if (q > _influenceRadiusSquared) continue; - q = 1.0f - (float)Math.Sqrt(q) / Definition.InfluenceRadius; + q = 1.0f - MathF.Sqrt(q) / Definition.InfluenceRadius; float dispFactor = deltaTime2 * (q * (_pressure + _pressureNear * q)); @@ -281,7 +281,7 @@ private void CreateSprings(FluidParticle p) if (!_springs.ContainsKey(hash)) { //TODO: Use pool? - Spring spring = new Spring(p, neighbour) { RestLength = (float)Math.Sqrt(q) }; + Spring spring = new Spring(p, neighbour) { RestLength = MathF.Sqrt(q) }; _springs.Add(hash, spring); } } diff --git a/Libraries/Farseer Physics Engine 3.5/Fluids/2/FluidSystem2.cs b/Libraries/Farseer Physics Engine 3.5/Fluids/2/FluidSystem2.cs index 2a06736062..2ee981c73a 100644 --- a/Libraries/Farseer Physics Engine 3.5/Fluids/2/FluidSystem2.cs +++ b/Libraries/Farseer Physics Engine 3.5/Fluids/2/FluidSystem2.cs @@ -284,7 +284,7 @@ private void ApplyViscosity(float deltaTime) Vector2.DistanceSquared(ref particle.Position, ref tempParticle.Position, out q); if ((q < InfluenceRadiusSquared) && (q != 0)) { - q = (float)Math.Sqrt(q); + q = MathF.Sqrt(q); Vector2.Subtract(ref tempParticle.Position, ref particle.Position, out _rij); Vector2.Divide(ref _rij, q, out _rij); @@ -329,7 +329,7 @@ private void DoubleDensityRelaxation() Vector2.DistanceSquared(ref particle.Position, ref tempParticle.Position, out q); if (q < InfluenceRadiusSquared && q != 0) { - q = (float)Math.Sqrt(q); + q = MathF.Sqrt(q); q /= InfluenceRadius; float qq = ((1 - q) * (1 - q)); particle.Density += qq; @@ -348,7 +348,7 @@ private void DoubleDensityRelaxation() Vector2.DistanceSquared(ref particle.Position, ref tempParticle.Position, out q); if ((q < InfluenceRadiusSquared) && (q != 0)) { - q = (float)Math.Sqrt(q); + q = MathF.Sqrt(q); Vector2.Subtract(ref tempParticle.Position, ref particle.Position, out _rij); Vector2.Divide(ref _rij, q, out _rij); q /= InfluenceRadius; diff --git a/Libraries/Farseer Physics Engine 3.5/Settings.cs b/Libraries/Farseer Physics Engine 3.5/Settings.cs index 8060d42f86..faad20c4e2 100644 --- a/Libraries/Farseer Physics Engine 3.5/Settings.cs +++ b/Libraries/Farseer Physics Engine 3.5/Settings.cs @@ -220,7 +220,7 @@ public static class Settings /// public static float MixFriction(float friction1, float friction2) { - return (float)Math.Sqrt(friction1 * friction2); + return MathF.Sqrt(friction1 * friction2); } /// diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Display.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Display.cs new file mode 100644 index 0000000000..f472035e0a --- /dev/null +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Display.cs @@ -0,0 +1,11 @@ +namespace Microsoft.Xna.Framework +{ + public static class Display + { + public static int GetNumberOfDisplays() + => Sdl.Display.GetNumVideoDisplays(); + + public static string GetDisplayName(int displayIndex) + => Sdl.Display.GetDisplayName(displayIndex); + } +} diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs index 3e73f59802..c7761012a0 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs @@ -76,6 +76,8 @@ public virtual bool IsBorderless } } + public virtual int TargetDisplay { get => 0; set => throw new NotImplementedException(); } + internal MouseState MouseState; internal TouchPanelState TouchPanelState; diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj index 41c2f71d81..004f032375 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj @@ -38,6 +38,7 @@ + diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj index a8b4128907..09c55647c7 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj @@ -38,6 +38,7 @@ + diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj index e6bc855bf5..6ce9d2328f 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj @@ -77,6 +77,7 @@ + diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs index 5da9dc0657..f8cfe0c7ea 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs @@ -87,6 +87,22 @@ public override bool IsBorderless } } + public override int TargetDisplay + { + get => Sdl.Window.GetDisplayIndex(Handle); + set + { + int maxDisplayIndex = Sdl.Display.GetNumVideoDisplays() - 1; + + // if the value is out of range, set it to 0 (the primary display) + if (value > maxDisplayIndex || value < 0) { value = 0; } + + if (value == Sdl.Window.GetDisplayIndex(Handle)) { return; } + + Sdl.Window.SetPosition(Handle, Sdl.Window.PosCentered | value, Sdl.Window.PosCentered | value); + } + } + public static GameWindow Instance; public uint? Id; public bool IsFullScreen; @@ -163,13 +179,6 @@ internal void CreateWindow() var winx = Sdl.Window.PosCentered; var winy = Sdl.Window.PosCentered; - // if we are on Linux, start on the current screen - if (CurrentPlatform.OS == OS.Linux) - { - winx |= GetMouseDisplay(); - winy |= GetMouseDisplay(); - } - _handle = Sdl.Window.Create(AssemblyHelper.GetDefaultWindowTitle(), winx, winy, _width, _height, initflags); @@ -269,11 +278,8 @@ public override void EndScreenDeviceChange(string screenDeviceName, int clientWi OnClientSizeChanged(); - int ignore, minx = 0, miny = 0; - Sdl.Window.GetBorderSize(_handle, out miny, out minx, out ignore, out ignore); - - var centerX = Math.Max(prevBounds.X + ((prevBounds.Width - clientWidth) / 2), minx); - var centerY = Math.Max(prevBounds.Y + ((prevBounds.Height - clientHeight) / 2), miny); + var centerX = prevBounds.X + ((prevBounds.Width - clientWidth) / 2); + var centerY = prevBounds.Y + ((prevBounds.Height - clientHeight) / 2); if (IsFullScreen && !_willBeFullScreen) { @@ -291,7 +297,7 @@ public override void EndScreenDeviceChange(string screenDeviceName, int clientWi // after the window gets resized, window position information // becomes wrong (for me it always returned 10 8). Solution is // to not try and set the window position because it will be wrong. - if ((Sdl.Patch > 4 || !AllowUserResizing) && !_wasMoved) + if (((Sdl.Patch > 4 && Sdl.Minor == 0) || !AllowUserResizing) && !_wasMoved) Sdl.Window.SetPosition(Handle, centerX, centerY); Sdl.Window.Show(Handle); From 38ca721cb0fd94aadbb9c1886ba2338bbd5f1180 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Wed, 11 Dec 2024 13:27:12 +0200 Subject: [PATCH 3/3] Update bug-reports.yml --- .github/DISCUSSION_TEMPLATE/bug-reports.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index fcf95819ca..8e84514d69 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -73,8 +73,7 @@ body: label: Version description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - - v1.6.19.1 (Unto the Breach Update Hotfix 2) - - v1.7.0.1 (Unstable) + - v1.7.7.0 (Winter Update) - Other validations: required: true