From eeac247a8ea909a89c82c69a8bb243270d1289ae Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Thu, 4 Jun 2020 16:41:07 +0300 Subject: [PATCH] (6eeea9b7c) v0.9.10.0.0 --- .gitignore | 3 + .../BarotraumaClient/ClientSource/Camera.cs | 2 +- .../Characters/AI/EnemyAIController.cs | 17 +- .../Characters/AI/HumanAIController.cs | 11 +- .../Characters/Animation/Ragdoll.cs | 37 +- .../ClientSource/Characters/Character.cs | 35 +- .../ClientSource/Characters/CharacterHUD.cs | 11 +- .../ClientSource/Characters/CharacterInfo.cs | 6 +- .../Characters/CharacterNetworking.cs | 32 +- .../Characters/Health/AfflictionHusk.cs | 2 +- .../Characters/Health/AfflictionPsychosis.cs | 48 +- .../Characters/Health/CharacterHealth.cs | 15 +- .../ClientSource/Characters/Jobs/JobPrefab.cs | 12 +- .../ClientSource/Characters/Limb.cs | 123 +++- .../ClientSource/DebugConsole.cs | 149 +++-- .../ClientSource/Events/EventManager.cs | 4 - .../Events/Missions/CargoMission.cs | 3 +- .../ClientSource/GUI/ChatBox.cs | 13 +- .../ClientSource/GUI/FileSelection.cs | 24 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 59 +- .../ClientSource/GUI/GUICanvas.cs | 52 +- .../ClientSource/GUI/GUIComponent.cs | 2 +- .../ClientSource/GUI/GUIDropDown.cs | 2 +- .../ClientSource/GUI/GUIImage.cs | 16 +- .../ClientSource/GUI/GUILayoutGroup.cs | 14 +- .../ClientSource/GUI/GUIMessageBox.cs | 4 +- .../ClientSource/GUI/GUITextBlock.cs | 4 +- .../ClientSource/GUI/GUITextBox.cs | 9 +- .../ClientSource/GUI/HUDLayoutSettings.cs | 20 +- .../ClientSource/GUI/RectTransform.cs | 13 +- .../ClientSource/GUI/TabMenu.cs | 15 +- .../ClientSource/GUI/VideoPlayer.cs | 7 +- .../BarotraumaClient/ClientSource/GameMain.cs | 6 +- .../ClientSource/GameSession/CrewManager.cs | 491 ++++++++++------ .../GameModes/SinglePlayerCampaign.cs | 2 +- .../GameModes/Tutorials/CaptainTutorial.cs | 2 +- .../GameModes/Tutorials/OfficerTutorial.cs | 9 +- .../GameModes/Tutorials/ScenarioTutorial.cs | 2 +- .../GameModes/Tutorials/Tutorial.cs | 2 +- .../ClientSource/GameSession/RoundSummary.cs | 4 +- .../ClientSource/GameSettings.cs | 163 +++++- .../ClientSource/Items/CharacterInventory.cs | 58 +- .../ClientSource/Items/Components/Door.cs | 5 +- .../Items/Components/ElectricalDischarger.cs | 11 +- .../Items/Components/Holdable/Holdable.cs | 5 +- .../Items/Components/Holdable/RangedWeapon.cs | 2 +- .../Items/Components/ItemComponent.cs | 5 +- .../Items/Components/ItemLabel.cs | 6 +- .../Items/Components/LightComponent.cs | 5 +- .../Items/Components/Machines/Controller.cs | 2 +- .../Items/Components/Machines/Engine.cs | 4 + .../Items/Components/Machines/Fabricator.cs | 18 +- .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Machines/Sonar.cs | 112 +++- .../Items/Components/Machines/Steering.cs | 42 +- .../Items/Components/Repairable.cs | 19 +- .../Items/Components/Signal/Connection.cs | 2 +- .../Items/Components/Signal/Wire.cs | 48 +- .../ClientSource/Items/Components/Turret.cs | 2 +- .../ClientSource/Items/Inventory.cs | 22 +- .../ClientSource/Items/Item.cs | 72 ++- .../ClientSource/Items/ItemPrefab.cs | 4 +- .../ClientSource/Map/FireSource.cs | 4 +- .../BarotraumaClient/ClientSource/Map/Gap.cs | 38 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 75 ++- .../ClientSource/Map/ItemAssemblyPrefab.cs | 1 + .../ClientSource/Map/Lights/ConvexHull.cs | 109 +++- .../ClientSource/Map/Lights/LightManager.cs | 248 ++------ .../ClientSource/Map/Lights/LightSource.cs | 39 +- .../ClientSource/Map/LinkedSubmarine.cs | 2 +- .../ClientSource/Map/MapEntity.cs | 3 +- .../ClientSource/Map/Structure.cs | 4 + .../ClientSource/Map/Submarine.cs | 5 +- .../ClientSource/Map/SubmarineInfo.cs | 6 +- .../ClientSource/Map/WayPoint.cs | 14 +- .../ClientSource/Media/Video.cs | 2 +- .../Networking/ChildServerRelay.cs | 13 +- .../Networking/FileTransfer/FileReceiver.cs | 10 +- .../ClientSource/Networking/GameClient.cs | 36 +- .../ClientSource/Networking/NetStats.cs | 22 +- .../Primitives/Peers/LidgrenClientPeer.cs | 3 + .../Primitives/Peers/SteamP2PClientPeer.cs | 21 +- .../Primitives/Peers/SteamP2POwnerPeer.cs | 18 +- .../ClientSource/Networking/ServerLog.cs | 5 +- .../ClientSource/Networking/ServerSettings.cs | 4 +- .../ClientSource/Networking/SteamManager.cs | 231 ++++---- .../Networking/Voip/VoipCapture.cs | 8 - .../ClientSource/Particles/Particle.cs | 10 +- .../ClientSource/Particles/ParticleEmitter.cs | 19 +- .../ClientSource/Particles/ParticleManager.cs | 10 +- .../ClientSource/Particles/ParticlePrefab.cs | 5 + .../ClientSource/Physics/PhysicsBody.cs | 4 +- .../ClientSource/PlayerInput.cs | 64 +- .../BarotraumaClient/ClientSource/Program.cs | 7 +- .../ClientSource/Screens/CampaignSetupUI.cs | 2 +- .../CharacterEditor/CharacterEditorScreen.cs | 65 ++- .../Screens/CharacterEditor/Wizard.cs | 2 +- .../ClientSource/Screens/GameScreen.cs | 7 +- .../ClientSource/Screens/LevelEditorScreen.cs | 22 +- .../ClientSource/Screens/MainMenuScreen.cs | 16 +- .../ClientSource/Screens/NetLobbyScreen.cs | 132 +++-- .../Screens/ParticleEditorScreen.cs | 13 +- .../ClientSource/Screens/Screen.cs | 3 +- .../ClientSource/Screens/ServerListScreen.cs | 30 +- .../Screens/SpriteEditorScreen.cs | 10 +- .../Screens/SteamWorkshopScreen.cs | 149 +++-- .../ClientSource/Screens/SubEditorScreen.cs | 115 +++- .../Serialization/SerializableEntityEditor.cs | 89 ++- .../ClientSource/Sounds/Sound.cs | 4 +- .../ClientSource/Sounds/SoundManager.cs | 6 +- .../ClientSource/Sounds/SoundPlayer.cs | 68 ++- .../DeformAnimations/CustomDeformation.cs | 4 +- .../Sprite/DeformAnimations/Inflate.cs | 2 +- .../DeformAnimations/JointBendDeformation.cs | 2 +- .../DeformAnimations/NoiseDeformation.cs | 2 +- .../DeformAnimations/PositionalDeformation.cs | 2 +- .../DeformAnimations/SpriteDeformation.cs | 23 +- .../ClientSource/Sprite/DeformableSprite.cs | 24 +- .../ClientSource/Sprite/Sprite.cs | 37 +- .../StatusEffects/StatusEffect.cs | 4 +- .../Traitors/TraitorMissionPrefab.cs | 1 + .../ClientSource/Utils/HttpEncoder.cs | 10 +- .../ClientSource/Utils/HttpUtility.cs | 17 +- .../Utils/LocalizationCSVtoXML.cs | 8 +- .../ClientSource/Utils/TextureLoader.cs | 187 +++++- .../Content/Effects/deformshader.xnb | Bin 8215 -> 8781 bytes .../Content/Effects/deformshader_opengl.xnb | Bin 8468 -> 8871 bytes .../Content/Effects/solidcolor.xnb | Bin 1875 -> 2429 bytes .../Content/Effects/solidcolor_opengl.xnb | Bin 1499 -> 1890 bytes .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/Shaders/deformshader.fx | 14 + .../Shaders/deformshader_opengl.fx | 14 + .../BarotraumaClient/Shaders/solidcolor.fx | 15 +- .../Shaders/solidcolor_opengl.fx | 17 +- .../BarotraumaClient/WindowsClient.csproj | 9 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../Characters/CharacterNetworking.cs | 44 +- .../ServerSource/DebugConsole.cs | 196 +++++-- .../Events/Missions/CargoMission.cs | 4 +- .../Events/Missions/SalvageMission.cs | 8 +- .../BarotraumaServer/ServerSource/GameMain.cs | 13 +- .../GameModes/MultiPlayerCampaign.cs | 5 +- .../ServerSource/Items/Components/Door.cs | 3 +- .../Items/Components/Holdable/Holdable.cs | 3 +- .../Items/Components/Machines/Controller.cs | 2 +- .../Components/Signal/CustomInterface.cs | 2 +- .../ServerSource/Items/Inventory.cs | 3 + .../ServerSource/Networking/BanList.cs | 2 +- .../ServerSource/Networking/ChatMessage.cs | 5 +- .../Networking/ChildServerRelay.cs | 2 +- .../Networking/FileTransfer/FileSender.cs | 4 +- .../ServerSource/Networking/GameServer.cs | 11 +- .../Peers/Server/LidgrenServerPeer.cs | 10 +- .../Peers/Server/SteamP2PServerPeer.cs | 10 +- .../ServerSource/Networking/ServerSettings.cs | 13 +- .../ServerSource/Networking/WhiteList.cs | 2 +- .../BarotraumaServer/ServerSource/Program.cs | 7 +- .../Traitors/Goals/GoalDestroyItemsWithTag.cs | 4 +- .../ServerSource/Traitors/TraitorMission.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 8 +- .../Data/ContentPackages/Vanilla 0.9.xml | 3 + .../Characters/AI/AIController.cs | 2 +- .../Characters/AI/EnemyAIController.cs | 522 +++++++++++------ .../Characters/AI/HumanAIController.cs | 139 ++--- .../Characters/AI/IndoorsSteeringManager.cs | 144 +++-- .../SharedSource/Characters/AI/LatchOntoAI.cs | 52 +- .../Characters/AI/NPCConversation.cs | 6 +- .../Characters/AI/Objectives/AIObjective.cs | 5 +- .../Objectives/AIObjectiveChargeBatteries.cs | 1 + .../AI/Objectives/AIObjectiveCombat.cs | 19 +- .../AI/Objectives/AIObjectiveContainItem.cs | 2 +- .../Objectives/AIObjectiveExtinguishFire.cs | 2 +- .../Objectives/AIObjectiveFightIntruders.cs | 7 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 58 +- .../AI/Objectives/AIObjectiveFixLeaks.cs | 2 +- .../AI/Objectives/AIObjectiveGetItem.cs | 67 ++- .../AI/Objectives/AIObjectiveGoTo.cs | 52 +- .../AI/Objectives/AIObjectiveIdle.cs | 41 +- .../AI/Objectives/AIObjectiveLoop.cs | 4 +- .../AI/Objectives/AIObjectiveManager.cs | 87 ++- .../AI/Objectives/AIObjectiveOperateItem.cs | 63 +- .../AI/Objectives/AIObjectivePumpWater.cs | 2 +- .../AI/Objectives/AIObjectiveRepairItem.cs | 9 +- .../AI/Objectives/AIObjectiveRepairItems.cs | 28 +- .../AI/Objectives/AIObjectiveRescue.cs | 45 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 58 +- .../SharedSource/Characters/AI/Order.cs | 95 ++- .../SharedSource/Characters/AI/PathFinder.cs | 26 +- .../Characters/AI/SteeringPath.cs | 37 +- .../Characters/AI/Wreck/WreckAI.cs | 21 +- .../Characters/AI/Wreck/WreckAIConfig.cs | 3 + .../SharedSource/Characters/AICharacter.cs | 14 +- .../Characters/Animation/AnimController.cs | 3 +- .../Animation/FishAnimController.cs | 64 +- .../Animation/HumanoidAnimController.cs | 87 +-- .../Characters/Animation/Ragdoll.cs | 254 +++++--- .../SharedSource/Characters/Attack.cs | 29 +- .../SharedSource/Characters/Character.cs | 413 +++++++++---- .../SharedSource/Characters/CharacterInfo.cs | 2 +- .../Characters/CharacterPrefab.cs | 2 +- .../Health/Afflictions/AfflictionHusk.cs | 41 +- .../Health/Afflictions/AfflictionPrefab.cs | 7 + .../Health/Afflictions/AfflictionPsychosis.cs | 2 +- .../Health/Buffs/BuffDurationIncrease.cs | 14 +- .../Characters/Health/CharacterHealth.cs | 82 ++- .../SharedSource/Characters/Jobs/JobPrefab.cs | 8 +- .../SharedSource/Characters/Limb.cs | 242 +++++++- .../Params/Animation/AnimationParams.cs | 2 +- .../Params/Animation/FishAnimations.cs | 19 +- .../Characters/Params/CharacterParams.cs | 17 +- .../Characters/Params/EditableParams.cs | 14 +- .../Params/Ragdoll/RagdollParams.cs | 17 +- .../SharedSource/Characters/SkillSettings.cs | 2 +- .../SharedSource/ContentPackage.cs | 79 ++- .../SharedSource/DebugConsole.cs | 65 ++- .../BarotraumaShared/SharedSource/Enums.cs | 19 +- .../SharedSource/Events/EventManager.cs | 126 +++- .../Events/EventManagerSettings.cs | 3 +- .../Events/Missions/CargoMission.cs | 18 +- .../Events/Missions/CombatMission.cs | 17 - .../Events/Missions/MonsterMission.cs | 2 +- .../Events/Missions/SalvageMission.cs | 16 +- .../SharedSource/Events/MonsterEvent.cs | 70 ++- .../Events/ScriptedEventPrefab.cs | 2 - .../SharedSource/Events/ScriptedEventSet.cs | 127 +++- .../Extensions/IEnumerableExtensions.cs | 13 +- .../SharedSource/GameAnalyticsManager.cs | 2 +- .../GameSession/AutoItemPlacer.cs | 4 + .../GameModes/MultiPlayerCampaign.cs | 2 +- .../SharedSource/GameSession/GameSession.cs | 7 +- .../SharedSource/GameSettings.cs | 399 +++++-------- .../SharedSource/Items/CharacterInventory.cs | 24 +- .../Items/Components/DockingPort.cs | 107 +++- .../SharedSource/Items/Components/Door.cs | 41 +- .../Items/Components/ElectricalDischarger.cs | 26 +- .../Items/Components/Holdable/Holdable.cs | 69 ++- .../Items/Components/Holdable/MeleeWeapon.cs | 1 + .../Items/Components/Holdable/Pickable.cs | 6 +- .../Items/Components/Holdable/Propulsion.cs | 2 +- .../Items/Components/Holdable/RangedWeapon.cs | 4 +- .../Items/Components/Holdable/RepairTool.cs | 88 ++- .../Items/Components/ItemComponent.cs | 168 +++--- .../Items/Components/ItemContainer.cs | 30 +- .../Items/Components/Machines/Controller.cs | 20 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Engine.cs | 15 +- .../Items/Components/Machines/Fabricator.cs | 2 +- .../Components/Machines/OxygenGenerator.cs | 4 +- .../Items/Components/Machines/Pump.cs | 9 +- .../Items/Components/Machines/Reactor.cs | 21 +- .../Items/Components/Machines/Steering.cs | 86 ++- .../Items/Components/Power/PowerTransfer.cs | 2 + .../Items/Components/Projectile.cs | 6 +- .../Items/Components/Repairable.cs | 30 +- .../Items/Components/Signal/AndComponent.cs | 6 +- .../Components/Signal/ArithmeticComponent.cs | 8 +- .../Components/Signal/ConnectionPanel.cs | 2 +- .../Items/Components/Signal/DelayComponent.cs | 6 +- .../Components/Signal/EqualsComponent.cs | 6 +- .../Signal/ExponentiationComponent.cs | 2 +- .../Components/Signal/FunctionComponent.cs | 2 +- .../Items/Components/Signal/LightComponent.cs | 10 +- .../Components/Signal/MemoryComponent.cs | 2 +- .../Components/Signal/ModuloComponent.cs | 2 +- .../Items/Components/Signal/MotionSensor.cs | 17 +- .../Components/Signal/OscillatorComponent.cs | 4 +- .../Components/Signal/RegExFindComponent.cs | 8 +- .../Items/Components/Signal/RelayComponent.cs | 2 +- .../Components/Signal/SignalCheckComponent.cs | 6 +- .../Items/Components/Signal/SmokeDetector.cs | 44 +- .../Items/Components/Signal/Terminal.cs | 2 +- .../Signal/TrigonometricFunctionComponent.cs | 4 +- .../Items/Components/Signal/WaterDetector.cs | 4 +- .../Items/Components/Signal/WifiComponent.cs | 47 +- .../Items/Components/Signal/Wire.cs | 10 +- .../SharedSource/Items/Components/Turret.cs | 81 ++- .../SharedSource/Items/Components/Wearable.cs | 19 +- .../SharedSource/Items/Item.cs | 64 +- .../SharedSource/Items/ItemPrefab.cs | 23 +- .../SharedSource/Map/Entity.cs | 4 +- .../SharedSource/Map/Explosion.cs | 88 +-- .../SharedSource/Map/FireSource.cs | 32 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 68 +-- .../BarotraumaShared/SharedSource/Map/Hull.cs | 113 ++-- .../SharedSource/Map/ItemAssemblyPrefab.cs | 41 +- .../SharedSource/Map/Levels/CaveGenerator.cs | 75 ++- .../SharedSource/Map/Levels/Level.cs | 56 +- .../Map/Levels/LevelGenerationParams.cs | 6 +- .../Map/Levels/LevelObjects/LevelTrigger.cs | 27 +- .../Map/Levels/Ruins/RuinGenerationParams.cs | 6 +- .../Map/Levels/Ruins/RuinGenerator.cs | 5 +- .../SharedSource/Map/Map/LocationType.cs | 2 +- .../SharedSource/Map/Md5Hash.cs | 2 +- .../SharedSource/Map/Structure.cs | 45 +- .../SharedSource/Map/StructurePrefab.cs | 2 +- .../SharedSource/Map/Submarine.cs | 47 +- .../SharedSource/Map/SubmarineBody.cs | 33 +- .../SharedSource/Map/SubmarineInfo.cs | 17 +- .../SharedSource/Map/WayPoint.cs | 19 +- .../Networking/ChildServerRelay.cs | 20 +- .../Networking/ClientPermissions.cs | 2 +- .../SharedSource/Networking/KarmaManager.cs | 11 +- .../Networking/Primitives/Message/Message.cs | 8 +- .../SharedSource/Networking/ServerLog.cs | 2 +- .../SharedSource/Networking/ServerSettings.cs | 2 +- .../SharedSource/Networking/Voip/VoipQueue.cs | 20 +- .../SharedSource/Networking/WhiteList.cs | 2 +- .../SharedSource/Physics/Physics.cs | 2 +- .../SharedSource/Physics/PhysicsBody.cs | 6 +- .../SharedSource/ProcGen/VoronoiElements.cs | 14 +- .../SharedSource/Screens/GameScreen.cs | 6 - .../Serialization/SerializableProperty.cs | 21 +- .../Serialization/XMLExtensions.cs | 2 +- .../SharedSource/Sprite/ConditionalSprite.cs | 25 +- .../SharedSource/Sprite/DeformableSprite.cs | 6 +- .../SharedSource/Sprite/Sprite.cs | 5 +- .../StatusEffects/DelayedEffect.cs | 2 +- .../StatusEffects/PropertyConditional.cs | 52 +- .../StatusEffects/StatusEffect.cs | 40 +- .../SharedSource/SteamAchievementManager.cs | 29 +- .../SharedSource/TextManager.cs | 2 +- .../BarotraumaShared/SharedSource/TextPack.cs | 11 +- .../SharedSource/Utils/SafeIO.cs | 546 ++++++++++++++++++ .../SharedSource/Utils/SaveUtil.cs | 32 +- .../SharedSource/Utils/ToolBox.cs | 15 +- .../SharedSource/Utils/UpdaterUtil.cs | 12 +- .../BarotraumaShared/Submarines/Azimuth.sub | Bin 217050 -> 217658 bytes .../BarotraumaShared/Submarines/Berilia.sub | Bin 302787 -> 303664 bytes .../BarotraumaShared/Submarines/Dugong.sub | Bin 210903 -> 211321 bytes .../BarotraumaShared/Submarines/Hemulen.sub | Bin 255181 -> 255391 bytes .../BarotraumaShared/Submarines/Humpback.sub | Bin 207126 -> 207685 bytes .../BarotraumaShared/Submarines/Kastrull.sub | Bin 553602 -> 572807 bytes .../Submarines/KastrullDrone.sub | Bin 289192 -> 289702 bytes .../BarotraumaShared/Submarines/Orca.sub | Bin 205089 -> 205454 bytes .../BarotraumaShared/Submarines/Remora.sub | Bin 331372 -> 332408 bytes .../Submarines/RemoraDrone.sub | Bin 71714 -> 73396 bytes .../BarotraumaShared/Submarines/Selkie.sub | Bin 256791 -> 256997 bytes .../BarotraumaShared/Submarines/Typhon.sub | Bin 283215 -> 283740 bytes .../BarotraumaShared/Submarines/Typhon2.sub | Bin 298915 -> 292385 bytes .../BarotraumaShared/Submarines/Venture.sub | Bin 400601 -> 400870 bytes Barotrauma/BarotraumaShared/changelog.txt | 149 +++++ Barotrauma/BarotraumaShared/config.xml | 2 +- Deploy/Linux/DeployLinux.sh | 4 +- Deploy/Linux/DeployLinuxUnstable.sh | 4 +- Deploy/Linux/DeployMac.sh | 4 +- Deploy/Linux/DeployMacUnstable.sh | 4 +- Deploy/Linux/DeployWindows.sh | 4 +- Deploy/Linux/DeployWindowsUnstable.sh | 4 +- Deploy/Windows/DeployLinux.bat | 4 +- Deploy/Windows/DeployLinuxUnstable.bat | 4 +- Deploy/Windows/DeployMac.bat | 4 +- Deploy/Windows/DeployMacUnstable.bat | 4 +- Deploy/Windows/DeployWindows.bat | 4 +- Deploy/Windows/DeployWindowsUnstable.bat | 4 +- .../SteamRemoteStorage.cs | 35 +- .../Facepunch.Steamworks/Structs/UgcEditor.cs | 7 + .../Dynamics/Body.cs | 4 +- .../Graphics/GraphicsDevice.DirectX.cs | 4 +- .../SamplerStateCollection.DirectX.cs | 17 +- .../Graphics/SamplerStateCollection.cs | 5 +- .../Graphics/SpriteBatch.cs | 2 +- .../Graphics/SpriteBatcher.cs | 107 ++-- .../Graphics/TextureCollection.DirectX.cs | 11 +- .../Graphics/TextureCollection.cs | 11 +- 366 files changed, 7772 insertions(+), 3692 deletions(-) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs diff --git a/.gitignore b/.gitignore index 2afaafc0a..4dd2d8677 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,8 @@ Libraries/webm_mem_playback/opus_x64_linux/ # Mac *.DS_Store +# Win +desktop.ini + #Merge script temp.txt diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 48648f15b..1c5adb3ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -200,7 +200,7 @@ namespace Barotrauma worldView = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); viewMatrix = Matrix.CreateTranslation(new Vector3(GameMain.GraphicsWidth / 2.0f, GameMain.GraphicsHeight / 2.0f, 0)); - globalZoomScale = (float)Math.Pow(new Vector2(resolution.X, resolution.Y).Length() / new Vector2(1920, 1080).Length(), 2); + globalZoomScale = (float)Math.Pow(new Vector2(GUI.UIWidth, resolution.Y).Length() / GUI.ReferenceResolution.Length(), 2); } public void UpdateTransform(bool interpolate = true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index a9ff967ca..7d612effb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -19,11 +19,14 @@ namespace Barotrauma var target = _selectedAiTarget ?? _lastAiTarget; if (target != null && target.Entity != null) { - var memory = GetTargetMemory(target); - Vector2 targetPos = memory.Location; - targetPos.Y = -targetPos.Y; - GUI.DrawLine(spriteBatch, pos, targetPos, Color.White * 0.5f, 0, 4); - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{target.Entity.ToString()} ({memory.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + var memory = GetTargetMemory(target, false); + if (memory != null) + { + Vector2 targetPos = memory.Location; + targetPos.Y = -targetPos.Y; + GUI.DrawLine(spriteBatch, pos, targetPos, Color.White * 0.5f, 0, 4); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{target.Entity} ({memory.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + } } } else if (SelectedAiTarget?.Entity != null) @@ -35,7 +38,7 @@ namespace Barotrauma } targetPos.Y = -targetPos.Y; GUI.DrawLine(spriteBatch, pos, targetPos, GUI.Style.Red * 0.5f, 0, 4); - if (wallTarget != null) + if (wallTarget != null && (State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)) { Vector2 wallTargetPos = wallTarget.Position; if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.Position; } @@ -43,7 +46,7 @@ namespace Barotrauma 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); } - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity.ToString()} ({GetTargetMemory(SelectedAiTarget).Priority.FormatZeroDecimal()})", GUI.Style.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity} ({GetTargetMemory(SelectedAiTarget, false)?.Priority.FormatZeroDecimal()})", GUI.Style.Red, Color.Black); GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"({targetValue.FormatZeroDecimal()})", GUI.Style.Red, Color.Black); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index fdf426063..48d6c619c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -44,19 +44,20 @@ namespace Barotrauma var currentObjective = ObjectiveManager.CurrentObjective; if (currentObjective != null) { - if (currentOrder == null) + int offset = currentOrder != null ? 20 : 0; + if (currentOrder == null || currentOrder.Priority <= 0) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20 + offset), $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } var subObjective = currentObjective.CurrentSubObjective; if (subObjective != null) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 40), $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 40 + offset), $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } var activeObjective = ObjectiveManager.GetActiveObjective(); if (activeObjective != null) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 60), $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 60 + offset), $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } } } @@ -86,7 +87,7 @@ namespace Barotrauma new Vector2(path.CurrentNode.DrawPosition.X, -path.CurrentNode.DrawPosition.Y), Color.BlueViolet, 0, 3); - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 80), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 100), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index eb622fe34..132570e65 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -389,7 +389,7 @@ namespace Barotrauma { foreach (Limb limb in Limbs) { - if (limb == null || limb.IsSevered || limb.ActiveSprite == null) continue; + if (limb == null || limb.IsSevered || limb.ActiveSprite == null) { continue; } Vector2 spriteOrigin = limb.ActiveSprite.Origin; spriteOrigin.X = limb.ActiveSprite.SourceRect.Width - spriteOrigin.X; @@ -404,8 +404,8 @@ namespace Barotrauma float gibParticleAmount = MathHelper.Clamp(limb.Mass / character.AnimController.Mass, 0.1f, 1.0f); foreach (ParticleEmitter emitter in character.GibEmitters) { - if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) continue; - if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) continue; + if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } + if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, limb.WorldPosition, character.CurrentHull, amountMultiplier: gibParticleAmount); } @@ -418,7 +418,8 @@ namespace Barotrauma if (playSound) { - SoundPlayer.PlayDamageSound("Gore", 1.0f, limbJoint.LimbA.body); + var damageSound = character.GetSound(s => s.Type == CharacterSound.SoundType.Damage); + SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: damageSound != null ? damageSound.Range : 800); } } @@ -446,9 +447,10 @@ namespace Barotrauma float depthOffset = GetDepthOffset(); for (int i = 0; i < limbs.Length; i++) { - if (depthOffset != 0.0f) { inversedLimbDrawOrder[i].ActiveSprite.Depth += depthOffset; } - inversedLimbDrawOrder[i].Draw(spriteBatch, cam, color); - if (depthOffset != 0.0f) { inversedLimbDrawOrder[i].ActiveSprite.Depth -= depthOffset; } + var limb = inversedLimbDrawOrder[i]; + if (depthOffset != 0.0f) { limb.ActiveSprite.Depth += depthOffset; } + limb.Draw(spriteBatch, cam, color); + if (depthOffset != 0.0f) { limb.ActiveSprite.Depth -= depthOffset; } } LimbJoints.ForEach(j => j.Draw(spriteBatch)); } @@ -489,8 +491,8 @@ namespace Barotrauma public void DebugDraw(SpriteBatch spriteBatch) { - if (!GameMain.DebugDraw || !character.Enabled) return; - if (simplePhysicsEnabled) return; + if (!GameMain.DebugDraw || !character.Enabled) { return; } + if (simplePhysicsEnabled) { return; } foreach (Limb limb in Limbs) { @@ -508,7 +510,7 @@ namespace Barotrauma Collider.DebugDraw(spriteBatch, frozen ? GUI.Style.Red : (inWater ? Color.SkyBlue : Color.Gray)); GUI.Font.DrawString(spriteBatch, Collider.LinearVelocity.X.FormatSingleDecimal(), new Vector2(Collider.DrawPosition.X, -Collider.DrawPosition.Y), Color.Orange); - foreach (RevoluteJoint joint in LimbJoints) + foreach (var joint in LimbJoints) { Vector2 pos = ConvertUnits.ToDisplayUnits(joint.WorldAnchorA); GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 5, 5), Color.White, true); @@ -522,7 +524,10 @@ namespace Barotrauma if (limb.body.TargetPosition != null) { Vector2 pos = ConvertUnits.ToDisplayUnits((Vector2)limb.body.TargetPosition); - if (currentHull?.Submarine != null) pos += currentHull.Submarine.DrawPosition; + if (currentHull?.Submarine != null) + { + pos += currentHull.Submarine.DrawPosition; + } pos.Y = -pos.Y; GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X - 10, (int)pos.Y - 10, 20, 20), Color.Cyan, false, 0.01f); @@ -541,13 +546,19 @@ namespace Barotrauma if (character.MemState.Count > 1) { Vector2 prevPos = ConvertUnits.ToDisplayUnits(character.MemState[0].Position); - if (currentHull?.Submarine != null) prevPos += currentHull.Submarine.DrawPosition; + if (currentHull?.Submarine != null) + { + prevPos += currentHull.Submarine.DrawPosition; + } prevPos.Y = -prevPos.Y; for (int i = 1; i < character.MemState.Count; i++) { Vector2 currPos = ConvertUnits.ToDisplayUnits(character.MemState[i].Position); - if (currentHull?.Submarine != null) currPos += currentHull.Submarine.DrawPosition; + if (currentHull?.Submarine != null) + { + currPos += currentHull.Submarine.DrawPosition; + } currPos.Y = -currPos.Y; GUI.DrawRectangle(spriteBatch, new Rectangle((int)currPos.X - 3, (int)currPos.Y - 3, 6, 6), Color.Cyan * 0.6f, true, 0.01f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 5ec765476..96502da37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -357,7 +358,16 @@ namespace Barotrauma partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { - if (attackResult.Damage <= 1.0f || IsDead) { return; } + if (IsDead) { return; } + if (attacker != null) + { + if (attackResult.Damage <= 0.01f) { return; } + } + else + { + if (attackResult.Damage <= 1.0f) { return; } + } + if (soundTimer < soundInterval * 0.5f) { PlaySound(CharacterSound.SoundType.Damage); @@ -813,20 +823,22 @@ namespace Barotrauma return progressBar; } + private readonly List matchingSounds = new List(); private SoundChannel soundChannel; public void PlaySound(CharacterSound.SoundType soundType) { if (sounds == null || sounds.Count == 0) { return; } if (soundChannel != null && soundChannel.IsPlaying) { return; } if (GameMain.SoundManager?.Disabled ?? true) { return; } - - var matchingSounds = sounds.Where(s => - s.Type == soundType && - (s.Gender == Gender.None || (info != null && info.Gender == s.Gender))); - if (!matchingSounds.Any()) { return; } - - var matchingSoundsList = matchingSounds.ToList(); - var selectedSound = matchingSoundsList[Rand.Int(matchingSoundsList.Count)]; + matchingSounds.Clear(); + foreach (var s in sounds) + { + if (s.Type == soundType && (s.Gender == Gender.None || (info != null && info.Gender == s.Gender))) + { + matchingSounds.Add(s); + } + } + var selectedSound = matchingSounds.GetRandom(); if (selectedSound?.Sound == null) { return; } soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, CurrentHull); soundTimer = soundInterval; @@ -846,6 +858,11 @@ namespace Barotrauma activeObjectiveEntities.Remove(found); } + /// + /// Note that when a predicate is provided, the random option uses Linq.Where() extension method, which creates a new collection. + /// + public CharacterSound GetSound(Func predicate = null, bool random = false) => random ? sounds.GetRandom(predicate) : sounds.FirstOrDefault(predicate); + partial void ImplodeFX() { Vector2 centerOfMass = AnimController.GetCenterOfMass(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index f051a3b79..989b52794 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -29,7 +29,7 @@ namespace Barotrauma { if (hudFrame == null) { - hudFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null) + hudFrame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) { CanBeFocused = false }; @@ -163,7 +163,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Submarine == null || item.Submarine.TeamID != character.TeamID || item.Submarine.Info.IsWreck) { continue; } - if (!item.Repairables.Any(r => item.ConditionPercentage <= r.AIRepairThreshold)) { continue; } + if (!item.Repairables.Any(r => item.ConditionPercentage <= r.RepairThreshold)) { continue; } if (Submarine.VisibleEntities != null && !Submarine.VisibleEntities.Contains(item)) { continue; } Vector2 diff = item.WorldPosition - character.WorldPosition; @@ -202,6 +202,7 @@ namespace Barotrauma foreach (Item brokenItem in brokenItems) { + if (brokenItem.NonInteractable) { continue; } float dist = Vector2.Distance(character.WorldPosition, brokenItem.WorldPosition); Vector2 drawPos = brokenItem.DrawPosition; float alpha = Math.Min((1000.0f - dist) / 1000.0f * 2.0f, 1.0f); @@ -373,7 +374,7 @@ namespace Barotrauma { GUIComponent.DrawToolTip( spriteBatch, - character.Info?.Job == null ? character.DisplayName : character.Name + " (" + character.Info.Job.Name + ")", + character.Info?.Job == null ? character.DisplayName : character.DisplayName + " (" + character.Info.Job.Name + ")", HUDLayoutSettings.PortraitArea); } } @@ -393,10 +394,6 @@ namespace Barotrauma startPos = cam.WorldToScreen(startPos); string focusName = character.FocusedCharacter.DisplayName; - if (character.FocusedCharacter.Info != null) - { - focusName = character.FocusedCharacter.Info.DisplayName; - } Vector2 textPos = startPos; Vector2 textSize = GUI.Font.MeasureString(focusName); Vector2 largeTextSize = GUI.SubHeadingFont.MeasureString(focusName); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 8058762f4..6419470f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -146,7 +146,8 @@ namespace Barotrauma "+" + ((int)((newLevel - prevLevel) * 100.0f)).ToString() + " XP", GUI.Style.Green, textPopupPos, - Vector2.UnitY * 10.0f); + Vector2.UnitY * 10.0f, + playSound: Character.Controlled?.Info == this); } else if (prevLevel % 0.1f > 0.05f && newLevel % 0.1f < 0.05f) { @@ -154,7 +155,8 @@ namespace Barotrauma "+10 XP", GUI.Style.Green, textPopupPos, - Vector2.UnitY * 10.0f); + Vector2.UnitY * 10.0f, + playSound: Character.Controlled?.Info == this); } if ((int)newLevel > (int)prevLevel) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index e5bf6f36f..8a815e11f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -502,11 +502,9 @@ namespace Barotrauma causeOfDeathAffliction = AfflictionPrefab.Prefabs[afflictionName]; } } - - byte severedLimbCount = msg.ReadByte(); if (!IsDead) { - if (causeOfDeathType == CauseOfDeathType.Pressure) + if (causeOfDeathType == CauseOfDeathType.Pressure || causeOfDeathAffliction == AfflictionPrefab.Pressure) { Implode(true); } @@ -515,26 +513,26 @@ namespace Barotrauma Kill(causeOfDeathType, causeOfDeathAffliction?.Instantiate(1.0f), true); } } - - for (int i = 0; i < severedLimbCount; i++) - { - int severedJointIndex = msg.ReadByte(); - if (severedJointIndex < 0 || severedJointIndex >= AnimController.LimbJoints.Length) - { - string errorMsg = $"Error in CharacterNetworking.ReadStatus: severed joint index out of bounds (index: {severedJointIndex}, joint count: {AnimController.LimbJoints.Length})"; - GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:JointIndexOutOfBounts", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - } - else - { - AnimController.SeverLimbJoint(AnimController.LimbJoints[severedJointIndex]); - } - } } else { if (IsDead) { Revive(); } CharacterHealth.ClientRead(msg); } + byte severedLimbCount = msg.ReadByte(); + for (int i = 0; i < severedLimbCount; i++) + { + int severedJointIndex = msg.ReadByte(); + if (severedJointIndex < 0 || severedJointIndex >= AnimController.LimbJoints.Length) + { + string errorMsg = $"Error in CharacterNetworking.ReadStatus: severed joint index out of bounds (index: {severedJointIndex}, joint count: {AnimController.LimbJoints.Length})"; + GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:JointIndexOutOfBounts", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + } + else + { + AnimController.SeverLimbJoint(AnimController.LimbJoints[severedJointIndex]); + } + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs index 1ba700e72..cbef93fdc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs @@ -1,6 +1,6 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs index c6a8139fc..03ab84938 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -20,9 +21,9 @@ namespace Barotrauma } const int MaxFakeFireSources = 10; - private float minFakeFireSourceInterval = 10.0f, maxFakeFireSourceInterval = 200.0f; + const float MinFakeFireSourceInterval = 30.0f, MaxFakeFireSourceInterval = 240.0f; private float createFireSourceTimer; - private List fakeFireSources = new List(); + private readonly List fakeFireSources = new List(); enum FloodType { @@ -31,26 +32,30 @@ namespace Barotrauma HideFlooding } - private float minSoundInterval = 10.0f, maxSoundInterval = 60.0f; + const float MinSoundInterval = 10.0f, MaxSoundInterval = 180.0f; private FloodType currentFloodType; private float soundTimer; - private float minFloodInterval = 30.0f, maxFloodInterval = 180.0f; + const float MinFloodInterval = 60.0f, MaxFloodInterval = 240.0f; private float createFloodTimer; private float currentFloodState; private float currentFloodDuration; + private float fakeBrokenInterval = 30.0f; + private float fakeBrokenTimer = 0.0f; + partial void UpdateProjSpecific(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) { if (Character.Controlled != characterHealth.Character) return; UpdateFloods(deltaTime); UpdateSounds(characterHealth.Character, deltaTime); - UpdateFires(characterHealth.Character, deltaTime); + UpdateFires(characterHealth.Character, deltaTime); + UpdateFakeBroken(deltaTime); } private void UpdateSounds(Character character, float deltaTime) { - if (soundTimer < MathHelper.Lerp(maxSoundInterval, minSoundInterval, Strength / 100.0f)) + if (soundTimer < MathHelper.Lerp(MaxSoundInterval, MinSoundInterval, Strength / 100.0f)) { soundTimer += deltaTime; return; @@ -97,7 +102,7 @@ namespace Barotrauma return; } - if (createFloodTimer < MathHelper.Lerp(maxFloodInterval, minFloodInterval, Strength / 100.0f)) + if (createFloodTimer < MathHelper.Lerp(MaxFloodInterval, MinFloodInterval, Strength / 100.0f)) { createFloodTimer += deltaTime; return; @@ -124,7 +129,7 @@ namespace Barotrauma createFireSourceTimer += deltaTime; if (fakeFireSources.Count < MaxFakeFireSources && character.Submarine != null && - createFireSourceTimer > MathHelper.Lerp(maxFakeFireSourceInterval, minFakeFireSourceInterval, Strength / 100.0f)) + createFireSourceTimer > MathHelper.Lerp(MaxFakeFireSourceInterval, MinFakeFireSourceInterval, Strength / 100.0f)) { Hull fireHull = Hull.hullList.GetRandom(h => h.Submarine == character.Submarine); @@ -140,9 +145,9 @@ namespace Barotrauma foreach (FakeFireSource fakeFireSource in fakeFireSources) { - if (fakeFireSource.Hull.Surface > fakeFireSource.Hull.Rect.Y - fakeFireSource.Hull.Rect.Height + fakeFireSource.Position.Y) + if (fakeFireSource.Hull.DrawSurface > fakeFireSource.Hull.Rect.Y - fakeFireSource.Hull.Rect.Height + fakeFireSource.Position.Y) { - fakeFireSource.LifeTime -= deltaTime * 10.0f; + fakeFireSource.LifeTime -= deltaTime * 100.0f; } fakeFireSource.LifeTime -= deltaTime; @@ -162,5 +167,28 @@ namespace Barotrauma fakeFireSources.RemoveAll(fs => fs.LifeTime <= 0.0f); } + + private void UpdateFakeBroken(float deltaTime) + { + fakeBrokenTimer -= deltaTime; + if (fakeBrokenTimer > 0.0f) { return; } + + foreach (Item item in Item.ItemList) + { + var repairable = item.GetComponent(); + if (repairable == null) { continue; } + if (ShouldFakeBrokenItem(item)) + { + repairable.FakeBrokenTimer = 60.0f; + } + } + + fakeBrokenTimer = fakeBrokenInterval; + } + + private bool ShouldFakeBrokenItem(Item item) + { + return Rand.Range(0.0f, 1000.0f) < Strength; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 4fde706fd..54c1ba354 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -251,8 +251,7 @@ namespace Barotrauma { get { - // 0.38775510204f = percentage of offset before reaching the healthbar portion of the graphic going from bottom upwards - return new Point(2, (int)(HUDLayoutSettings.HealthBarArea.Size.Y * 0.38775510204f)); + return new Point(Math.Max(2, GUI.IntScaleCeiling(1.5f)), Math.Min(GUI.IntScaleFloor(18f), 19)); } } @@ -260,7 +259,7 @@ namespace Barotrauma { get { - return new Point((int)Math.Ceiling(HUDLayoutSettings.HealthBarArea.Size.X - 45 * GUI.Scale), (int)(healthBarHolder.Rect.Height - Math.Min(23 * GUI.Scale, 25)) / 2); + return new Point(healthBarHolder.Rect.Width - Math.Min(GUI.IntScale(45f), 47), GUI.IntScale(15f)); } } @@ -597,12 +596,14 @@ namespace Barotrauma switch (alignment) { case Alignment.Left: - healthInterfaceFrame.RectTransform.SetPosition(Anchor.CenterLeft); + healthInterfaceFrame.RectTransform.SetPosition(Anchor.BottomLeft); break; case Alignment.Right: - healthInterfaceFrame.RectTransform.SetPosition(Anchor.CenterRight); + healthInterfaceFrame.RectTransform.SetPosition(Anchor.BottomRight); break; } + + healthInterfaceFrame.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.Padding, screenResolution.Y - HUDLayoutSettings.ChatBoxArea.Y + HUDLayoutSettings.Padding); healthInterfaceFrame.RectTransform.RecalculateChildren(false); } @@ -1107,7 +1108,7 @@ namespace Barotrauma float currHealth = healthBar.BarSize; Color prevColor = healthBar.Color; healthBarShadow.BarSize = healthShadowSize; - healthBarShadow.Color = GUI.Style.Red; + healthBarShadow.Color = Color.Lerp(GUI.Style.Red, Color.Black, 0.5f); healthBarShadow.Visible = true; healthBar.BarSize = currHealth; healthBar.Color = prevColor; @@ -1822,7 +1823,7 @@ namespace Barotrauma Vector2 iconPos = highlightArea.Center.ToVector2(); //Affliction mostSevereAffliction = thisAfflictions.FirstOrDefault(a => !a.Prefab.IsBuff && !thisAfflictions.Any(a2 => !a2.Prefab.IsBuff && a2.Strength > a.Strength)) ?? thisAfflictions.FirstOrDefault(); - Affliction mostSevereAffliction = SortAfflictionsBySeverity(thisAfflictions).FirstOrDefault(); + Affliction mostSevereAffliction = SortAfflictionsBySeverity(thisAfflictions, excludeBuffs: false).FirstOrDefault(); if (mostSevereAffliction != null) { DrawLimbAfflictionIcon(spriteBatch, mostSevereAffliction, iconScale, ref iconPos); } if (thisAfflictions.Count() > 1) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs index feb8dee03..4b8aed672 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs @@ -8,12 +8,14 @@ namespace Barotrauma { partial class JobPrefab : IPrefab, IDisposable { - public GUIButton CreateInfoFrame(int variant) + public GUIButton CreateInfoFrame(out GUIComponent buttonContainer) { int width = 500, height = 400; - GUIButton backFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); - GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, height), backFrame.RectTransform, Anchor.Center)); + GUIButton frameHolder = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, frameHolder.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + + GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, height), frameHolder.RectTransform, Anchor.Center)); GUIFrame paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), style: null); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform), Name, font: GUI.LargeFont); @@ -32,6 +34,8 @@ namespace Barotrauma font: GUI.SmallFont); } + buttonContainer = paddedFrame; + /*if (!ItemIdentifiers.TryGetValue(variant, out var itemIdentifiers)) { return backFrame; } var itemContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 0.5f), paddedFrame.RectTransform, Anchor.TopRight) { RelativeOffset = new Vector2(0.0f, 0.2f + descriptionBlock.RectTransform.RelativeSize.Y) }) @@ -49,7 +53,7 @@ namespace Barotrauma font: GUI.SmallFont); }*/ - return backFrame; + return frameHolder; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index ecdea8d8c..30f07e97d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -3,25 +3,24 @@ using Barotrauma.Particles; using Barotrauma.SpriteDeformations; using Barotrauma.Extensions; using FarseerPhysics; -using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using SpriteParams = Barotrauma.RagdollParams.SpriteParams; namespace Barotrauma { - partial class LimbJoint : RevoluteJoint + partial class LimbJoint { public void UpdateDeformations(float deltaTime) { float diff = Math.Abs(UpperLimit - LowerLimit); float strength = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, MathHelper.Pi, diff)); - float jointAngle = this.JointAngle * strength; + float jointAngle = JointAngle * strength; JointBendDeformation limbADeformation = LimbA.Deformations.Find(d => d is JointBendDeformation) as JointBendDeformation; JointBendDeformation limbBDeformation = LimbB.Deformations.Find(d => d is JointBendDeformation) as JointBendDeformation; @@ -70,7 +69,6 @@ namespace Barotrauma } } } - } public void Draw(SpriteBatch spriteBatch) @@ -126,7 +124,7 @@ namespace Barotrauma { get { - var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.IsActive && c.DeformableSprite != null); + var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.Exclusive && c.IsActive && c.DeformableSprite != null); if (conditionalSprite != null) { return conditionalSprite.DeformableSprite; @@ -144,7 +142,7 @@ namespace Barotrauma { get { - var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.IsActive && c.ActiveSprite != null); + var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.Exclusive && c.IsActive && c.ActiveSprite != null); if (conditionalSprite != null) { return conditionalSprite.ActiveSprite; @@ -166,6 +164,12 @@ namespace Barotrauma public Sprite DamagedSprite { get; private set; } + public bool Hide + { + get => Params.Hide; + set => Params.Hide = value; + } + public List ConditionalSprites { get; private set; } = new List(); private Dictionary spriteAnimState = new Dictionary(); private Dictionary> DecorativeSpriteGroups = new Dictionary>(); @@ -274,7 +278,17 @@ namespace Barotrauma DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams)); break; case "conditionalsprite": - var conditionalSprite = new ConditionalSprite(subElement, character, file: GetSpritePath(subElement, null)); + ISerializableEntity targetEntity; + string target = subElement.GetAttributeString("target", null); + if (string.Equals(target, "character", StringComparison.OrdinalIgnoreCase)) + { + targetEntity = character; + } + else + { + targetEntity = this; + } + var conditionalSprite = new ConditionalSprite(subElement, targetEntity, file: GetSpritePath(subElement, null)); ConditionalSprites.Add(conditionalSprite); if (conditionalSprite.DeformableSprite != null) { @@ -373,12 +387,16 @@ namespace Barotrauma private string GetSpritePath(XElement element, SpriteParams spriteParams) { - string texturePath = element.GetAttributeString("texture", null); - if (string.IsNullOrWhiteSpace(texturePath) && spriteParams != null) + if (spriteParams != null) { - texturePath = spriteParams.Ragdoll.Texture; + return GetSpritePath(spriteParams.GetTexturePath()); + } + else + { + string texturePath = element.GetAttributeString("texture", null); + texturePath = string.IsNullOrWhiteSpace(texturePath) ? ragdoll.RagdollParams.Texture : texturePath; + return GetSpritePath(texturePath); } - return GetSpritePath(texturePath); } /// @@ -419,12 +437,29 @@ namespace Barotrauma } } - partial void AddDamageProjSpecific(IEnumerable afflictions, bool playSound, IEnumerable appliedDamageModifiers) + partial void AddDamageProjSpecific(bool playSound, AttackResult result) { - float bleedingDamage = character.CharacterHealth.DoesBleed ? afflictions.Where(a => a is AfflictionBleeding).Sum(a => a.GetVitalityDecrease(character.CharacterHealth)) : 0; - float damage = afflictions.Where(a => a.Prefab.AfflictionType == "damage").Sum(a => a.GetVitalityDecrease(character.CharacterHealth)); + float bleedingDamage = 0; + if (character.CharacterHealth.DoesBleed) + { + foreach (var affliction in result.Afflictions) + { + if (affliction is AfflictionBleeding) + { + bleedingDamage += affliction.GetVitalityDecrease(character.CharacterHealth); + } + } + } + float damage = 0; + foreach (var affliction in result.Afflictions) + { + if (affliction.Prefab.AfflictionType == "damage") + { + damage += affliction.GetVitalityDecrease(character.CharacterHealth); + } + } float damageMultiplier = 1; - foreach (DamageModifier damageModifier in appliedDamageModifiers) + foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { foreach (var afflictionPrefab in AfflictionPrefab.List) { @@ -433,6 +468,7 @@ namespace Barotrauma if (afflictionPrefab.Effects.Any(e => e.MaxVitalityDecrease > 0)) { damageMultiplier *= damageModifier.DamageMultiplier; + break; } } } @@ -440,7 +476,7 @@ namespace Barotrauma if (playSound) { string damageSoundType = (bleedingDamage > damage) ? "LimbSlash" : "LimbBlunt"; - foreach (DamageModifier damageModifier in appliedDamageModifiers) + foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { if (!string.IsNullOrWhiteSpace(damageModifier.DamageSound)) { @@ -457,9 +493,8 @@ namespace Barotrauma { foreach (ParticleEmitter emitter in character.DamageEmitters) { - if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) continue; - if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) continue; - + if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } + if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, WorldPosition, character.CurrentHull, amountMultiplier: damageParticleAmount); } } @@ -471,9 +506,8 @@ namespace Barotrauma foreach (ParticleEmitter emitter in character.BloodEmitters) { - if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) continue; - if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) continue; - + if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } + if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, WorldPosition, character.CurrentHull, sizeMultiplier: bloodParticleSize, amountMultiplier: bloodParticleAmount); } @@ -481,15 +515,14 @@ namespace Barotrauma { character.CurrentHull.AddDecal(character.BloodDecalName, WorldPosition, MathHelper.Clamp(bloodParticleSize, 0.5f, 1.0f)); } - } - + } } partial void UpdateProjSpecific(float deltaTime) { if (!body.Enabled) { return; } - if (!character.IsDead) + if (!IsDead) { DamageOverlayStrength -= deltaTime; BurnOverlayStrength -= deltaTime; @@ -534,6 +567,10 @@ namespace Barotrauma { LightSource.LightSprite.Depth = ActiveSprite.Depth; } + if (LightSource.DeformableLightSprite != null) + { + LightSource.DeformableLightSprite.Sprite.Depth = ActiveSprite.Depth; + } } UpdateSpriteStates(deltaTime); @@ -543,6 +580,8 @@ namespace Barotrauma { float brightness = 1.0f - (burnOverLayStrength / 100.0f) * 0.5f; var spriteParams = Params.GetSprite(); + if (spriteParams == null) { return; } + Color color = new Color(spriteParams.Color.R / 255f * brightness, spriteParams.Color.G / 255f * brightness, spriteParams.Color.B / 255f * brightness, spriteParams.Color.A / 255f); if (deadTimer > 0) { @@ -568,7 +607,7 @@ namespace Barotrauma float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); - bool hideLimb = Params.Hide || + bool hideLimb = Hide || OtherWearables.Any(w => w.HideLimb) || wearingItems.Any(w => w != null && w.HideLimb); @@ -589,6 +628,11 @@ namespace Barotrauma { var deformation = SpriteDeformation.GetDeformation(Deformations, deformSprite.Size); deformSprite.Deform(deformation); + if (LightSource != null && LightSource.DeformableLightSprite != null) + { + deformation = SpriteDeformation.GetDeformation(Deformations, deformSprite.Size, dir == Direction.Left); + LightSource.DeformableLightSprite.Deform(deformation); + } } else { @@ -600,6 +644,31 @@ namespace Barotrauma { body.Draw(spriteBatch, activeSprite, color, null, Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); } + // Handle non-exlusive, i.e. additional conditional sprites + foreach (var conditionalSprite in ConditionalSprites) + { + // Exclusive conditional sprites are handled in the Properties + if (conditionalSprite.Exclusive) { continue; } + if (!conditionalSprite.IsActive) { continue; } + if (conditionalSprite.DeformableSprite != null) + { + var defSprite = conditionalSprite.DeformableSprite; + if (Deformations != null && Deformations.Any()) + { + var deformation = SpriteDeformation.GetDeformation(Deformations, defSprite.Size); + defSprite.Deform(deformation); + } + else + { + defSprite.Reset(); + } + body.Draw(defSprite, cam, Vector2.One * Scale * TextureScale, color, Params.MirrorHorizontally); + } + else + { + body.Draw(spriteBatch, conditionalSprite.Sprite, color, null, Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); + } + } } SpriteEffects spriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipHorizontally; if (LightSource != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index ea20b2c74..fd785b74b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -5,7 +5,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Xml.Linq; @@ -456,18 +456,6 @@ namespace Barotrauma GameMain.CharacterEditorScreen.Select(); })); - commands.Add(new Command("money", "", args => - { - if (args.Length == 0) { return; } - if (GameMain.GameSession.GameMode is CampaignMode campaign) - { - if (int.TryParse(args[0], out int money)) - { - campaign.Money += money; - } - } - }, isCheat: true)); - commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks debug logging.", (string[] args) => { SteamManager.NetworkingDebugLog = !SteamManager.NetworkingDebugLog; @@ -504,6 +492,7 @@ namespace Barotrauma AssignRelayToServer("setpassword", true); commands.Add(new Command("traitorlist", "", (string[] args) => { })); AssignRelayToServer("traitorlist", true); + AssignRelayToServer("money", true); AssignOnExecute("control", (string[] args) => { @@ -669,6 +658,44 @@ namespace Barotrauma } }, isCheat: true)); + commands.Add(new Command("listcloudfiles", "Lists all of your files on the Steam Cloud.", args => + { + int i = 0; + foreach (var file in Steamworks.SteamRemoteStorage.Files) + { + NewMessage($"* {i}: {file.Filename}, {file.Size} bytes", Color.Orange); + i++; + } + NewMessage($"Bytes remaining: {Steamworks.SteamRemoteStorage.QuotaRemainingBytes}/{Steamworks.SteamRemoteStorage.QuotaBytes}", Color.Yellow); + })); + + commands.Add(new Command("removefromcloud", "Removes a file from Steam Cloud.", args => + { + if (args.Length < 1) { return; } + var files = Steamworks.SteamRemoteStorage.Files; + Steamworks.SteamRemoteStorage.RemoteFile file; + if (int.TryParse(args[0], out int index) && index>=0 && index f.Filename.Equals(args[0], StringComparison.InvariantCultureIgnoreCase)); + } + + if (!string.IsNullOrEmpty(file.Filename)) + { + if (file.Delete()) + { + NewMessage($"Deleting {file.Filename}", Color.Orange); + } + else + { + ThrowError($"Failed to delete {file.Filename}"); + } + } + })); + commands.Add(new Command("resetall", "Reset all items and structures to prefabs. Only applicable in the subeditor.", args => { if (Screen.Selected == GameMain.SubEditorScreen) @@ -846,7 +873,7 @@ namespace Barotrauma return; } - if (Submarine.MainSub.SaveAs(System.IO.Path.Combine(SubmarineInfo.SavePath, fileName + ".sub"))) + if (Submarine.MainSub.SaveAs(Barotrauma.IO.Path.Combine(SubmarineInfo.SavePath, fileName + ".sub"))) { NewMessage("Sub saved", Color.Green); } @@ -1038,8 +1065,7 @@ namespace Barotrauma foreach (var deconstructItem in itemPrefab.DeconstructItems) { - var targetItem = MapEntityPrefab.Find(null, deconstructItem.ItemIdentifier, showErrorMessages: false) as ItemPrefab; - if (targetItem == null) + if (!(MapEntityPrefab.Find(null, deconstructItem.ItemIdentifier, showErrorMessages: false) is ItemPrefab targetItem)) { ThrowError("Error in item \"" + itemPrefab.Name + "\" - could not find deconstruct item \"" + deconstructItem.ItemIdentifier + "\"!"); continue; @@ -1054,9 +1080,14 @@ namespace Barotrauma if (fabricationRecipe != null) { - if (!fabricationRecipe.RequiredItems.Any(r => r.ItemPrefab == targetItem)) + var ingredient = fabricationRecipe.RequiredItems.Find(r => r.ItemPrefab == targetItem); + if (ingredient == null) { - NewMessage("Deconstructing \"" + itemPrefab.Name + "\" produces \"" + deconstructItem.ItemIdentifier + "\", which isn't required in the fabrication recipe of the item.", Color.Orange); + NewMessage("Deconstructing \"" + itemPrefab.Name + "\" produces \"" + deconstructItem.ItemIdentifier + "\", which isn't required in the fabrication recipe of the item.", Color.Red); + } + else if (ingredient.UseCondition && ingredient.MinCondition < deconstructItem.OutCondition) + { + NewMessage($"Deconstructing \"{itemPrefab.Name}\" produces more \"{deconstructItem.ItemIdentifier}\", than what's required to fabricate the item (required: {ingredient.ItemPrefab.Name} {(int)(ingredient.MinCondition * 100)}%, output: {deconstructItem.ItemIdentifier} {(int)(deconstructItem.OutCondition * 100)}%)", Color.Red); } } } @@ -1267,6 +1298,13 @@ namespace Barotrauma TextManager.Language = "English"; })); + commands.Add(new Command("eventstats", "", (string[] args) => + { + var debugLines = ScriptedEventSet.GetDebugStatistics(); + string filePath = "eventstats.txt"; + File.WriteAllLines(filePath, debugLines); + ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); + })); #if DEBUG commands.Add(new Command("printreceivertransfers", "", (string[] args) => { @@ -1440,7 +1478,7 @@ namespace Barotrauma element.Value = lines[i]; i++; } - doc.Save(destinationPath); + doc.SaveSafe(destinationPath); }, () => { @@ -1475,7 +1513,7 @@ namespace Barotrauma while ((!(nextNode is XElement) || nextNode == element) && nextNode != null) nextNode = nextNode.NextNode; destinationElement = nextNode as XElement; } - destinationDoc.Save(destinationPath); + destinationDoc.SaveSafe(destinationPath); }, () => { @@ -1730,69 +1768,69 @@ namespace Barotrauma GameMain.Config.SaveNewPlayerConfig(); - var saveFiles = System.IO.Directory.GetFiles(SaveUtil.SaveFolder); + var saveFiles = Barotrauma.IO.Directory.GetFiles(SaveUtil.SaveFolder); foreach (string saveFile in saveFiles) { - System.IO.File.Delete(saveFile); + Barotrauma.IO.File.Delete(saveFile); NewMessage("Deleted " + saveFile, Color.Green); } - if (System.IO.Directory.Exists(System.IO.Path.Combine(SaveUtil.SaveFolder, "temp"))) + if (Barotrauma.IO.Directory.Exists(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp"))) { - System.IO.Directory.Delete(System.IO.Path.Combine(SaveUtil.SaveFolder, "temp"), true); + Barotrauma.IO.Directory.Delete(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp"), true); NewMessage("Deleted temp save folder", Color.Green); } - if (System.IO.Directory.Exists(ServerLog.SavePath)) + if (Barotrauma.IO.Directory.Exists(ServerLog.SavePath)) { - var logFiles = System.IO.Directory.GetFiles(ServerLog.SavePath); + var logFiles = Barotrauma.IO.Directory.GetFiles(ServerLog.SavePath); foreach (string logFile in logFiles) { - System.IO.File.Delete(logFile); + Barotrauma.IO.File.Delete(logFile); NewMessage("Deleted " + logFile, Color.Green); } } - if (System.IO.File.Exists("filelist.xml")) + if (Barotrauma.IO.File.Exists("filelist.xml")) { - System.IO.File.Delete("filelist.xml"); + Barotrauma.IO.File.Delete("filelist.xml"); NewMessage("Deleted filelist", Color.Green); } - if (System.IO.File.Exists("Data/bannedplayers.txt")) + if (Barotrauma.IO.File.Exists("Data/bannedplayers.txt")) { - System.IO.File.Delete("Data/bannedplayers.txt"); + Barotrauma.IO.File.Delete("Data/bannedplayers.txt"); NewMessage("Deleted bannedplayers.txt", Color.Green); } - if (System.IO.File.Exists("Submarines/TutorialSub.sub")) + if (Barotrauma.IO.File.Exists("Submarines/TutorialSub.sub")) { - System.IO.File.Delete("Submarines/TutorialSub.sub"); + Barotrauma.IO.File.Delete("Submarines/TutorialSub.sub"); NewMessage("Deleted TutorialSub from the submarine folder", Color.Green); } - /*if (System.IO.File.Exists(GameServer.SettingsFile)) + /*if (Barotrauma.IO.File.Exists(GameServer.SettingsFile)) { - System.IO.File.Delete(GameServer.SettingsFile); + Barotrauma.IO.File.Delete(GameServer.SettingsFile); NewMessage("Deleted server settings", Color.Green); } - if (System.IO.File.Exists(GameServer.ClientPermissionsFile)) + if (Barotrauma.IO.File.Exists(GameServer.ClientPermissionsFile)) { - System.IO.File.Delete(GameServer.ClientPermissionsFile); + Barotrauma.IO.File.Delete(GameServer.ClientPermissionsFile); NewMessage("Deleted client permission file", Color.Green); }*/ - if (System.IO.File.Exists("crashreport.log")) + if (Barotrauma.IO.File.Exists("crashreport.log")) { - System.IO.File.Delete("crashreport.log"); + Barotrauma.IO.File.Delete("crashreport.log"); NewMessage("Deleted crashreport.log", Color.Green); } - if (!System.IO.File.Exists("Content/Map/TutorialSub.sub")) + if (!Barotrauma.IO.File.Exists("Content/Map/TutorialSub.sub")) { ThrowError("TutorialSub.sub not found!"); } @@ -1841,7 +1879,7 @@ namespace Barotrauma "giveperm", (string[] args) => { - if (args.Length < 1) return; + if (args.Length < 1) { return; } NewMessage("Valid permissions are:", Color.White); foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) @@ -1898,7 +1936,7 @@ namespace Barotrauma { if (args.Length < 1) return; - ShowQuestionPrompt("Console command permissions to grant to client " + args[0] + "? You may enter multiple commands separated with a space.", (commandNames) => + ShowQuestionPrompt("Console command permissions to grant to client " + args[0] + "? You may enter multiple commands separated with a space or use \"all\" to give the permission to use all console commands.", (commandNames) => { GameMain.Client?.SendConsoleCommand("givecommandperm " + args[0] + " " + commandNames); }, args, 1); @@ -1911,7 +1949,7 @@ namespace Barotrauma { if (args.Length < 1) return; - ShowQuestionPrompt("Console command permissions to revoke from client " + args[0] + "? You may enter multiple commands separated with a space.", (commandNames) => + ShowQuestionPrompt("Console command permissions to revoke from client " + args[0] + "? You may enter multiple commands separated with a space or use \"all\" to revoke the permission to use any console commands.", (commandNames) => { GameMain.Client?.SendConsoleCommand("revokecommandperm " + args[0] + " " + commandNames); }, args, 1); @@ -2206,7 +2244,7 @@ namespace Barotrauma ThrowError("Cannot use the flipx command while playing online."); return; } - Submarine.MainSub?.FlipX(); + if (Submarine.MainSub.SubBody != null) { Submarine.MainSub?.FlipX(); } }, isCheat: true)); commands.Add(new Command("gender", "Set the gender of the controlled character. Allowed parameters: Male, Female, None.", args => @@ -2328,9 +2366,16 @@ namespace Barotrauma } try { - SubmarineInfo subInfo = new SubmarineInfo(args[0]); - Submarine spawnedSub = Submarine.Load(subInfo, false); - spawnedSub.SetPosition(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition)); + var subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (subInfo == null) + { + ThrowError($"Could not find a submarine with the name \"{args[0]}\"."); + } + else + { + Submarine spawnedSub = Submarine.Load(subInfo, false); + spawnedSub.SetPosition(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition)); + } } catch (Exception e) { @@ -2338,7 +2383,15 @@ namespace Barotrauma ThrowError(errorMsg, e); GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnSubmarine:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace); } - }, isCheat: true)); + }, + () => + { + return new string[][] + { + SubmarineInfo.SavedSubmarines.Select(s => s.DisplayName).ToArray() + }; + }, + isCheat: true)); commands.Add(new Command("pause", "Toggles the pause state when playing offline", (string[] args) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 442c0384e..9a5ecc7e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -83,10 +83,6 @@ namespace Barotrauma } foreach (ScriptedEventSet eventSet in pendingEventSets) { - float distanceTraveled = MathHelper.Clamp( - (Submarine.MainSub.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), - 0.0f, 1.0f); - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), "New event (ID " + eventSet.DebugIdentifier + ") after: ", Color.Orange * 0.8f, null, 0, GUI.SmallFont); y += 12; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 267103d4f..204452ec4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -6,6 +6,7 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { + items.Clear(); ushort itemCount = msg.ReadUInt16(); for (int i = 0; i < itemCount; i++) { @@ -17,7 +18,7 @@ namespace Barotrauma } if (items.Count != itemCount) { - throw new System.Exception("Error in CargoMission.ClientReadInitial: item count does not match the server count (" + itemCount + " != " + items.Count + "mission: " + Prefab.Identifier + ")"); + throw new System.Exception("Error in CargoMission.ClientReadInitial: item count does not match the server count (" + itemCount + " != " + items.Count + ", mission: " + Prefab.Identifier + ")"); } if (requiredDeliveryAmount == 0) { requiredDeliveryAmount = items.Count; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 67c14dc5f..6724f3ab0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -122,7 +122,7 @@ namespace Barotrauma }; chatSendButton.RectTransform.AbsoluteOffset = new Point((int)(InputBox.Rect.Height * 0.15f), 0); InputBox.TextBlock.RectTransform.MaxSize - = new Point((int)(InputBox.Rect.Width - chatSendButton.Rect.Width * 1.25f - InputBox.TextBlock.Padding.Z), int.MaxValue); + = new Point((int)(InputBox.Rect.Width - chatSendButton.Rect.Width * 1.25f - InputBox.TextBlock.Padding.X - chatSendButton.RectTransform.AbsoluteOffset.X), int.MaxValue); showNewMessagesButton = new GUIButton(new RectTransform(new Vector2(1f, 0.075f), GUIFrame.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0.0f, 0.125f) }, TextManager.Get("chat.shownewmessages")); showNewMessagesButton.OnClicked += (GUIButton btn, object userdata) => @@ -384,17 +384,6 @@ namespace Barotrauma prevUIScale = GUI.Scale; } - //hide chatbox when accessing the inventory of another character to prevent overlaps - if (Character.Controlled?.SelectedCharacter?.Inventory != null && - Character.Controlled.SelectedCharacter.CanInventoryBeAccessed) - { - SetVisibility(false); - } - else - { - SetVisibility(true); - } - if (showNewMessagesButton.Visible && chatBox.ScrollBar.BarScroll == 1f) { showNewMessagesButton.Visible = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 167e80dc1..0afc20985 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; @@ -38,7 +38,7 @@ namespace Barotrauma private static GUIDropDown fileTypeDropdown; private static GUIButton openButton; - private static FileSystemWatcher fileSystemWatcher; + private static System.IO.FileSystemWatcher fileSystemWatcher; private static string currentFileTypePattern; @@ -78,10 +78,10 @@ namespace Barotrauma currentDirectory += "/"; } fileSystemWatcher?.Dispose(); - fileSystemWatcher = new FileSystemWatcher(currentDirectory) + fileSystemWatcher = new System.IO.FileSystemWatcher(currentDirectory) { Filter = "*", - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName + NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName }; fileSystemWatcher.Created += OnFileSystemChanges; fileSystemWatcher.Deleted += OnFileSystemChanges; @@ -97,11 +97,11 @@ namespace Barotrauma set; } - private static void OnFileSystemChanges(object sender, FileSystemEventArgs e) + private static void OnFileSystemChanges(object sender, System.IO.FileSystemEventArgs e) { switch (e.ChangeType) { - case WatcherChangeTypes.Created: + case System.IO.WatcherChangeTypes.Created: { var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), e.Name) { @@ -114,15 +114,15 @@ namespace Barotrauma fileList.Content.RectTransform.SortChildren(SortFiles); } break; - case WatcherChangeTypes.Deleted: + case System.IO.WatcherChangeTypes.Deleted: { var itemFrame = fileList.Content.FindChild(c => (c is GUITextBlock tb) && (tb.Text == e.Name || tb.Text == e.Name + "/")); if (itemFrame != null) { fileList.RemoveChild(itemFrame); } } break; - case WatcherChangeTypes.Renamed: + case System.IO.WatcherChangeTypes.Renamed: { - RenamedEventArgs renameArgs = e as RenamedEventArgs; + System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs; var itemFrame = fileList.Content.FindChild(c => (c is GUITextBlock tb) && (tb.Text == renameArgs.OldName || tb.Text == renameArgs.OldName + "/")) as GUITextBlock; itemFrame.UserData = (bool?)Directory.Exists(e.FullPath); itemFrame.Text = renameArgs.Name; @@ -156,7 +156,7 @@ namespace Barotrauma public static void Init() { - backgroundFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null) + backgroundFrame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) { Color = Color.Black * 0.5f, HoverColor = Color.Black * 0.5f, @@ -169,10 +169,10 @@ namespace Barotrauma var horizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, window.RectTransform, Anchor.Center), true); sidebar = new GUIListBox(new RectTransform(new Vector2(0.29f, 1.0f), horizontalLayout.RectTransform)); - var drives = DriveInfo.GetDrives(); + var drives = System.IO.DriveInfo.GetDrives(); foreach (var drive in drives) { - if (drive.DriveType == DriveType.Ram) { continue; } + if (drive.DriveType == System.IO.DriveType.Ram) { continue; } if (ignoredDrivePrefixes.Any(p => drive.Name.StartsWith(p))) { continue; } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), sidebar.Content.RectTransform), drive.Name.Replace('\\','/')); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 17d668614..f2d904791 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.CharacterEditor; @@ -80,8 +80,8 @@ namespace Barotrauma public static readonly string[] colorComponentLabels = { "R", "G", "B", "A" }; public static Vector2 ReferenceResolution => new Vector2(1920f, 1080f); - public static float Scale => (GameMain.GraphicsWidth / ReferenceResolution.X + GameMain.GraphicsHeight / ReferenceResolution.Y) / 2.0f * GameSettings.HUDScale; - public static float xScale => GameMain.GraphicsWidth / ReferenceResolution.X * GameSettings.HUDScale; + public static float Scale => (UIWidth / ReferenceResolution.X + GameMain.GraphicsHeight / ReferenceResolution.Y) / 2.0f * GameSettings.HUDScale; + public static float xScale => UIWidth / ReferenceResolution.X * GameSettings.HUDScale; public static float yScale => GameMain.GraphicsHeight / ReferenceResolution.Y * GameSettings.HUDScale; public static int IntScale(float f) => (int)(f * Scale); public static int IntScaleFloor(float f) => (int)Math.Floor(f * Scale); @@ -90,6 +90,23 @@ namespace Barotrauma public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth; public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y); public static float RelativeVerticalAspectRatio => VerticalAspectRatio / (ReferenceResolution.Y / ReferenceResolution.X); + public static bool IsUltrawide => HorizontalAspectRatio > 2.0f; + + public static int UIWidth + { + get + { + // Ultrawide + if (IsUltrawide) + { + return (int)(GameMain.GraphicsHeight * ReferenceResolution.X / ReferenceResolution.Y); + } + else + { + return GameMain.GraphicsWidth; + } + } + } public static float SlicedSpriteScale { @@ -521,6 +538,37 @@ namespace Barotrauma DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)anchorPivotStringSize.X - padding, yPos), anchorPivotString, Color.LightGreen, Color.Black, 0, SmallFont); yPos += (int)anchorPivotStringSize.Y + padding / 2; } + else + { + string guiScaleString = $"GUI.Scale: {Scale}"; + string guixScaleString = $"GUI.xScale: {xScale}"; + string guiyScaleString = $"GUI.yScale: {yScale}"; + string relativeHorizontalAspectRatioString = $"RelativeHorizontalAspectRatio: {RelativeHorizontalAspectRatio}"; + string relativeVerticalAspectRatioString = $"RelativeVerticalAspectRatio: {RelativeVerticalAspectRatio}"; + Vector2 guiScaleStringSize = SmallFont.MeasureString(guiScaleString); + Vector2 guixScaleStringSize = SmallFont.MeasureString(guixScaleString); + Vector2 guiyScaleStringSize = SmallFont.MeasureString(guiyScaleString); + Vector2 relativeHorizontalAspectRatioStringSize = SmallFont.MeasureString(relativeHorizontalAspectRatioString); + Vector2 relativeVerticalAspectRatioStringSize = SmallFont.MeasureString(relativeVerticalAspectRatioString); + + int padding = IntScale(10); + int yPos = padding; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guiScaleStringSize.X - padding, yPos), guiScaleString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)guiScaleStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guixScaleStringSize.X - padding, yPos), guixScaleString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)guixScaleStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guiyScaleStringSize.X - padding, yPos), guiyScaleString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)guiyScaleStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)relativeHorizontalAspectRatioStringSize.X - padding, yPos), relativeHorizontalAspectRatioString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)relativeHorizontalAspectRatioStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)relativeVerticalAspectRatioStringSize.X - padding, yPos), relativeVerticalAspectRatioString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)relativeVerticalAspectRatioStringSize.Y + padding / 2; + } } if (HUDLayoutSettings.DebugDraw) HUDLayoutSettings.Draw(spriteBatch); @@ -1917,8 +1965,9 @@ namespace Barotrauma Inventory.draggingItem = null; Inventory.DraggingInventory = null; - PauseMenu = new GUIFrame(new RectTransform(Vector2.One, Canvas), style: null, color: Color.Black * 0.5f); - + PauseMenu = new GUIFrame(new RectTransform(Vector2.One, Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, PauseMenu.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + var pauseMenuInner = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.3f), PauseMenu.RectTransform, Anchor.Center) { MinSize = new Point(250, 300) }); var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.6f), pauseMenuInner.RectTransform, Anchor.Center)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs index 2ad741c20..8595c032e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs @@ -8,7 +8,7 @@ namespace Barotrauma { public class GUICanvas : RectTransform { - protected GUICanvas() : base(Vector2.One, parent: null) { } + protected GUICanvas() : base(size, parent: null) { } private static GUICanvas _instance; public static GUICanvas Instance @@ -22,15 +22,63 @@ namespace Barotrauma { GameMain.Instance.OnResolutionChanged += RecalculateSize; } + _instance.ItemComponentHolder = new GUIFrame(new RectTransform(Vector2.One, _instance, Anchor.Center)).RectTransform; } return _instance; } } + public RectTransform ItemComponentHolder; + + private static Vector2 size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); + + protected override Rectangle NonScaledUIRect => UIRect; + + private enum ResizeAxis { Both = 0, X = 1, Y = 2 } + // Turn public, if there is a need to call this manually. private static void RecalculateSize() { - Instance.Resize(Vector2.One, resizeChildren: true); + Vector2 recalculatedSize = size; + + // Scale children that are supposed to encompass the whole screen so that they are properly scaled on ultrawide as well + for (int i = 0; i < Instance.Children.Count(); i++) + { + RectTransform target = Instance.GetChild(i); + if (target == null || target.RelativeSize.X < 1 && target.RelativeSize.Y < 1) continue; + + ResizeAxis axis; + + if (target.RelativeSize.X >= 1 && target.RelativeSize.Y >= 1) + { + axis = ResizeAxis.Both; + } + else if (target.RelativeSize.X >= 1) + { + axis = ResizeAxis.X; + } + else + { + axis = ResizeAxis.Y; + } + + switch (axis) + { + case ResizeAxis.Both: + target.RelativeSize = recalculatedSize; + break; + + case ResizeAxis.X: + target.RelativeSize = new Vector2(recalculatedSize.X, target.RelativeSize.Y); + break; + + case ResizeAxis.Y: + target.RelativeSize = new Vector2(target.RelativeSize.X, recalculatedSize.Y); + break; + } + } + + Instance.Resize(size, resizeChildren: true); Instance.GetAllChildren().Select(c => c.GUIComponent as GUITextBlock).ForEach(t => t?.SetTextPos()); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index b8e4012a1..76f412e71 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -5,7 +5,7 @@ using System.Linq; using Barotrauma.Extensions; using System; using System.Xml.Linq; -using System.IO; +using Barotrauma.IO; using RestSharp; using System.Net; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index f4b41a3e8..d1560e7ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -394,7 +394,7 @@ namespace Barotrauma if (Dropped) { - listBox.AddToGUIUpdateList(false, UpdateOrder); + listBox.AddToGUIUpdateList(false, 1); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs index 913a701fd..e8a601c8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs @@ -10,7 +10,17 @@ namespace Barotrauma public class GUIImage : GUIComponent { //paths of the textures that are being loaded asynchronously - private static readonly HashSet activeTextureLoads = new HashSet(); + private static readonly List activeTextureLoads = new List(); + + private static bool loadingTextures; + + public static bool LoadingTextures + { + get + { + return loadingTextures; + } + } public float Rotation; @@ -25,7 +35,7 @@ namespace Barotrauma private bool lazyLoaded, loading; public bool LoadAsynchronously; - + public bool Crop { get @@ -122,6 +132,7 @@ namespace Barotrauma { if (LoadAsynchronously) { + loadingTextures = true; loading = true; TaskPool.Add(LoadTextureAsync(), (Task) => { @@ -223,6 +234,7 @@ namespace Barotrauma lock (activeTextureLoads) { activeTextureLoads.Remove(Sprite.FullPath); + loadingTextures = activeTextureLoads.Count > 0; } } return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs index 41f708b39..e2a351874 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs @@ -76,19 +76,7 @@ namespace Barotrauma public GUILayoutGroup(RectTransform rectT, bool isHorizontal = false, Anchor childAnchor = Anchor.TopLeft) : base(null, rectT) { -#if DEBUG - if (GameMain.DebugDraw) - { - CanBeFocused = true; - } - else - { -#endif - CanBeFocused = false; -#if DEBUG - } -#endif - + CanBeFocused = false; this.isHorizontal = isHorizontal; this.childAnchor = childAnchor; rectT.ChildrenChanged += (child) => needsToRecalculate = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index b9091d17c..7a48e553c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -11,7 +11,7 @@ namespace Barotrauma public static List MessageBoxes = new List(); private static int DefaultWidth { - get { return Math.Max(400, 400 * (GameMain.GraphicsWidth / 1920)); } + get { return Math.Max(400, (int)(400 * (GameMain.GraphicsWidth / GUI.ReferenceResolution.X))); } } private float inGameCloseTimer = 0.0f; @@ -63,7 +63,7 @@ namespace Barotrauma } public GUIMessageBox(string headerText, string text, string[] buttons, Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, Type type = Type.Default, string tag = "", Sprite icon = null) - : base(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: GUI.Style.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") + : base(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: GUI.Style.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") { int width = (int)(DefaultWidth * (type == Type.Default ? 1.0f : 1.5f)), height = 0; if (relativeSize.HasValue) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index faedb8243..0725d9dcd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -510,6 +510,8 @@ namespace Barotrauma var currPosition = positions[0]; + float topY = positions.Min(p => p.Item1.Y); + for (int i = 1; i < positions.Count; i++) { var p1 = positions[i]; @@ -527,7 +529,7 @@ namespace Barotrauma else { diffY = Math.Abs(p1.Item1.Y - pos.Y); - if (diffY < halfHeight) + if (diffY < halfHeight || (p1.Item1.Y == topY && pos.Y < topY)) { //we are on this line, select the nearest character float diffX = Math.Abs(p1.Item1.X - pos.X) - Math.Abs(p2.Item1.X - pos.X); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 93be1de8d..9be942058 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -67,6 +67,8 @@ namespace Barotrauma private Vector2 selectionEndPos; private Vector2 selectionRectSize; + private bool mouseHeldInside; + private readonly Memento memento = new Memento(); // Skip one update cycle, fixes Enter key instantly deselecting the chatbox @@ -414,6 +416,7 @@ namespace Barotrauma State = ComponentState.Hover; if (PlayerInput.PrimaryMouseButtonDown()) { + mouseHeldInside = true; Select(); } else @@ -436,7 +439,11 @@ namespace Barotrauma } else { - if ((PlayerInput.LeftButtonClicked() || PlayerInput.RightButtonClicked()) && selected) Deselect(); + if ((PlayerInput.LeftButtonClicked() || PlayerInput.RightButtonClicked()) && selected) + { + if (!mouseHeldInside) { Deselect(); } + mouseHeldInside = false; + } isSelecting = false; State = ComponentState.None; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 8df88614e..6be9c9283 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -50,10 +50,6 @@ namespace Barotrauma get; private set; } - /*public static Rectangle HealthBarAreaRight - { - get; private set; - }*/ public static Rectangle HealthBarArea { get; private set; @@ -120,17 +116,11 @@ namespace Barotrauma //horizontal slices at the corners of the screen for health bar and affliction icons int afflictionAreaHeight = (int)(50 * GUI.Scale); - int healthBarWidth = BottomRightInfoArea.Width + CharacterInventory.SlotSize.X + CharacterInventory.Spacing * 2 + CharacterInventory.HideButtonWidth; + int healthBarWidth = (int)(BottomRightInfoArea.Width * 1.58f); int healthBarHeight = (int)(50f * GUI.Scale); - HealthBarArea = new Rectangle(BottomRightInfoArea.X - (healthBarWidth - BottomRightInfoArea.Width) + (int)(2 * GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + (int)(10 * GUI.Scale), healthBarWidth, healthBarHeight); - AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); - - //HealthBarAreaRight = new Rectangle(Padding, GameMain.GraphicsHeight - healthBarHeight - Padding, healthBarWidth, healthBarHeight); - /*if (HealthBarAreaRight.Y + healthBarHeight * 0.75f < PortraitArea.Y) - { - HealthBarAreaRight = new Rectangle(GameMain.GraphicsWidth - Padding - healthBarWidth, HealthBarAreaRight.Y, HealthBarAreaRight.Width, HealthBarAreaRight.Height); - }*/ - //AfflictionAreaRight = new Rectangle(HealthBarAreaRight.X, HealthBarAreaRight.Y + healthBarHeight + Padding, healthBarWidth, afflictionAreaHeight); + HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight); + AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); + int messageAreaWidth = GameMain.GraphicsWidth / 3; MessageAreaTop = new Rectangle((GameMain.GraphicsWidth - messageAreaWidth) / 2, ButtonAreaTop.Bottom, messageAreaWidth, ButtonAreaTop.Height); @@ -146,7 +136,7 @@ namespace Barotrauma CrewArea = new Rectangle(Padding, Padding, (int)Math.Max(400 * GUI.Scale, 220), ObjectiveAnchor.Top - Padding * 2); - InventoryAreaLower = new Rectangle(Padding, inventoryTopY, GameMain.GraphicsWidth - Padding * 2, GameMain.GraphicsHeight - inventoryTopY); + InventoryAreaLower = new Rectangle(ChatBoxArea.Right + Padding * 7, inventoryTopY, GameMain.GraphicsWidth - Padding * 9 - ChatBoxArea.Width, GameMain.GraphicsHeight - inventoryTopY); int healthWindowWidth = (int)(GameMain.GraphicsWidth * 0.5f); int healthWindowHeight = (int)(GameMain.GraphicsWidth * 0.5f * 0.65f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index d810b720d..8f7c9d2bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -253,11 +253,12 @@ namespace Barotrauma return _rect; } } - public Rectangle ParentRect => Parent != null ? Parent.Rect : ScreenRect; - + public Rectangle ParentRect => Parent != null ? Parent.Rect : UIRect; protected Rectangle NonScaledRect => new Rectangle(NonScaledTopLeft, NonScaledSize); - protected Rectangle NonScaledParentRect => parent != null ? Parent.NonScaledRect : ScreenRect; - protected Rectangle ScreenRect => new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); + protected virtual Rectangle NonScaledUIRect => NonScaledRect; + protected Rectangle NonScaledParentRect => parent != null ? Parent.NonScaledRect : UIRect; + protected Rectangle NonScaledParentUIRect => parent != null ? Parent.NonScaledUIRect : UIRect; + protected Rectangle UIRect => new Rectangle(0, 0, GUI.UIWidth, GameMain.GraphicsHeight); private Pivot pivot; /// @@ -444,14 +445,14 @@ namespace Barotrauma protected void RecalculateRelativeSize() { - relativeSize = new Vector2(NonScaledSize.X, NonScaledSize.Y) / new Vector2(NonScaledParentRect.Width, NonScaledParentRect.Height); + relativeSize = new Vector2(NonScaledSize.X, NonScaledSize.Y) / new Vector2(NonScaledParentUIRect.Width, NonScaledParentUIRect.Height); recalculateRect = true; SizeChanged?.Invoke(); } protected void RecalculateAbsoluteSize() { - Point size = NonScaledParentRect.Size; + Point size = NonScaledParentUIRect.Size; switch (ScaleBasis) { case ScaleBasis.BothWidth: diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 22fa899be..deea61de3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -177,7 +177,8 @@ namespace Barotrauma { tabButtons.Clear(); - infoFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); + infoFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, infoFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); switch (selectedTab) { @@ -488,6 +489,12 @@ namespace Barotrauma Color = (GameMain.NetworkMember != null && GameMain.Client.Character == character) ? ownCharacterBGColor : Color.Transparent }; + frame.OnSecondaryClicked += (component, data) => + { + GameMain.GameSession?.CrewManager?.CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + return true; + }; + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) { AbsoluteSpacing = 2 @@ -593,6 +600,8 @@ namespace Barotrauma { GUITextBlock characterNameBlock; Sprite permissionIcon = GetPermissionIcon(client); + JobPrefab prefab = client.Character?.Info?.Job?.Prefab; + Color nameColor = prefab != null ? prefab.UIColor : Color.White; if (permissionIcon != null) { @@ -600,7 +609,7 @@ namespace Barotrauma float characterNameWidthAdjustment = (iconSize.X + paddedFrame.AbsoluteSpacing) / characterColumnWidth; characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(client.Name, GUI.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: client.Character != null ? client.Character.Info.Job.Prefab.UIColor : Color.White); + ToolBox.LimitString(client.Name, GUI.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: nameColor); float iconWidth = iconSize.X / (float)characterColumnWidth; int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUI.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width); @@ -609,7 +618,7 @@ namespace Barotrauma else { characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(client.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: client.Character != null ? client.Character.Info.Job.Prefab.UIColor : Color.White); + ToolBox.LimitString(client.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: nameColor); } if (client.Character != null && client.Character.IsDead) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs index af99de492..d3feabe8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs @@ -3,7 +3,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Xml.Linq; using Barotrauma.Media; -using System.IO; +using Barotrauma.IO; using Microsoft.Xna.Framework.Input; namespace Barotrauma @@ -56,7 +56,7 @@ namespace Barotrauma public VideoPlayer() // GUI elements with size set to Point.Zero are resized based on content { - int screenWidth = (int)(GameMain.GraphicsWidth * 0.55f); + int screenWidth = (int)(GameMain.GraphicsWidth * 0.65f); scaledVideoResolution = new Point(screenWidth, (int)(screenWidth / 16f * 9f)); int width = scaledVideoResolution.X; @@ -178,6 +178,7 @@ namespace Barotrauma videoFrame.RectTransform.NonScaledSize = scaledVideoResolution + new Point(scaledBorderSize, scaledBorderSize); videoView.RectTransform.NonScaledSize = scaledVideoResolution; + videoFrame.RectTransform.AbsoluteOffset = new Point(0, videoFrame.RectTransform.NonScaledSize.Y); title.RectTransform.NonScaledSize = new Point(scaledTextWidth, scaledTitleHeight); title.RectTransform.AbsoluteOffset = new Point((int)(5 * GUI.Scale), (int)(10 * GUI.Scale)); @@ -247,7 +248,7 @@ namespace Barotrauma } else { - videoFrame.RectTransform.AbsoluteOffset = new Point(0, (int)(100 * GUI.Scale)); + videoFrame.RectTransform.AbsoluteOffset = new Point(0, 0); okButton = new GUIButton(new RectTransform(scaledButtonSize, videoFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft) { AbsoluteOffset = new Point(scaledBorderSize, scaledBorderSize) }, TextManager.Get("Back")) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 2224d5cfc..3d4a8018d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -12,7 +12,7 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using GameAnalyticsSDK.Net; -using System.IO; +using Barotrauma.IO; using System.Threading; using Barotrauma.Tutorials; using Barotrauma.Media; @@ -532,6 +532,8 @@ namespace Barotrauma ScriptedEventSet.LoadPrefabs(); AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); + Order.Init(); + EventManagerSettings.Init(); TitleScreen.LoadState = 50.0f; yield return CoroutineStatus.Running; @@ -860,7 +862,7 @@ namespace Barotrauma //TODO: do we need to check Inventory.SelectedSlot? && Inventory.SelectedSlot == null && CharacterHealth.OpenHealthWindow == null && !CrewManager.IsCommandInterfaceOpen - && !(Screen.Selected is SubEditorScreen editor && !editor.WiringMode && Character.Controlled.SelectedConstruction != null)) + && !(Screen.Selected is SubEditorScreen editor && !editor.WiringMode && Character.Controlled?.SelectedConstruction != null)) { // Otherwise toggle pausing, unless another window/interface is open. GUI.TogglePauseMenu(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 4ad42edb8..044ec7c39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -1264,7 +1264,7 @@ namespace Barotrauma } else { - CreateCommandUI(HUDLayoutSettings.PortraitArea.Contains(PlayerInput.MousePosition) ? Character.Controlled : GUI.MouseOn?.UserData as Character); + CreateCommandUI(HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) ? Character.Controlled : GUI.MouseOn?.UserData as Character); } GUI.PlayUISound(GUISoundType.PopupMenu); clicklessSelectionActive = isOpeningClick = true; @@ -1288,18 +1288,9 @@ namespace Barotrauma else if (PlayerInput.SecondaryMouseButtonClicked() && characterContext == null && (optionNodes.Any(n => GUI.IsMouseOn(n.Item1)) || shortcutNodes.Any(n => GUI.IsMouseOn(n)))) { - var node = optionNodes.Find(n => GUI.IsMouseOn(n.Item1))?.Item1; - if (node == null) - { - node = shortcutNodes.Find(n => GUI.IsMouseOn(n)); - } - // Make sure the node is for an option-less order... - if (node.UserData is Order order && !order.HasOptions) - { - CreateAssignmentNodes(node); - } - // ...or an order option - else if (node.UserData is Tuple) + var node = optionNodes.Find(n => GUI.IsMouseOn(n.Item1))?.Item1 ?? shortcutNodes.Find(n => GUI.IsMouseOn(n)); + // Make sure the node is for an option-less order or an order option + if ((node.UserData is Order order && !order.HasOptions && (!order.MustSetTarget || itemContext != null)) || node.UserData is Tuple) { CreateAssignmentNodes(node); } @@ -1543,31 +1534,31 @@ namespace Barotrauma } private GUIFrame commandFrame, targetFrame; private GUIButton centerNode, returnNode, expandNode, shortcutCenterNode; - private List> optionNodes = new List>(); + private readonly List> optionNodes = new List>(); private Keys returnNodeHotkey = Keys.None, expandNodeHotkey = Keys.None; - private List shortcutNodes = new List(); - private List extraOptionNodes = new List(); + private readonly List shortcutNodes = new List(); + private readonly List extraOptionNodes = new List(); private GUICustomComponent nodeConnectors; private GUIImage background; private GUIButton selectedNode; - private float selectionTime = 0.75f, timeSelected = 0.0f; + private readonly float selectionTime = 0.75f; + private float timeSelected = 0.0f; private bool clicklessSelectionActive, isOpeningClick, isSelectionHighlighted; - private Point centerNodeSize, nodeSize, shortcutCenterNodeSize, shortcutNodeSize, returnNodeSize; + private Point centerNodeSize, nodeSize, shortcutCenterNodeSize, shortcutNodeSize, returnNodeSize, assignmentNodeSize; private float centerNodeMargin, optionNodeMargin, shortcutCenterNodeMargin, shortcutNodeMargin, returnNodeMargin; private List availableCategories; private Stack historyNodes = new Stack(); - private List extraOptionCharacters = new List(); + private readonly List extraOptionCharacters = new List(); /// /// node.Color = node.HighlightColor * nodeColorMultiplier /// private const float nodeColorMultiplier = 0.75f; - private const int assignmentNodeMaxCount = 8; private int nodeDistance = (int)(GUI.Scale * 250); - private float returnNodeDistanceModifier = 0.65f; + private const float returnNodeDistanceModifier = 0.65f; private Order dismissedOrderPrefab; private Character characterContext; private Item itemContext; @@ -1576,6 +1567,7 @@ namespace Barotrauma private readonly List contextualOrders = new List(); private Point shorcutCenterNodeOffset; private const int maxShorcutNodeCount = 4; + private bool WasCommandInterfaceDisabledThisUpdate { get; set; } private bool CanIssueOrders { @@ -1584,7 +1576,7 @@ namespace Barotrauma #if DEBUG return Character.Controlled == null || Character.Controlled.Info != null && Character.Controlled.SpeechImpediment < 100.0f; #else - return Character.Controlled != null && Character.Controlled.SpeechImpediment < 100.0f; + return Character.Controlled?.Info != null && Character.Controlled.SpeechImpediment < 100.0f; #endif } } @@ -1743,16 +1735,21 @@ namespace Barotrauma private void ScaleCommandUI() { - centerNodeSize = new Point((int)(100 * GUI.Scale)); + // Node sizes nodeSize = new Point((int)(100 * GUI.Scale)); - shortcutCenterNodeSize = new Point((int)(48 * GUI.Scale)); - shortcutNodeSize = new Point((int)(64 * GUI.Scale)); + centerNodeSize = nodeSize; returnNodeSize = new Point((int)(48 * GUI.Scale)); + assignmentNodeSize = new Point((int)(64 * GUI.Scale)); + shortcutCenterNodeSize = returnNodeSize; + shortcutNodeSize = assignmentNodeSize; + + // Node margins (used in drawing the connecting lines) centerNodeMargin = centerNodeSize.X * 0.5f; optionNodeMargin = nodeSize.X * 0.5f; shortcutCenterNodeMargin = shortcutCenterNodeSize.X * 0.45f; shortcutNodeMargin = shortcutNodeSize.X * 0.5f; returnNodeMargin = returnNodeSize.X * 0.5f; + nodeDistance = (int)(150 * GUI.Scale); shorcutCenterNodeOffset = new Point(0, (int)(1.25f * nodeDistance)); } @@ -1783,12 +1780,12 @@ namespace Barotrauma { if (centerNode == null || optionNodes == null) { return; } var startNodePos = centerNode.Rect.Center.ToVector2(); - if (targetFrame == null || !targetFrame.Visible) + // Don't draw connectors for mini map options or assignment nodes + if ((targetFrame == null || !targetFrame.Visible) && !(optionNodes.FirstOrDefault()?.Item1.UserData is Character)) { optionNodes.ForEach(n => DrawNodeConnector(startNodePos, centerNodeMargin, n.Item1, optionNodeMargin, spriteBatch)); } DrawNodeConnector(startNodePos, centerNodeMargin, returnNode, returnNodeMargin, spriteBatch); - DrawNodeConnector(startNodePos, centerNodeMargin, expandNode, optionNodeMargin, spriteBatch); if (shortcutCenterNode == null || !shortcutCenterNode.Visible) { return; } DrawNodeConnector(startNodePos, centerNodeMargin, shortcutCenterNode, shortcutCenterNodeMargin, spriteBatch); startNodePos = shortcutCenterNode.Rect.Center.ToVector2(); @@ -1850,6 +1847,7 @@ namespace Barotrauma shortcutNodes.Remove(node); }; RemoveOptionNodes(); + if (returnNode != null) { returnNode.RemoveChild(returnNode.GetChildByUserData("hotkey")); @@ -1857,15 +1855,20 @@ namespace Barotrauma returnNode.Visible = false; historyNodes.Push(returnNode); } - SetReturnNode(centerNode, new Point( - (int)(node.RectTransform.AbsoluteOffset.X * -returnNodeDistanceModifier), - (int)(node.RectTransform.AbsoluteOffset.Y * -returnNodeDistanceModifier))); + + // When the mini map is shown, always position the return node on the bottom + var offset = node?.UserData is Order order && order.GetMatchingItems(true).Count > 1 ? + new Point(0, (int)(returnNodeDistanceModifier * nodeDistance)) : + node.RectTransform.AbsoluteOffset.Multiply(-returnNodeDistanceModifier); + SetReturnNode(centerNode, offset); + SetCenterNode(node); if (shortcutCenterNode != null) { commandFrame.RemoveChild(shortcutCenterNode); shortcutCenterNode = null; } + CreateNodes(userData); CreateReturnNodeHotkey(); return true; @@ -1914,9 +1917,14 @@ namespace Barotrauma } } - private void SetCenterNode(GUIButton node) + private void SetCenterNode(GUIButton node, bool resetAnchor = false) { node.RectTransform.Parent = commandFrame.RectTransform; + if (resetAnchor) + { + node.RectTransform.SetPosition(Anchor.Center); + } + node.RectTransform.SetPosition(Anchor.Center); node.RectTransform.MoveOverTime(Point.Zero, CommandNodeAnimDuration); node.RectTransform.ScaleOverTime(centerNodeSize, CommandNodeAnimDuration); node.RemoveChild(node.GetChildByUserData("hotkey")); @@ -2021,14 +2029,13 @@ namespace Barotrauma private void CreateShortcutNodes() { - var sub = Character.Controlled != null && Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1 ? - Submarine.MainSubs[1] : Submarine.MainSub; + Submarine sub = GetTargetSubmarine(); if (sub == null) { return; } shortcutNodes.Clear(); - if (shortcutNodes.Count < maxShorcutNodeCount && sub.GetItems(false).Find(i => i.HasTag("reactor"))?.GetComponent() is Reactor reactor) + if (shortcutNodes.Count < maxShorcutNodeCount && sub.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) { var reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor @@ -2037,8 +2044,9 @@ namespace Barotrauma reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) { var order = new Order(Order.GetPrefab("operatereactor"), reactor.Item, reactor, Character.Controlled); + var option = order.Prefab.Options[0]; shortcutNodes.Add( - CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, order.Prefab.Options[0], order.Prefab.OptionNames[0], -1)); + CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, option, order.Prefab.GetOptionName(option), -1)); } } @@ -2046,7 +2054,7 @@ namespace Barotrauma // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("captain")) && - sub.GetItems(false).Find(i => i.HasTag("navterminal")) is Item nav && characters.None(c => c.SelectedConstruction == nav) && + sub.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { shortcutNodes.Add( @@ -2097,7 +2105,10 @@ namespace Barotrauma (n.UserData is Tuple orderWithOption && orderWithOption.Item1.Identifier == orderIdentifier)) && !orderPrefab.TargetAllCharacters && orderPrefab.Category != null) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); + if (!orderPrefab.MustSetTarget || orderPrefab.GetMatchingItems(sub, true).Any()) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); + } if (shortcutNodes.Count >= maxShorcutNodeCount) { break; } } } @@ -2129,16 +2140,19 @@ namespace Barotrauma private void CreateOrderNodes(OrderCategory orderCategory) { - // TODO: Save the available orders for each category so we don't have to check them again during the same game? var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.TargetAllCharacters); - orders.RemoveAll(o => (o.ItemComponentType != null || o.ItemIdentifiers.Length > 0) && o.MustSetTarget && o.GetMatchingItems(true).None()); + Order order; + bool disableNode; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(orders.Count), GetFirstNodeAngle(orders.Count)); for (int i = 0; i < orders.Count; i++) { + order = orders[i]; + disableNode = !CanSomeoneHearCharacter() || + (order.MustSetTarget && (order.ItemComponentType != null || order.ItemIdentifiers.Length > 0) && order.GetMatchingItems(true).None()); optionNodes.Add(new Tuple( - CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), orders[i], (i + 1) % 10), - CanSomeoneHearCharacter() ? Keys.D0 + (i + 1) % 10 : Keys.None)); + CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), + !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); } } @@ -2150,7 +2164,7 @@ namespace Barotrauma if (contextualOrders.None()) { // Check if targeting an item or a hull - if (itemContext != null) + if (itemContext != null && !itemContext.NonInteractable) { foreach (Order p in Order.PrefabList) { @@ -2165,11 +2179,11 @@ namespace Barotrauma // If targeting a periscope connected to a turret, show the 'operateweapons' order var orderIdentifier = "operateweapons"; var operateWeaponsPrefab = Order.GetPrefab(orderIdentifier); - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Components.Any(c => c is Controller) && - (itemContext.GetConnectedComponents().Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)) || - itemContext.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)))) + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Components.Any(c => c is Controller)) { - contextualOrders.Add(operateWeaponsPrefab); + var turret = itemContext.GetConnectedComponents().FirstOrDefault(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)) ?? + itemContext.GetConnectedComponents(recursive: true).FirstOrDefault(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)); + if (turret != null) { contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled)); } } // If targeting a repairable item, show the 'repairsystems' order @@ -2193,6 +2207,14 @@ namespace Barotrauma { contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, null, Character.Controlled)); } + + // Remove the 'pumpwater' order if the target pump is auto-controlled (as it will immediately overwrite the work done by the bot) + orderIdentifier = "pumpwater"; + if (contextualOrders.FirstOrDefault(o => o.Identifier.Equals(orderIdentifier)) is Order o && + itemContext.Components.FirstOrDefault(c => c.GetType() == o.ItemComponentType) is Pump pump) + { + if (pump.IsAutoControlled) { contextualOrders.Remove(o); } + } } else if(hullContext != null) { @@ -2218,11 +2240,12 @@ namespace Barotrauma } var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); + bool disableNode = !CanSomeoneHearCharacter(); for (int i = 0; i < contextualOrders.Count; i++) { optionNodes.Add(new Tuple( - CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), contextualOrders[i], (i + 1) % 10), - CanSomeoneHearCharacter() ? Keys.D0 + (i + 1) % 10 : Keys.None)); + CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), contextualOrders[i], (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), + !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); } } @@ -2239,7 +2262,7 @@ namespace Barotrauma item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier))); } - private GUIButton CreateOrderNode(Point size, RectTransform parent, Point offset, Order order, int hotkey) + private GUIButton CreateOrderNode(Point size, RectTransform parent, Point offset, Order order, int hotkey, bool disableNode = false, bool checkIfOrderCanBeHeard = true) { var node = new GUIButton( new RectTransform(size, parent: parent, anchor: Anchor.Center), style: null) @@ -2249,16 +2272,17 @@ namespace Barotrauma node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); - var canSomeoneHearCharacter = CanSomeoneHearCharacter(); + if (checkIfOrderCanBeHeard && !disableNode) { disableNode = !CanSomeoneHearCharacter(); } + var mustSetOptionOrTarget = order.HasOptions || (order.MustSetTarget && itemContext == null); node.OnClicked = (button, userData) => { - if (!canSomeoneHearCharacter || !CanIssueOrders) { return false; } + if (disableNode || !CanIssueOrders) { return false; } var o = userData as Order; if (o.MustManuallyAssign && characterContext == null) { CreateAssignmentNodes(node); } - else if (o.HasOptions) + else if (mustSetOptionOrTarget) { NavigateForward(button, userData); } @@ -2270,11 +2294,11 @@ namespace Barotrauma return true; }; var icon = CreateNodeIcon(node.RectTransform, order.SymbolSprite, order.Color, - tooltip: order.HasOptions || characterContext != null ? order.Name : order.Name + + tooltip: mustSetOptionOrTarget || characterContext != null ? order.Name : order.Name + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); - - if (!canSomeoneHearCharacter) + + if (disableNode) { node.CanBeFocused = icon.CanBeFocused = false; CreateBlockIcon(node.RectTransform); @@ -2288,11 +2312,7 @@ namespace Barotrauma private void CreateOrderOptions(Order order) { - // This is largely based on the CreateOrderTargetFrame() method - - Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1 ? - Submarine.MainSubs[1] : - Submarine.MainSub; + Submarine submarine = GetTargetSubmarine(); var matchingItems = (itemContext == null && order.MustSetTarget) ? order.GetMatchingItems(submarine, true) : new List(); //more than one target item -> create a minimap-like selection with a pic of the sub @@ -2332,7 +2352,7 @@ namespace Barotrauma UserData = submarine }; - List optionFrames = new List(); + List optionElements = new List(); foreach (Item item in matchingItems) { var itemTargetFrame = targetFrame.Children.First().FindChild(item); @@ -2353,52 +2373,87 @@ namespace Barotrauma anchor = Anchor.TopRight; } - var optionFrame = new GUIFrame( - new RectTransform( - new Point((int)(250 * GUI.Scale), (int)((40 + order.Options.Length * 40) * GUI.Scale)), - parent: itemTargetFrame.RectTransform, - anchor: anchor), - style: "InnerFrame"); - - new GUIFrame( - new RectTransform(Vector2.One, optionFrame.RectTransform, anchor: Anchor.Center), - style: "OuterGlow", - color: Color.Black * 0.7f); - - var optionContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f), optionFrame.RectTransform, anchor: Anchor.Center)) + GUIComponent optionElement; + if (order.Options.Length > 1) { - RelativeSpacing = 0.05f, - Stretch = true - }; + optionElement = new GUIFrame( + new RectTransform( + new Point((int)(250 * GUI.Scale), (int)((40 + order.Options.Length * 40) * GUI.Scale)), + parent: itemTargetFrame.RectTransform, + anchor: anchor), + style: "InnerFrame"); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), optionContainer.RectTransform), item != null ? item.Name : order.Name); + new GUIFrame( + new RectTransform(Vector2.One, optionElement.RectTransform, anchor: Anchor.Center), + style: "OuterGlow", + color: Color.Black * 0.7f); - for (int i = 0; i < order.Options.Length; i++) - { - optionNodes.Add(new Tuple( - new GUIButton( - new RectTransform(new Vector2(1.0f, 0.2f), optionContainer.RectTransform), - text: order.OptionNames[i], - style: "GUITextBox") - { - UserData = new Tuple( - item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)), - order.Options[i]), - Font = GUI.SmallFont, - OnClicked = (_, userData) => + var optionContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f), optionElement.RectTransform, anchor: Anchor.Center)) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), optionContainer.RectTransform), item != null ? item.Name : order.Name); + + for (int i = 0; i < order.Options.Length; i++) + { + optionNodes.Add(new Tuple( + new GUIButton( + new RectTransform(new Vector2(1.0f, 0.2f), optionContainer.RectTransform), + text: order.GetOptionName(i), + style: "GUITextBox") { - if (!CanIssueOrders) { return false; } - var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); - DisableCommandUI(); - return true; - } - }, - Keys.None)); + UserData = new Tuple( + item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)), + order.Options[i]), + Font = GUI.SmallFont, + OnClicked = (_, userData) => + { + if (!CanIssueOrders) { return false; } + var o = userData as Tuple; + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); + DisableCommandUI(); + return true; + } + }, + Keys.None)); + } } - optionFrames.Add(optionFrame); + else + { + var userData = new Tuple(item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)), ""); + optionElement = new GUIButton( + new RectTransform( + new Point((int)(50 * GUI.Scale)), + parent: itemTargetFrame.RectTransform, + anchor: anchor), + style: null) + { + UserData = userData, + Font = GUI.SmallFont, + ToolTip = item?.Name ?? order.Name, + OnClicked = (_, userData) => + { + if (!CanIssueOrders) { return false; } + var o = userData as Tuple; + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); + DisableCommandUI(); + return true; + } + }; + + Sprite icon = null; + order.MinimapIcons?.TryGetValue(item.Prefab.Identifier, out icon); + var colorMultiplier = characters.Any(c => c.CurrentOrder != null && + c.CurrentOrder.Identifier == userData.Item1.Identifier && + c.CurrentOrder.TargetEntity == userData.Item1.TargetEntity) ? 0.5f : 1f; + CreateNodeIcon(optionElement.RectTransform, icon ?? order.SymbolSprite, order.Color * colorMultiplier); + optionNodes.Add(new Tuple(optionElement, Keys.None)); + } + optionElements.Add(optionElement); } - GUI.PreventElementOverlap(optionFrames, clampArea: new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20)); + GUI.PreventElementOverlap(optionElements, clampArea: new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20)); var shadow = new GUIFrame( new RectTransform(targetFrame.Rect.Size + new Point((int)(200 * GUI.Scale)), targetFrame.RectTransform, anchor: Anchor.Center), @@ -2420,7 +2475,7 @@ namespace Barotrauma for (int i = 0; i < order.Options.Length; i++) { optionNodes.Add(new Tuple( - CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o, order.Options[i], order.OptionNames[i], (i + 1) % 10), + CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o, order.Options[i], order.GetOptionName(i), (i + 1) % 10), Keys.D0 + (i + 1) % 10)); } } @@ -2428,12 +2483,7 @@ namespace Barotrauma private GUIButton CreateOrderOptionNode(Point size, RectTransform parent, Point offset, Order order, string option, string optionName, int hotkey) { - var node = new GUIButton( - new RectTransform(size, parent: parent, anchor: Anchor.Center) - { - AbsoluteOffset = offset - }, - style: null) + var node = new GUIButton(new RectTransform(size, parent: parent, anchor: Anchor.Center), style: null) { UserData = new Tuple(order, option), OnClicked = (_, userData) => @@ -2445,6 +2495,8 @@ namespace Barotrauma return true; } }; + node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); + GUIImage icon = null; if (order.Prefab.OptionSprites.TryGetValue(option, out Sprite sprite)) { @@ -2472,40 +2524,47 @@ namespace Barotrauma new Tuple(node.UserData as Order, null) : node.UserData as Tuple; var characters = GetCharactersForManualAssignment(order.Item1); - if (characters.Count < 1) { return; } + if (characters.None()) { return; } if (!(optionNodes.Find(n => n.Item1 == node) is Tuple optionNode) || !optionNodes.Remove(optionNode)) { shortcutNodes.Remove(node); }; RemoveOptionNodes(); + if (returnNode != null) { returnNode.Children.ForEach(child => child.Visible = false); returnNode.Visible = false; historyNodes.Push(returnNode); } - SetReturnNode(centerNode, new Point( - (int)(node.RectTransform.AbsoluteOffset.X * -returnNodeDistanceModifier), - (int)(node.RectTransform.AbsoluteOffset.Y * -returnNodeDistanceModifier))); + SetReturnNode(centerNode, new Point(0, (int)(returnNodeDistanceModifier * nodeDistance))); + if (targetFrame == null || !targetFrame.Visible) { SetCenterNode(node as GUIButton); } else { - var clickedOptionNode = new GUIButton( + if (string.IsNullOrEmpty(order.Item2)) + { + SetCenterNode(node as GUIButton, resetAnchor: true); + } + else + { + var clickedOptionNode = new GUIButton( new RectTransform(centerNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null) - { - UserData = node.UserData - }; - if (order.Item1.Prefab.OptionSprites.TryGetValue(order.Item2, out Sprite sprite)) - { - CreateNodeIcon(clickedOptionNode.RectTransform, sprite, order.Item1.Color, tooltip: order.Item2); + { + UserData = node.UserData + }; + if (order.Item1.Prefab.OptionSprites.TryGetValue(order.Item2, out Sprite sprite)) + { + CreateNodeIcon(clickedOptionNode.RectTransform, sprite, order.Item1.Color, tooltip: order.Item2); + } + SetCenterNode(clickedOptionNode); + node = null; } - SetCenterNode(clickedOptionNode); - node = null; targetFrame.Visible = false; } if (shortcutCenterNode != null) @@ -2514,21 +2573,36 @@ namespace Barotrauma shortcutCenterNode = null; } - var needToExpand = characters.Count > assignmentNodeMaxCount + 1; - var nodeCount = needToExpand ? assignmentNodeMaxCount + 1 : characters.Count; - var extraNodeDistance = Math.Max(nodeCount - 6, 0) * (GUI.Scale * 30); - var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance + extraNodeDistance, - GetCircumferencePointCount(nodeCount), - GetFirstNodeAngle(nodeCount)); - - var i = 0; - var assignmentNodeCount = (needToExpand ? nodeCount - 1 : nodeCount); - for (; i < assignmentNodeCount; i++) + var characterCount = characters.Count; + int hotkey = 1; + Vector2[] offsets; + var needToExpand = characterCount > 10; + if (characterCount > 5) { - CreateAssignmentNode(order, characters[i], offsets[i].ToPoint(), (i + 1) % 10); + // First ring + var charactersOnFirstRing = needToExpand ? 5 : (int)Math.Floor(characterCount / 2f); + offsets = GetAssignmentNodeOffsets(charactersOnFirstRing); + for (int i = 0; i < charactersOnFirstRing; i++) + { + CreateAssignmentNode(order, characters[i], offsets[i].ToPoint(), hotkey++ % 10); + } + // Second ring + var charactersOnSecondRing = needToExpand ? 4 : characterCount - charactersOnFirstRing; + offsets = GetAssignmentNodeOffsets(needToExpand ? 5 : charactersOnSecondRing, false); + for (int i = 0; i < charactersOnSecondRing; i++) + { + CreateAssignmentNode(order, characters[charactersOnFirstRing + i], offsets[i].ToPoint(), hotkey++ % 10); + } + } + else + { + offsets = GetAssignmentNodeOffsets(characterCount); + for (int i = 0; i < characterCount; i++) + { + CreateAssignmentNode(order, characters[i], offsets[i].ToPoint(), hotkey++ % 10); + } } - int hotkey; if (!needToExpand) { hotkey = optionNodes.Count + 1; @@ -2539,12 +2613,14 @@ namespace Barotrauma } extraOptionCharacters.Clear(); - extraOptionCharacters.AddRange(characters.GetRange(i, characters.Count - i)); + // Sort expanded assignment nodes by characters' jobs and then by their names + extraOptionCharacters.AddRange(characters.GetRange(hotkey - 1, characterCount - (hotkey - 1)) + .OrderBy(c => c?.Info?.Job?.Name).ThenBy(c => c?.Info?.DisplayName)); expandNode = new GUIButton( - new RectTransform(nodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center) + new RectTransform(assignmentNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center) { - AbsoluteOffset = offsets[i].ToPoint() + AbsoluteOffset = offsets.Last().ToPoint() }, style: null) { @@ -2560,6 +2636,22 @@ namespace Barotrauma returnNodeHotkey = Keys.D0 + hotkey % 10; } + private Vector2[] GetAssignmentNodeOffsets(int characters, bool firstRing = true) + { + var nodeDistance = 1.8f * this.nodeDistance; + var nodePositionsOnEachSide = characters % 2 > 0 ? 7 : 6; + var nodeCountForCalculation = 2 * nodePositionsOnEachSide + 2; + var offsets = MathUtils.GetPointsOnCircumference(firstRing ? new Vector2(0f, 0.5f * nodeDistance) : Vector2.Zero, + nodeDistance, nodeCountForCalculation, MathHelper.ToRadians(180f + 360f / nodeCountForCalculation)); + var emptySpacesPerSide = (nodePositionsOnEachSide - characters) / 2; + var offsetsInUse = new Vector2[nodePositionsOnEachSide - 2 * emptySpacesPerSide]; + for (int i = 0; i < offsetsInUse.Length; i++) + { + offsetsInUse[i] = offsets[i + emptySpacesPerSide]; + } + return offsetsInUse; + } + private bool ExpandAssignmentNodes(GUIButton node, object userData) { node.OnClicked = (button, _) => @@ -2569,60 +2661,86 @@ namespace Barotrauma return true; }; - var order = userData as Tuple; - // TODO: The value 100 should be determined by how large the inner circle is - var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, (nodeDistance + GUI.Scale * 100) * 1.55f, - GetCircumferencePointCount(extraOptionCharacters.Count), - GetFirstNodeAngle(extraOptionCharacters.Count)); + var availableNodePositions = 20; + var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 2.7f * this.nodeDistance, availableNodePositions, + firstAngle: MathHelper.ToRadians(-90f - ((extraOptionCharacters.Count - 1) * 0.5f * (360f / availableNodePositions)))); for (int i = 0; i < extraOptionCharacters.Count; i++) { - CreateAssignmentNode(order, extraOptionCharacters[i], offsets[i].ToPoint(), -1); + CreateAssignmentNode(userData as Tuple, extraOptionCharacters[i], offsets[i].ToPoint(), -1, nameLabelScale: 1.15f); } return true; } - private void CreateAssignmentNode(Tuple order, Character character, Point offset, int hotkey) + private void CreateAssignmentNode(Tuple order, Character character, Point offset, int hotkey, float nameLabelScale = 1f) { // Button var node = new GUIButton( - new RectTransform(nodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), + new RectTransform(assignmentNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null) { - OnClicked = (button, userData) => + UserData = character, + OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } - SetCharacterOrder(character, order.Item1, order.Item2, Character.Controlled); + SetCharacterOrder(userData as Character, order.Item1, order.Item2, Character.Controlled); DisableCommandUI(); return true; } }; node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); - // Container - var icon = new GUIImage( - new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), - "CommandNodeContainer", - scaleToFit: true) - { - Color = character.Info.Job.Prefab.UIColor * nodeColorMultiplier, - HoverColor = character.Info.Job.Prefab.UIColor, - PressedColor = character.Info.Job.Prefab.UIColor, - SelectedColor = character.Info.Job.Prefab.UIColor, - UserData = "colorsource" - }; - // Character icon - new GUICustomComponent( - new RectTransform(Vector2.One, node.RectTransform), - (spriteBatch, _) => - { - character.Info.DrawJobIcon(spriteBatch, - new Rectangle((int)(node.Rect.X + node.Rect.Width * 0.5f), (int)(node.Rect.Y + node.Rect.Height * 0.1f), (int)(node.Rect.Width * 0.6f), (int)(node.Rect.Height * 0.8f))); - character.Info.DrawIcon(spriteBatch, new Vector2(node.Rect.X + node.Rect.Width * 0.35f, node.Center.Y), node.Rect.Size.ToVector2() * 0.7f); - }) + var jobColor = character.Info?.Job?.Prefab?.UIColor ?? Color.White; + + // Order icon + GUIImage orderIcon; + if (character.CurrentOrder != null && !character.CurrentOrder.Identifier.Equals("dismissed")) { - ToolTip = character.Info.DisplayName + " (" + character.Info.Job.Name + ")" + orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), character.CurrentOrder.SymbolSprite, scaleToFit: true); + var tooltip = character.CurrentOrder.Name; + if (!string.IsNullOrWhiteSpace(character.CurrentOrderOption)) { tooltip += " (" + character.CurrentOrder.GetOptionName(character.CurrentOrderOption) + ")"; }; + orderIcon.ToolTip = tooltip; + } + else + { + orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), "CommandIdleNode", scaleToFit: true); + } + orderIcon.Color = jobColor * nodeColorMultiplier; + orderIcon.HoverColor = jobColor; + orderIcon.PressedColor = jobColor; + orderIcon.SelectedColor = jobColor; + orderIcon.UserData = "colorsource"; + + // Name label + var width = (int)(nameLabelScale * nodeSize.X); + var font = GUI.SmallFont; + var nameLabel = new GUITextBlock( + new RectTransform(new Point(width, 0), parent: node.RectTransform, anchor: Anchor.TopCenter, pivot: Pivot.BottomCenter) + { + RelativeOffset = new Vector2(0f, -0.25f) + }, + ToolBox.LimitString(character.Info?.DisplayName, font, width), textColor: jobColor * nodeColorMultiplier, font: font, textAlignment: Alignment.Center, style: null) + { + CanBeFocused = false, + ForceUpperCase = true, + HoverTextColor = jobColor }; + if (character.Info?.Job?.Prefab?.IconSmall is Sprite smallJobIcon) + { + // Job icon + new GUIImage( + new RectTransform(new Vector2(0.4f), node.RectTransform, anchor: Anchor.TopCenter, pivot: Pivot.Center) + { + RelativeOffset = new Vector2(0.0f, -((orderIcon.RectTransform.RelativeSize.Y - 1) / 2)) + }, + smallJobIcon, scaleToFit: true) + { + CanBeFocused = false, + Color = jobColor, + HoverColor = jobColor + }; + } + #if DEBUG bool canHear = true; #else @@ -2630,7 +2748,7 @@ namespace Barotrauma #endif if (!canHear) { - node.CanBeFocused = icon.CanBeFocused = false; + node.CanBeFocused = orderIcon.CanBeFocused = false; CreateBlockIcon(node.RectTransform); } if (hotkey >= 0) @@ -2705,8 +2823,9 @@ namespace Barotrauma private void CreateBlockIcon(RectTransform parent) { - new GUIImage(new RectTransform(Vector2.One, parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true) + new GUIImage(new RectTransform(new Vector2(0.9f), parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true) { + CanBeFocused = false, Color = GUI.Style.Red * nodeColorMultiplier, HoverColor = GUI.Style.Red }; @@ -2749,6 +2868,7 @@ namespace Barotrauma private bool TryGetBreachedHullAtHoveredWall(out Hull breachedHull) { breachedHull = null; + // Based on the IsValidTarget() method of AIObjectiveFixLeaks class List leaks = Gap.GapList.FindAll(g => g != null && g.ConnectedWall != null && g.ConnectedDoor == null && g.Open > 0 && g.linkedTo.Any(l => l != null) && g.Submarine != null && (Character.Controlled != null && g.Submarine.TeamID == Character.Controlled.TeamID && g.Submarine.Info.IsPlayer)); @@ -2765,6 +2885,25 @@ namespace Barotrauma return false; } + private Submarine GetTargetSubmarine() + { + var sub = Submarine.MainSub; + if (Character.Controlled != null) + { + // Pick the second main sub when we have two teams (in combat mission) + if (Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1) + { + sub = Submarine.MainSubs[1]; + } + // Target current submarine (likely a shuttle) when undocked from the main sub + if (Character.Controlled.Submarine is Submarine currentSub && currentSub != sub && currentSub.TeamID == Character.Controlled.TeamID && !currentSub.IsConnectedTo(sub)) + { + sub = currentSub; + } + } + return sub; + } + #region Crew Member Assignment Logic private Character GetCharacterForQuickAssignment(Order order) @@ -2776,7 +2915,7 @@ namespace Barotrauma { return operatingCharacter; } - return GetCharactersSortedForOrder(order, false).FirstOrDefault(); + return GetCharactersSortedForOrder(order, false).FirstOrDefault() ?? Character.Controlled; } private List GetCharactersForManualAssignment(Order order) @@ -2784,6 +2923,11 @@ namespace Barotrauma #if !DEBUG if (Character.Controlled == null) { return new List(); } #endif + if (order.Identifier == dismissedOrderPrefab.Identifier) + { + return characters.FindAll(c => c.CurrentOrder != null && c.CurrentOrder.Identifier != dismissedOrderPrefab.Identifier) + .OrderBy(c => c.Info.DisplayName).ToList(); + } return GetCharactersSortedForOrder(order, order.Identifier != "follow").ToList(); } @@ -2822,10 +2966,7 @@ namespace Barotrauma if (canIssueOrders) { - //report buttons are hidden when accessing another character's inventory - ReportButtonFrame.Visible = !Character.Controlled.ShouldLockHud() && - (Character.Controlled?.SelectedCharacter?.Inventory == null || - !Character.Controlled.SelectedCharacter.CanInventoryBeAccessed); + ReportButtonFrame.Visible = !Character.Controlled.ShouldLockHud(); if (!ReportButtonFrame.Visible) { return; } var reportButtonParent = ChatBox ?? GameMain.Client?.ChatBox; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 25f8abb4a..f61c1174a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -25,7 +25,7 @@ namespace Barotrauma : base(preset, param) { int buttonHeight = (int)(HUDLayoutSettings.ButtonAreaTop.Height * 0.7f); - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(HUDLayoutSettings.ButtonAreaTop.Right - 200, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonHeight / 2, 200, buttonHeight), GUICanvas.Instance), + endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(HUDLayoutSettings.ButtonAreaTop.Right - GUI.IntScale(200), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonHeight / 2, GUI.IntScale(200), buttonHeight), GUICanvas.Instance), TextManager.Get("EndRound"), textAlignment: Alignment.Center) { Font = GUI.SmallFont, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 8825ec280..794a296b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -195,7 +195,7 @@ namespace Barotrauma.Tutorials // GameMain.GameSession.CrewManager.HighlightOrderButton(captain_security, "operateweapons", highlightColor, new Vector2(5, 5)); HighlightOrderOption("fireatwill"); } - while (!HasOrder(captain_security, "operateweapons", "fireatwill")); + while (!HasOrder(captain_security, "operateweapons")); RemoveCompletedObjective(segments[2]); yield return new WaitForSeconds(4f, false); TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Command)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index b11083859..c17ba6aed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Xml.Linq; using System.Linq; using Barotrauma.Items.Components; @@ -315,6 +315,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(4, GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Shoot), GameMain.Config.KeyBindText(InputType.Deselect)); // Kill hammerhead officer_hammerhead = SpawnMonster("hammerhead", officer_hammerheadSpawnPos); + ((EnemyAIController)officer_hammerhead.AIController).StayInsideLevel = false; officer_hammerhead.AIController.SelectTarget(officer.AiTarget); SetHighlight(officer_coilgunPeriscope, true); float originalDistance = Vector2.Distance(officer_coilgunPeriscope.WorldPosition, officer_hammerheadSpawnPos); @@ -329,7 +330,13 @@ namespace Barotrauma.Tutorials if (distance > originalDistance) { // Ensure that the Hammerhead targets the player + officer.AiTarget.SoundRange = float.MaxValue; + officer.AiTarget.SightRange = float.MaxValue; officer_hammerhead.AIController.SelectTarget(officer.AiTarget); + if ((officer_hammerhead.AIController as EnemyAIController)?.SelectedTargetingParams != null) + { + ((EnemyAIController)officer_hammerhead.AIController).SelectedTargetingParams.ReactDistance = 5000.0f; + } /*var ai = officer_hammerhead.AIController as EnemyAIController; ai.sight = 2.0f;*/ } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index d659ef6ea..e82aef15e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 21fa67877..8127788ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -515,7 +515,7 @@ namespace Barotrauma.Tutorials height += (int)GUI.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); } - var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: null, Color.Black * 0.5f); + var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor)); infoBlock.Flash(GUI.Style.Green); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 78b8f52fe..12cd4a55e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -32,7 +32,9 @@ namespace Barotrauma SoundPlayer.OverrideMusicDuration = 18.0f; } - GUIFrame frame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + GUIFrame background = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); + + GUIFrame frame = new GUIFrame(new RectTransform(Vector2.One, background.RectTransform, Anchor.Center), style: null) { UserData = "roundsummary" }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index b88bcafa0..c3f9fb6a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -44,6 +44,8 @@ namespace Barotrauma } } + private const int inventoryHotkeyCount = 10; + public void SetDefaultBindings(XDocument doc = null, bool legacy = false) { keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length]; @@ -100,6 +102,13 @@ namespace Barotrauma keyMapping[(int)InputType.Select] = new KeyOrMouse(MouseButton.PrimaryMouse); // shoot and deselect are handled in CheckBindings() so that we don't override the legacy settings. } + + inventoryKeyMapping = new KeyOrMouse[inventoryHotkeyCount]; + for (int i = 0; i < inventoryKeyMapping.Length; i++) + { + inventoryKeyMapping[i] = new KeyOrMouse(Keys.D0 + (i + 1) % 10); + } + if (doc != null) { LoadControls(doc); @@ -179,6 +188,26 @@ namespace Barotrauma } } + private void LoadInventoryKeybinds(XElement element) + { + for (int i = 0; i < inventoryKeyMapping.Length; i++) + { + XAttribute attribute = element.Attributes().ElementAt(i); + if (int.TryParse(attribute.Value.ToString(), out int mouseButtonInt)) + { + inventoryKeyMapping[i] = new KeyOrMouse((MouseButton)mouseButtonInt); + } + else if (Enum.TryParse(attribute.Value.ToString(), true, out MouseButton mouseButton)) + { + inventoryKeyMapping[i] = new KeyOrMouse(mouseButton); + } + else if (Enum.TryParse(attribute.Value.ToString(), true, out Keys key)) + { + inventoryKeyMapping[i] = new KeyOrMouse(key); + } + } + } + private void LoadControls(XDocument doc) { XElement keyMapping = doc.Root.Element("keymapping"); @@ -186,6 +215,12 @@ namespace Barotrauma { LoadKeyBinds(keyMapping); } + + XElement inventoryKeyMapping = doc.Root.Element("inventorykeymapping"); + if (inventoryKeyMapping != null) + { + LoadInventoryKeybinds(inventoryKeyMapping); + } } public KeyOrMouse KeyBind(InputType inputType) @@ -195,25 +230,14 @@ namespace Barotrauma public string KeyBindText(InputType inputType) { - KeyOrMouse bind = keyMapping[(int)inputType]; - - if (bind.MouseButton != MouseButton.None) - { - switch (bind.MouseButton) - { - case MouseButton.PrimaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse"); - case MouseButton.SecondaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse"); - default: - return TextManager.Get("input." + bind.MouseButton.ToString().ToLowerInvariant()); - - } - } - - return bind.ToString(); + return keyMapping[(int)inputType].Name; } - + + public KeyOrMouse InventoryKeyBind(int index) + { + return inventoryKeyMapping[index]; + } + private GUIListBox contentPackageList; private bool ChangeSliderText(GUIScrollBar scrollBar, float barScroll) @@ -259,7 +283,8 @@ namespace Barotrauma } else { - settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null, color: Color.Black * 0.5f); + settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, settingsFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); var settingsFrameContent = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.8f), settingsFrame.RectTransform, Anchor.Center)); settingsHolder = settingsFrameContent.RectTransform; } @@ -292,6 +317,25 @@ namespace Barotrauma ButtonEnabled = ContentPackage.List.Count(cp => cp.CorePackage) > 1 }; + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform), isHorizontal: true) + { + Stretch = true + }; + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnTextChanged += (textBox, text) => + { + foreach (GUIComponent child in contentPackageList.Content.Children) + { + if (!(child.UserData is ContentPackage cp)) { continue; } + child.Visible = string.IsNullOrEmpty(text) ? true : cp.Name.ToLower().Contains(text.ToLower()); + } + return true; + }; + contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.70f), leftPanel.RectTransform)) { OnSelected = (gc, obj) => false, @@ -551,6 +595,37 @@ namespace Barotrauma }; + GUITickBox textureCompressionTickBox = new GUITickBox(new RectTransform(tickBoxScale, leftColumn.RectTransform), TextManager.Get("EnableTextureCompression")) + { + ToolTip = TextManager.Get("EnableTextureCompressionToolTip"), + OnSelected = (GUITickBox box) => + { + if (box.Selected == TextureCompressionEnabled) { return true; } + bool prevTextureCompressionEnabled = TextureCompressionEnabled; + TextureCompressionEnabled = box.Selected; + + var msgBox = new GUIMessageBox( + TextManager.Get("RestartRequiredLabel"), + TextManager.Get("RestartRequiredGeneric"), + buttons: new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }); + msgBox.Buttons[0].OnClicked += (btn, userdata) => + { + ApplySettings(); + GameMain.Instance.Exit(); + return true; + }; msgBox.Buttons[1].OnClicked += (btn, userdata) => + { + TextureCompressionEnabled = prevTextureCompressionEnabled; + box.Selected = prevTextureCompressionEnabled; + msgBox.Close(); + return true; + }; + + return true; + }, + Selected = TextureCompressionEnabled + }; + GUITickBox pauseOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, leftColumn.RectTransform), TextManager.Get("PauseOnFocusLost")) { @@ -612,7 +687,8 @@ namespace Barotrauma { ChangeSliderText(scrollBar, barScroll); LightMapScale = MathHelper.Lerp(0.2f, 1.0f, barScroll); - UnsavedSettings = true; return true; + UnsavedSettings = true; + return true; }, Step = 0.25f }; @@ -1113,6 +1189,24 @@ namespace Barotrauma keyBox.SelectedColor = Color.Gold * 0.3f; } + for (int i = 0; i < inventoryHotkeyCount; i++) + { + var inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.06f), ((i + 1) <= inventoryHotkeyCount / 2 ? inputColumnLeft : inputColumnRight).RectTransform)) + { Stretch = true, IsHorizontal = true, RelativeSpacing = 0.01f, Color = new Color(12, 14, 15, 215) }; + var inputName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), inputContainer.RectTransform, Anchor.TopLeft) { MinSize = new Point(100, 0) }, + TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (i + 1).ToString()), font: GUI.SmallFont) + { ForceUpperCase = true }; + inputNameBlocks.Add(inputName); + var keyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), inputContainer.RectTransform), + text: inventoryKeyMapping[i].Name, font: GUI.SmallFont, style: "GUITextBoxNoIcon") + { + UserData = i + }; + keyBox.Text = ToolBox.LimitString(keyBox.Text, keyBox.Font, (int)(keyBox.Rect.Width - keyBox.Padding.X - keyBox.Padding.Z)); + keyBox.OnSelected += InventoryKeyBoxSelected; + keyBox.SelectedColor = Color.Gold * 0.3f; + } + GUITextBlock.AutoScaleAndNormalize(inputNameBlocks); var resetControlsArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), controlsLayoutGroup.RectTransform), style: null); @@ -1380,7 +1474,13 @@ namespace Barotrauma private void KeyBoxSelected(GUITextBox textBox, Keys key) { textBox.Text = ""; - CoroutineManager.StartCoroutine(WaitForKeyPress(textBox)); + CoroutineManager.StartCoroutine(WaitForKeyPress(textBox, keyMapping)); + } + + private void InventoryKeyBoxSelected(GUITextBox textBox, Keys key) + { + textBox.Text = ""; + CoroutineManager.StartCoroutine(WaitForKeyPress(textBox, inventoryKeyMapping)); } private void ResetControls(bool legacy) @@ -1476,7 +1576,7 @@ namespace Barotrauma return true; } - private IEnumerable WaitForKeyPress(GUITextBox keyBox) + private IEnumerable WaitForKeyPress(GUITextBox keyBox, KeyOrMouse[] keyArray) { yield return CoroutineStatus.Running; @@ -1500,42 +1600,43 @@ namespace Barotrauma if (PlayerInput.LeftButtonClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.LeftMouse); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.LeftMouse); } else if (PlayerInput.RightButtonClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.RightMouse); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.RightMouse); } else if (PlayerInput.MidButtonClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.MiddleMouse); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.MiddleMouse); } else if (PlayerInput.Mouse4ButtonClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.MouseButton4); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseButton4); } else if (PlayerInput.Mouse5ButtonClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.MouseButton5); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseButton5); } else if (PlayerInput.MouseWheelUpClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelUp); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelUp); } else if (PlayerInput.MouseWheelDownClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelDown); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelDown); } else if (PlayerInput.GetKeyboardState.GetPressedKeys().Length > 0) { Keys key = PlayerInput.GetKeyboardState.GetPressedKeys()[0]; - keyMapping[keyIndex] = new KeyOrMouse(key); + keyArray[keyIndex] = new KeyOrMouse(key); } else { yield return CoroutineStatus.Success; } - keyBox.Text = KeyBindText((InputType)keyIndex); + + keyBox.Text = keyArray[keyIndex].Name; keyBox.Text = ToolBox.LimitString(keyBox.Text, keyBox.Font, keyBox.Rect.Width); keyBox.Deselect(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 3ce961f69..50a8cb166 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -354,7 +354,6 @@ namespace Barotrauma break; case Layout.Right: { - int extraOffset = 0; int x = HUDLayoutSettings.InventoryAreaLower.Right; int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - Spacing; for (int i = 0; i < slots.Length; i++) @@ -371,17 +370,18 @@ namespace Barotrauma } int lowerX = x; + int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { if (HideSlot(i)) continue; if (PersonalSlots.HasFlag(SlotTypes[i])) { - SlotPositions[i] = new Vector2(personalSlotX, GameMain.GraphicsHeight - bottomOffset * 2 - extraOffset - Spacing * 2); + SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); personalSlotX -= slots[i].Rect.Width + Spacing; } else { - SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset - extraOffset); + SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); x += slots[i].Rect.Width + Spacing; } } @@ -391,7 +391,7 @@ namespace Barotrauma { if (!HideSlot(i)) continue; x -= slots[i].Rect.Width + Spacing; - SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset - extraOffset); + SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); } } break; @@ -399,12 +399,14 @@ namespace Barotrauma { int x = HUDLayoutSettings.InventoryAreaLower.X; int personalSlotX = x; + int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); + for (int i = 0; i < SlotPositions.Length; i++) { if (HideSlot(i)) continue; if (PersonalSlots.HasFlag(SlotTypes[i])) { - SlotPositions[i] = new Vector2(personalSlotX, GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2); + SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); personalSlotX += slots[i].Rect.Width + Spacing; } else @@ -523,8 +525,7 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { if (Items[i] != null && Items[i] != draggingItem && Character.Controlled?.Inventory == this && - GUI.KeyboardDispatcher.Subscriber == null && !CrewManager.IsCommandInterfaceOpen && - slots[i].QuickUseKey != Keys.None && PlayerInput.KeyHit(slots[i].QuickUseKey)) + GUI.KeyboardDispatcher.Subscriber == null && !CrewManager.IsCommandInterfaceOpen && PlayerInput.InventoryKeyHit(slots[i].InventoryKeyIndex)) { QuickUseItem(Items[i], true, false, true); } @@ -782,21 +783,16 @@ namespace Barotrauma } } - private void AssignQuickUseNumKeys() + public void AssignQuickUseNumKeys() { - int num = 1; + int keyBindIndex = 0; for (int i = 0; i < slots.Length; i++) { - if (HideSlot(i)) - { - slots[i].QuickUseKey = Keys.None; - continue; - } - + if (HideSlot(i)) continue; if (SlotTypes[i] == InvSlotType.Any) { - slots[i].QuickUseKey = Keys.D0 + num % 10; - num++; + slots[i].InventoryKeyIndex = keyBindIndex; + keyBindIndex++; } } } @@ -819,16 +815,21 @@ namespace Barotrauma { if (item.Container == null || character.Inventory.FindIndex(item.Container) == -1) // Not a subinventory in the character's inventory { - return item.ParentInventory is CharacterInventory ? - QuickUseAction.TakeFromCharacter : QuickUseAction.TakeFromContainer; + if (character.SelectedItems.Any(i => i?.OwnInventory != null && i.OwnInventory.CanBePut(item))) + { + return QuickUseAction.PutToEquippedItem; + } + else + { + return item.ParentInventory is CharacterInventory ? QuickUseAction.TakeFromCharacter : QuickUseAction.TakeFromContainer; + } } else { var selectedContainer = character.SelectedConstruction?.GetComponent(); if (selectedContainer != null && selectedContainer.Inventory != null && - !selectedContainer.Inventory.Locked && - allowInventorySwap) + !selectedContainer.Inventory.Locked) { // Move the item from the subinventory to the selected container return QuickUseAction.PutToContainer; @@ -928,24 +929,24 @@ namespace Barotrauma //attempt to put in a free slot first for (int i = capacity - 1; i >= 0; i--) { - if (Items[i] != null) continue; - if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) continue; + if (Items[i] != null) { continue; } + if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) { continue; } success = TryPutItem(item, i, true, false, Character.Controlled, true); - if (success) break; + if (success) { break; } } if (!success) { for (int i = capacity - 1; i >= 0; i--) { - if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) continue; - //something else already equipped in the slot, attempt to unequip it - if (Items[i] != null && Items[i].AllowedSlots.Contains(InvSlotType.Any)) + if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) { continue; } + // something else already equipped in a hand slot, attempt to unequip it so items aren't unnecessarily swapped to it + if (Items[i] != null && Items[i].AllowedSlots.Contains(InvSlotType.Any) && (SlotTypes[i] == InvSlotType.LeftHand || SlotTypes[i] == InvSlotType.RightHand)) { TryPutItem(Items[i], Character.Controlled, new List() { InvSlotType.Any }, true); } success = TryPutItem(item, i, true, false, Character.Controlled, true); - if (success) break; + if (success) { break; } } } break; @@ -1041,6 +1042,7 @@ namespace Barotrauma prevUIScale != UIScale || prevHUDScale != GUI.Scale) { + CreateSlots(); SetSlotPositions(layout); prevUIScale = UIScale; prevHUDScale = GUI.Scale; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 9e751d1df..fb818670e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -237,6 +237,7 @@ namespace Barotrauma.Items.Components { StopPicking(null); PlaySound(forcedOpen ? ActionType.OnPicked : ActionType.OnUse); + if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } } } } @@ -245,7 +246,8 @@ namespace Barotrauma.Items.Components { base.ClientRead(type, msg, sendingTime); - bool open = msg.ReadBoolean(); + bool open = msg.ReadBoolean(); + bool broken = msg.ReadBoolean(); bool forcedOpen = msg.ReadBoolean(); SetState(open, isNetworkMessage: true, sendNetworkMessage: false, forcedOpen: forcedOpen); Stuck = msg.ReadRangedSingle(0.0f, 100.0f, 8); @@ -258,6 +260,7 @@ namespace Barotrauma.Items.Components } if (isStuck) { OpenState = 0.0f; } + IsBroken = broken; PredictedState = null; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index e2e03e335..b87d69d5e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -52,5 +53,13 @@ namespace Barotrauma.Items.Components } } } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + CurrPowerConsumption = powerConsumption; + charging = true; + timer = Duration; + IsActive = true; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 4d06b050d..14767d23e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { - if (!IsActive || picker == null || !CanBeAttached() || !picker.IsKeyDown(InputType.Aim) || picker != Character.Controlled) { return; } + if (!IsActive || picker == null || !CanBeAttached(picker) || !picker.IsKeyDown(InputType.Aim) || picker != Character.Controlled) { return; } Vector2 gridPos = picker.Position; Vector2 roundedGridPos = new Vector2( @@ -71,6 +71,8 @@ namespace Barotrauma.Items.Components base.ClientRead(type, msg, sendingTime); bool shouldBeAttached = msg.ReadBoolean(); Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + UInt16 submarineID = msg.ReadUInt16(); + Submarine sub = Entity.FindEntityByID(submarineID) as Submarine; if (!attachable) { @@ -84,6 +86,7 @@ namespace Barotrauma.Items.Components { Drop(false, null); item.SetTransform(simPosition, 0.0f); + item.Submarine = sub; AttachToWall(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 09c3d6896..3491978b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Text; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 9a76f7419..d1025621a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -436,8 +436,7 @@ namespace Barotrauma.Items.Components if (subElement.Attribute("color") != null) color = subElement.GetAttributeColor("color", Color.White); string style = subElement.Attribute("style") == null ? null : subElement.GetAttributeString("style", ""); - - GuiFrame = new GUIFrame(RectTransform.Load(subElement, GUI.Canvas, Anchor.Center), style, color); + GuiFrame = new GUIFrame(RectTransform.Load(subElement, GUI.Canvas.ItemComponentHolder, Anchor.Center), style, color); DefaultLayout = GUILayoutSettings.Load(subElement); break; case "alternativelayout": diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index aa81a968c..4bc8d1250 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -28,7 +28,7 @@ namespace Barotrauma.Items.Components } private string text; - [Serialize("", true, translationTextTag: "Label.", description: "The text displayed in the label."), Editable(100)] + [Serialize("", true, translationTextTag: "Label.", description: "The text displayed in the label.", alwaysUseInstanceValues: true), Editable(100)] public string Text { get { return text; } @@ -58,7 +58,7 @@ namespace Barotrauma.Items.Components private set; } - [Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label (R,G,B,A).")] + [Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label (R,G,B,A).", alwaysUseInstanceValues: true)] public Color TextColor { get { return textColor; } @@ -69,7 +69,7 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, 10.0f), Serialize(1.0f, true, description: "The scale of the text displayed on the label.")] + [Editable(0.0f, 10.0f), Serialize(1.0f, true, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)] public float TextScale { get { return textBlock == null ? 1.0f : textBlock.TextScale; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 51a3a14fd..823cdd6ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -48,7 +48,10 @@ namespace Barotrauma.Items.Components { if (light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn) { - light.LightSprite.Draw(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), lightColor * lightBrightness, 0.0f, item.Scale, SpriteEffects.None, item.SpriteDepth - 0.0001f); + Vector2 origin = light.LightSprite.Origin; + if (light.LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = light.LightSprite.SourceRect.Width - origin.X; } + if (light.LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = light.LightSprite.SourceRect.Height - origin.Y; } + light.LightSprite.Draw(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), lightColor * lightBrightness, origin, -light.Rotation, item.Scale, light.LightSpriteEffect, item.SpriteDepth - 0.0001f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index d19b33666..01439da52 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - state = msg.ReadBoolean(); + State = msg.ReadBoolean(); ushort userID = msg.ReadUInt16(); if (userID == 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index cadfb6011..6240e6ddf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -16,6 +16,10 @@ namespace Barotrauma.Items.Components private GUIScrollBar forceSlider; private GUITickBox autoControlIndicator; + private int particlesPerSec = 60; + private float particleTimer; + + public float AnimSpeed { get; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 89165ecc0..f08be819d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -318,10 +318,10 @@ namespace Barotrauma.Items.Components { if (item.ParentInventory.slots[availableSlotIndex].HighlightTimer <= 0.0f) { - item.ParentInventory.slots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f); + item.ParentInventory.slots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); if (slotIndex < inputContainer.Capacity) { - inputContainer.Inventory.slots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f); + inputContainer.Inventory.slots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); } } } @@ -337,10 +337,22 @@ namespace Barotrauma.Items.Components slotRect.Center.ToVector2(), color: requiredItem.ItemPrefab.InventoryIconColor * 0.3f, scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y)); - + + if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) + { + GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X, slotRect.Bottom - 8, slotRect.Width, 8), Color.Black * 0.8f, true); + GUI.DrawRectangle(spriteBatch, + new Rectangle(slotRect.X, slotRect.Bottom - 8, (int)(slotRect.Width * requiredItem.MinCondition), 8), + GUI.Style.Green * 0.8f, true); + } + if (slotRect.Contains(PlayerInput.MousePosition)) { string toolTipText = requiredItem.ItemPrefab.Name; + if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) + { + toolTipText += " " + (int)Math.Round(requiredItem.MinCondition * 100) + "%"; + } if (!string.IsNullOrEmpty(requiredItem.ItemPrefab.Description)) { toolTipText += '\n' + requiredItem.ItemPrefab.Description; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index 069231f64..2632cc2cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -163,7 +163,7 @@ namespace Barotrauma.Items.Components { pumpSpeedLockTimer -= deltaTime; isActiveLockTimer -= deltaTime; - autoControlIndicator.Selected = pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f; + autoControlIndicator.Selected = IsAutoControlled; PowerButton.Enabled = isActiveLockTimer <= 0.0f; if (HasPower) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index c1ad43075..5b98c3fea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -18,6 +18,8 @@ namespace Barotrauma.Items.Components Disruption } + private PathFinder pathFinder; + private bool dynamicDockingIndicator = true; private bool unsentChanges; @@ -45,7 +47,7 @@ namespace Barotrauma.Items.Components private Sprite sonarBlip; private Sprite lineSprite; - private Dictionary targetIcons = new Dictionary(); + private readonly Dictionary targetIcons = new Dictionary(); private float displayBorderSize; @@ -65,7 +67,24 @@ namespace Barotrauma.Items.Components //Vector2 = vector from the ping source to the position of the disruption //float = strength of the disruption, between 0-1 - List> disruptedDirections = new List>(); + private readonly List> disruptedDirections = new List>(); + + class CachedDistance + { + public readonly Vector2 TransducerWorldPos; + public readonly Vector2 WorldPos; + public readonly float Distance; + public double RecalculationTime; + + public CachedDistance(Vector2 transducerWorldPos, Vector2 worldPos, float dist) + { + TransducerWorldPos = transducerWorldPos; + WorldPos = worldPos; + Distance = dist; + } + } + + private readonly Dictionary markerDistances = new Dictionary(); private readonly Color positiveColor = Color.Green; private readonly Color warningColor = Color.Orange; @@ -74,7 +93,7 @@ namespace Barotrauma.Items.Components public static readonly Vector2 controlBoxSize = new Vector2(0.33f, 0.32f); public static readonly Vector2 controlBoxOffset = new Vector2(0.025f, 0); - public static readonly float sonarAreaSize = 1.09f; + private static readonly float sonarAreaSize = 1.09f; private static readonly Dictionary blipColorGradient = new Dictionary() { @@ -94,6 +113,8 @@ namespace Barotrauma.Items.Components public float DisplayRadius { get; private set; } + public static Vector2 GUISizeCalculation => Vector2.One * Math.Min(GUI.RelativeHorizontalAspectRatio, 1f) * sonarAreaSize; + partial void InitProjSpecific(XElement element) { System.Diagnostics.Debug.Assert(Enum.GetValues(typeof(BlipType)).Cast().All(t => blipColorGradient.ContainsKey(t))); @@ -254,7 +275,7 @@ namespace Barotrauma.Items.Components controlContainer.RectTransform.SetPosition(Anchor.TopLeft); sonarView.RectTransform.ScaleBasis = ScaleBasis.Smallest; sonarView.RectTransform.SetPosition(Anchor.CenterRight); - sonarView.RectTransform.Resize(Vector2.One * GUI.RelativeHorizontalAspectRatio * sonarAreaSize); + sonarView.RectTransform.Resize(GUISizeCalculation); GUITextBlock.AutoScaleAndNormalize(passiveTickBox.TextBlock, activeTickBox.TextBlock, zoomText, directionalModeSwitchText); } } @@ -653,12 +674,16 @@ namespace Barotrauma.Items.Components DrawMarker(spriteBatch, GameMain.GameSession.StartLocation.Name, "outpost", - (Level.Loaded.StartPosition - transducerCenter), displayScale, center, DisplayRadius); + GameMain.GameSession.StartLocation.Name, + Level.Loaded.StartPosition, transducerCenter, + displayScale, center, DisplayRadius); DrawMarker(spriteBatch, GameMain.GameSession.EndLocation.Name, "outpost", - (Level.Loaded.EndPosition - transducerCenter), displayScale, center, DisplayRadius); + GameMain.GameSession.EndLocation.Name, + Level.Loaded.EndPosition, transducerCenter, + displayScale, center, DisplayRadius); foreach (AITarget aiTarget in AITarget.List) { @@ -670,7 +695,9 @@ namespace Barotrauma.Items.Components DrawMarker(spriteBatch, aiTarget.SonarLabel, aiTarget.SonarIconIdentifier, - aiTarget.WorldPosition - transducerCenter, displayScale, center, DisplayRadius * 0.975f); + aiTarget, + aiTarget.WorldPosition, transducerCenter, + displayScale, center, DisplayRadius * 0.975f); } } @@ -685,7 +712,9 @@ namespace Barotrauma.Items.Components DrawMarker(spriteBatch, mission.SonarLabel, mission.SonarIconIdentifier, - sonarPosition - transducerCenter, displayScale, center, DisplayRadius * 0.95f); + mission, + sonarPosition, transducerCenter, + displayScale, center, DisplayRadius * 0.95f); } } } @@ -704,7 +733,8 @@ namespace Barotrauma.Items.Components DrawMarker(spriteBatch, sub.Info.DisplayName, sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine", - sub.WorldPosition - transducerCenter, + sub, + sub.WorldPosition, transducerCenter, displayScale, center, DisplayRadius * 0.95f); } @@ -951,14 +981,13 @@ namespace Barotrauma.Items.Components } foreach (AITarget aiTarget in AITarget.List) { - if (aiTarget.SonarDisruption <= 0.0f || !aiTarget.Enabled) { continue; } + float disruption = aiTarget.Entity is Character c ? c.Params.SonarDisruption : aiTarget.SonarDisruption; + if (disruption <= 0.0f || !aiTarget.Enabled) { continue; } float distSqr = Vector2.DistanceSquared(aiTarget.WorldPosition, pingSource); if (distSqr > worldPingRadiusSqr) { continue; } - float disruptionDist = (float)Math.Sqrt(distSqr); disruptedDirections.Add(new Pair((aiTarget.WorldPosition - pingSource) / disruptionDist, aiTarget.SonarDisruption)); - - CreateBlipsForDisruption(aiTarget.WorldPosition, aiTarget.SonarDisruption); + CreateBlipsForDisruption(aiTarget.WorldPosition, disruption); } } @@ -1303,9 +1332,48 @@ namespace Barotrauma.Items.Components sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f, sonarBlip.Origin, 0, scale * 0.08f, SpriteEffects.None, 0); } - private void DrawMarker(SpriteBatch spriteBatch, string label, string iconIdentifier, Vector2 position, float scale, Vector2 center, float radius) + private void DrawMarker(SpriteBatch spriteBatch, string label, string iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius) { - float dist = position.Length(); + float linearDist = Vector2.Distance(worldPosition, transducerPosition); + float dist = linearDist; + if (linearDist > Range) + { + if (markerDistances.TryGetValue(targetIdentifier, out CachedDistance cachedDistance)) + { + if (Timing.TotalTime > cachedDistance.RecalculationTime && + (Vector2.DistanceSquared(cachedDistance.TransducerWorldPos, transducerPosition) > 500 * 500 || + Vector2.DistanceSquared(cachedDistance.WorldPos, worldPosition) > 500 * 500)) + { + markerDistances.Remove(targetIdentifier); + CalculateDistance(); + } + else + { + dist = Math.Max(cachedDistance.Distance, linearDist); + } + } + else + { + CalculateDistance(); + } + } + + void CalculateDistance() + { + pathFinder ??= new PathFinder(WayPoint.WayPointList, indoorsSteering: false); + var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition)); + if (!path.Unreachable) + { + var cachedDistance = new CachedDistance(transducerPosition, worldPosition, path.TotalLength) + { + RecalculationTime = Timing.TotalTime + Rand.Range(1.0f, 5.0f) + }; + markerDistances.Add(targetIdentifier, cachedDistance); + dist = path.TotalLength; + } + } + + Vector2 position = worldPosition - transducerPosition; position *= zoom; position *= scale; @@ -1314,16 +1382,16 @@ namespace Barotrauma.Items.Components float textAlpha = MathHelper.Clamp(1.5f - dist / 50000.0f, 0.5f, 1.0f); Vector2 dir = Vector2.Normalize(position); - Vector2 markerPos = (dist * zoom * scale > radius) ? dir * radius : position; + Vector2 markerPos = (linearDist * zoom * scale > radius) ? dir * radius : position; markerPos += center; markerPos.X = (int)markerPos.X; markerPos.Y = (int)markerPos.Y; float alpha = 1.0f; - if (dist * scale < radius) + if (linearDist * scale < radius) { - float normalizedDist = dist * scale / radius; + float normalizedDist = linearDist * scale / radius; alpha = Math.Max(normalizedDist - 0.4f, 0.0f); float mouseDist = Vector2.Distance(PlayerInput.MousePosition, markerPos); @@ -1334,14 +1402,6 @@ namespace Barotrauma.Items.Components } } - if (!GuiFrame.Children.First().Rect.Contains(markerPos)) - { - if (MathUtils.GetLineRectangleIntersection(center, markerPos, GuiFrame.Children.First().Rect, out Vector2 intersection)) - { - markerPos = intersection; - } - } - if (string.IsNullOrEmpty(iconIdentifier) || !targetIcons.ContainsKey(iconIdentifier)) { GUI.DrawRectangle(spriteBatch, new Rectangle((int)markerPos.X - 3, (int)markerPos.Y - 3, 6, 6), markerColor, thickness: 2); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index e73a9ff5d..f51566b02 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -390,7 +390,7 @@ namespace Barotrauma.Items.Components }; // Sonar area - steerArea = new GUICustomComponent(new RectTransform(Vector2.One * GUI.RelativeHorizontalAspectRatio * Sonar.sonarAreaSize, GuiFrame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest), + steerArea = new GUICustomComponent(new RectTransform(Sonar.GUISizeCalculation, GuiFrame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest), (spriteBatch, guiCustomComponent) => { DrawHUD(spriteBatch, guiCustomComponent.Rect); }, null); steerRadius = steerArea.Rect.Width / 2; @@ -438,11 +438,8 @@ namespace Barotrauma.Items.Components if (Voltage < MinVoltage) { return; } Rectangle velRect = new Rectangle(x + 20, y + 20, width - 40, height - 40); - Vector2 displaySubPos = (-sonar.DisplayOffset * sonar.Zoom) / sonar.Range * sonar.DisplayRadius * sonar.Zoom; - displaySubPos.Y = -displaySubPos.Y; - displaySubPos = displaySubPos.ClampLength(velRect.Width / 2); - displaySubPos = steerArea.Rect.Center.ToVector2() + displaySubPos; - + Vector2 steeringOrigin = steerArea.Rect.Center.ToVector2(); + if (!AutoPilot) { Vector2 unitSteeringInput = steeringInput / 100.0f; @@ -450,18 +447,18 @@ namespace Barotrauma.Items.Components Vector2 steeringInputPos = new Vector2( steeringInput.X * (float)Math.Sqrt(1.0f - 0.5f * unitSteeringInput.Y * unitSteeringInput.Y), -steeringInput.Y * (float)Math.Sqrt(1.0f - 0.5f * unitSteeringInput.X * unitSteeringInput.X)); - steeringInputPos += displaySubPos; + steeringInputPos += steeringOrigin; if (steeringIndicator != null) { - Vector2 dir = steeringInputPos - displaySubPos; + Vector2 dir = steeringInputPos - steeringOrigin; float angle = (float)Math.Atan2(dir.Y, dir.X); - steeringIndicator.Draw(spriteBatch, displaySubPos, Color.White, origin: steeringIndicator.Origin, rotate: angle, + steeringIndicator.Draw(spriteBatch, steeringOrigin, Color.White, origin: steeringIndicator.Origin, rotate: angle, scale: new Vector2(dir.Length() / steeringIndicator.size.X, 1.0f)); } else { - GUI.DrawLine(spriteBatch, displaySubPos, steeringInputPos, Color.LightGray); + GUI.DrawLine(spriteBatch, steeringOrigin, steeringInputPos, Color.LightGray); GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringInputPos.X - 5, (int)steeringInputPos.Y - 5, 10, 10), Color.White); } @@ -475,7 +472,7 @@ namespace Barotrauma.Items.Components Sonar sonar = item.GetComponent(); if (sonar != null && controlledSub != null) { - Vector2 displayPosToMaintain = ((posToMaintain.Value - sonar.DisplayOffset * sonar.Zoom - controlledSub.WorldPosition)) / sonar.Range * sonar.DisplayRadius * sonar.Zoom; + Vector2 displayPosToMaintain = ((posToMaintain.Value - controlledSub.WorldPosition)) / sonar.Range * sonar.DisplayRadius * sonar.Zoom; displayPosToMaintain.Y = -displayPosToMaintain.Y; displayPosToMaintain = displayPosToMaintain.ClampLength(velRect.Width / 2); displayPosToMaintain = steerArea.Rect.Center.ToVector2() + displayPosToMaintain; @@ -494,11 +491,11 @@ namespace Barotrauma.Items.Components if (maintainPosOriginIndicator != null) { - maintainPosOriginIndicator.Draw(spriteBatch, displaySubPos, GUI.Style.Orange, scale: 0.5f * sonar.Zoom); + maintainPosOriginIndicator.Draw(spriteBatch, steeringOrigin, GUI.Style.Orange, scale: 0.5f * sonar.Zoom); } else { - GUI.DrawRectangle(spriteBatch, new Rectangle((int)displaySubPos.X - 5, (int)displaySubPos.Y - 5, 10, 10), GUI.Style.Orange); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringOrigin.X - 5, (int)steeringOrigin.Y - 5, 10, 10), GUI.Style.Orange); } } } @@ -508,20 +505,19 @@ namespace Barotrauma.Items.Components Vector2 steeringPos = new Vector2( targetVelocity.X * 0.9f * (float)Math.Sqrt(1.0f - 0.5f * unitTargetVel.Y * unitTargetVel.Y), -targetVelocity.Y * 0.9f * (float)Math.Sqrt(1.0f - 0.5f * unitTargetVel.X * unitTargetVel.X)); - steeringPos += displaySubPos; - + steeringPos += steeringOrigin; if (steeringIndicator != null) { - Vector2 dir = steeringPos - displaySubPos; + Vector2 dir = steeringPos - steeringOrigin; float angle = (float)Math.Atan2(dir.Y, dir.X); - steeringIndicator.Draw(spriteBatch, displaySubPos, Color.Gray, origin: steeringIndicator.Origin, rotate: angle, + steeringIndicator.Draw(spriteBatch, steeringOrigin, Color.Gray, origin: steeringIndicator.Origin, rotate: angle, scale: new Vector2(dir.Length() / steeringIndicator.size.X, 0.7f)); } else { GUI.DrawLine(spriteBatch, - displaySubPos, + steeringOrigin, steeringPos, Color.CadetBlue, 0, 2); } @@ -669,11 +665,7 @@ namespace Barotrauma.Items.Components { if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen && !GameSession.IsTabMenuOpen) { - Vector2 displaySubPos = (-sonar.DisplayOffset * sonar.Zoom) / sonar.Range * sonar.DisplayRadius * sonar.Zoom; - displaySubPos.Y = -displaySubPos.Y; - displaySubPos = steerArea.Rect.Center.ToVector2() + displaySubPos; - - Vector2 inputPos = PlayerInput.MousePosition - displaySubPos; + Vector2 inputPos = PlayerInput.MousePosition - steerArea.Rect.Center.ToVector2(); inputPos.Y = -inputPos.Y; if (AutoPilot && !LevelStartSelected && !LevelEndSelected) { @@ -848,7 +840,6 @@ namespace Barotrauma.Items.Components Vector2 newSteeringInput = steeringInput; Vector2 newTargetVelocity = targetVelocity; float newSteeringAdjustSpeed = steeringAdjustSpeed; - bool maintainPos = false; Vector2? newPosToMaintain = null; bool headingToStart = false; @@ -859,8 +850,7 @@ namespace Barotrauma.Items.Components if (autoPilot) { - maintainPos = msg.ReadBoolean(); - if (maintainPos) + if (msg.ReadBoolean()) { newPosToMaintain = new Vector2( msg.ReadSingle(), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index ec7a53bd1..e7eb0f15d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -27,6 +27,8 @@ namespace Barotrauma.Items.Components private FixActions requestStartFixAction; + public float FakeBrokenTimer; + [Serialize("", false, description: "An optional description of the needed repairs displayed in the repair interface.")] public string Description { @@ -43,7 +45,7 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { if (!HasRequiredItems(character, false) || character.SelectedConstruction != item) return false; - return !item.IsFullCondition || character.IsTraitor && item.ConditionPercentage > MinSabotageCondition || (CurrentFixer == character && (!item.IsFullCondition || (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition))); + return item.ConditionPercentage < RepairThreshold || character.IsTraitor && item.ConditionPercentage > MinSabotageCondition || (CurrentFixer == character && (!item.IsFullCondition || (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition))); } partial void InitProjSpecific(XElement element) @@ -136,6 +138,17 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime) { + if (Character.Controlled == null || (Character.Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) + { + FakeBrokenTimer = 0.0f; + } + else + { + FakeBrokenTimer -= deltaTime; + } + + item.FakeBroken = FakeBrokenTimer > 0.0f; + if (!GameMain.IsMultiplayer) { switch (requestStartFixAction) @@ -150,10 +163,10 @@ namespace Barotrauma.Items.Components break; } } - + for (int i = 0; i < particleEmitters.Count; i++) { - if (item.ConditionPercentage >= particleEmitterConditionRanges[i].X && item.ConditionPercentage <= particleEmitterConditionRanges[i].Y) + if ((item.ConditionPercentage >= particleEmitterConditionRanges[i].X && item.ConditionPercentage <= particleEmitterConditionRanges[i].Y) || FakeBrokenTimer > 0.0f) { particleEmitters[i].Emit(deltaTime, item.WorldPosition, item.CurrentHull); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 01cbec3df..4184e554e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -379,7 +379,7 @@ namespace Barotrauma.Items.Components ConnectionPanel.HighlightedWire = wire; bool allowRewiring = GameMain.NetworkMember?.ServerSettings == null || GameMain.NetworkMember.ServerSettings.AllowRewiring; - if (allowRewiring && !wire.Locked && (!panel.Locked || Screen.Selected == GameMain.SubEditorScreen)) + if (allowRewiring && (!wire.Locked && !panel.Locked || Screen.Selected == GameMain.SubEditorScreen)) { //start dragging the wire if (PlayerInput.PrimaryMouseButtonHeld()) { DraggingConnected = wire; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 8f3cd4da6..fc67bfeac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -285,6 +285,8 @@ namespace Barotrauma.Items.Components public static void UpdateEditing(List wires) { + var doubleClicked = PlayerInput.DoubleClicked(); + Wire equippedWire = Character.Controlled?.SelectedItems[0]?.GetComponent() ?? Character.Controlled?.SelectedItems[1]?.GetComponent(); @@ -298,7 +300,7 @@ namespace Barotrauma.Items.Components } //dragging a node of some wire - if (draggingWire != null) + if (draggingWire != null && !doubleClicked) { if (Character.Controlled != null) { @@ -329,15 +331,18 @@ namespace Barotrauma.Items.Components if (selectedNodeIndex.HasValue) { - nodeWorldPos.X = MathUtils.Round(nodeWorldPos.X, Submarine.GridSize.X / 2.0f); - nodeWorldPos.Y = MathUtils.Round(nodeWorldPos.Y, Submarine.GridSize.Y / 2.0f); + if (!PlayerInput.IsShiftDown()) + { + nodeWorldPos.X = MathUtils.Round(nodeWorldPos.X, Submarine.GridSize.X / 2.0f); + nodeWorldPos.Y = MathUtils.Round(nodeWorldPos.Y, Submarine.GridSize.Y / 2.0f); + } draggingWire.nodes[(int)selectedNodeIndex] = nodeWorldPos; draggingWire.UpdateSections(); } else { - if (Vector2.DistanceSquared(nodeWorldPos, draggingWire.nodes[(int)highlightedNodeIndex]) > Submarine.GridSize.X * Submarine.GridSize.X) + if (Vector2.DistanceSquared(nodeWorldPos, draggingWire.nodes[(int)highlightedNodeIndex]) > Submarine.GridSize.X * Submarine.GridSize.X || PlayerInput.IsShiftDown()) { selectedNodeIndex = highlightedNodeIndex; } @@ -350,6 +355,8 @@ namespace Barotrauma.Items.Components return; } + bool updateHighlight = true; + //a wire has been selected -> check if we should start dragging one of the nodes float nodeSelectDist = 10, sectionSelectDist = 5; highlightedNodeIndex = null; @@ -405,6 +412,37 @@ namespace Barotrauma.Items.Components { selectedWire.nodes.RemoveAt(closestIndex); selectedWire.UpdateSections(); + } + // if only one end of the wire is disconnect pick it back up with double click + else if (doubleClicked && equippedWire == null && Character.Controlled != null && selectedWire.connections.Any(conn => conn != null)) + { + if (selectedWire.connections[0] == null && closestIndex == 0 || selectedWire.connections[1] == null && closestIndex == selectedWire.nodes.Count - 1) + { + selectedWire.IsActive = true; + selectedWire.nodes.RemoveAt(closestIndex); + selectedWire.UpdateSections(); + + // flip the wire + if (closestIndex == 0) + { + selectedWire.nodes.Reverse(); + selectedWire.connections[0] = selectedWire.connections[1]; + selectedWire.connections[1] = null; + } + + selectedWire.shouldClearConnections = false; + Character.Controlled.Inventory.TryPutItem(selectedWire.item, Character.Controlled, new List { InvSlotType.LeftHand, InvSlotType.RightHand }); + foreach (var entity in MapEntity.mapEntityList) + { + if (entity is Item item) + { + item.GetComponent()?.DisconnectedWires.Remove(selectedWire); + } + } + MapEntity.SelectedList.Clear(); + selectedWire.shouldClearConnections = true; + updateHighlight = false; + } } } } @@ -446,7 +484,7 @@ namespace Barotrauma.Items.Components } } - if (highlighted != null) + if (highlighted != null && updateHighlight) { highlighted.item.IsHighlighted = true; if (PlayerInput.PrimaryMouseButtonClicked()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index c4f22334c..bbf51b8e4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -5,7 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 8b1b6ba49..11b3c45b6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -31,7 +31,7 @@ namespace Barotrauma public Sprite SlotSprite; - public Keys QuickUseKey; + public int InventoryKeyIndex = -1; public int SubInventoryDir = -1; @@ -709,12 +709,15 @@ namespace Barotrauma /// Is the mouse on any inventory element (slot, equip button, subinventory...) /// /// - public static bool IsMouseOnInventory() + public static bool IsMouseOnInventory(bool ignoreDraggedItem = false) { var isSubEditor = Screen.Selected is SubEditorScreen editor && !editor.WiringMode; - if (Character.Controlled == null) return false; + if (Character.Controlled == null) { return false; } - if (draggingItem != null || DraggingInventory != null) return true; + if (!ignoreDraggedItem) + { + if (draggingItem != null || DraggingInventory != null) { return true; } + } if (Character.Controlled.Inventory != null && !isSubEditor) { @@ -966,7 +969,8 @@ namespace Barotrauma { Character.Controlled.ClearInputs(); - if (CharacterHealth.OpenHealthWindow != null && + if (!IsMouseOnInventory(ignoreDraggedItem: true) && + CharacterHealth.OpenHealthWindow != null && CharacterHealth.OpenHealthWindow.OnItemDropped(draggingItem, false)) { draggingItem = null; @@ -1078,7 +1082,7 @@ namespace Barotrauma protected static Rectangle GetSubInventoryHoverArea(SlotReference subSlot) { Rectangle hoverArea; - if (!subSlot.Inventory.Movable()) + if (!subSlot.Inventory.Movable() || Character.Controlled?.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item)) { hoverArea = subSlot.Slot.Rect; hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); @@ -1251,7 +1255,7 @@ namespace Barotrauma if (item != null && drawItem) { - if (!item.IsFullCondition && (itemContainer == null || !itemContainer.ShowConditionInContainedStateIndicator)) + if (!item.IsFullCondition && !item.Prefab.HideConditionBar && (itemContainer == null || !itemContainer.ShowConditionInContainedStateIndicator)) { GUI.DrawRectangle(spriteBatch, new Rectangle(rect.X, rect.Bottom - 8, rect.Width, 8), Color.Black * 0.8f, true); GUI.DrawRectangle(spriteBatch, @@ -1378,10 +1382,10 @@ namespace Barotrauma if (inventory != null && !inventory.Locked && Character.Controlled?.Inventory == inventory && - slot.QuickUseKey != Keys.None) + slot.InventoryKeyIndex != -1) { spriteBatch.Draw(slotHotkeySprite.Texture, rect.ScaleSize(1.15f), slotHotkeySprite.SourceRect, slotColor); - GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), slot.QuickUseKey.ToString().Substring(1, 1), Color.Black, font: GUI.HotkeyFont); + GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), GameMain.Config.InventoryKeyBind(slot.InventoryKeyIndex).Name, Color.Black, font: GUI.HotkeyFont); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 7589211bc..4702c1767 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -32,6 +32,20 @@ namespace Barotrauma private readonly Dictionary spriteAnimState = new Dictionary(); + private bool fakeBroken; + public bool FakeBroken + { + get { return fakeBroken; } + set + { + if (value != fakeBroken) + { + fakeBroken = value; + SetActiveSprite(); + } + } + } + private Sprite activeSprite; public override Sprite Sprite { @@ -71,6 +85,10 @@ namespace Barotrauma { get { + if (!GameMain.SubEditorScreen.ShowThalamus && prefab.Category.HasFlag(MapEntityCategory.Thalamus)) + { + return false; + } return parentInventory == null && (body == null || body.Enabled) && ShowItems; } } @@ -139,11 +157,12 @@ namespace Barotrauma } } + float displayCondition = FakeBroken ? 0.0f : condition; for (int i = 0; i < Prefab.BrokenSprites.Count;i++) { + if (Prefab.BrokenSprites[i].FadeIn) { continue; } float minCondition = i > 0 ? Prefab.BrokenSprites[i - i].MaxCondition : 0.0f; - if (condition <= minCondition || - condition <= Prefab.BrokenSprites[i].MaxCondition && !Prefab.BrokenSprites[i].FadeIn) + if (displayCondition <= minCondition || displayCondition <= Prefab.BrokenSprites[i].MaxCondition) { activeSprite = Prefab.BrokenSprites[i].Sprite; break; @@ -225,7 +244,9 @@ namespace Barotrauma BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; - if (condition < Prefab.Health) + float displayCondition = FakeBroken ? 0.0f : condition; + Vector2 drawOffset = Vector2.Zero; + if (displayCondition < Prefab.Health) { for (int i = 0; i < Prefab.BrokenSprites.Count; i++) { @@ -233,16 +254,17 @@ namespace Barotrauma { float min = i > 0 ? Prefab.BrokenSprites[i - i].MaxCondition : 0.0f; float max = Prefab.BrokenSprites[i].MaxCondition; - fadeInBrokenSpriteAlpha = 1.0f - ((condition - min) / (max - min)); - if (fadeInBrokenSpriteAlpha > 0.0f && fadeInBrokenSpriteAlpha < 1.0f) + fadeInBrokenSpriteAlpha = 1.0f - ((displayCondition - min) / (max - min)); + if (fadeInBrokenSpriteAlpha > 0.0f && fadeInBrokenSpriteAlpha <= 1.0f) { fadeInBrokenSprite = Prefab.BrokenSprites[i]; } continue; } - if (condition <= Prefab.BrokenSprites[i].MaxCondition) + if (displayCondition <= Prefab.BrokenSprites[i].MaxCondition) { activeSprite = Prefab.BrokenSprites[i].Sprite; + drawOffset = Prefab.BrokenSprites[i].Offset.ToVector2() * Scale; break; } } @@ -264,9 +286,13 @@ namespace Barotrauma { if (prefab.ResizeHorizontal || prefab.ResizeVertical) { - activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)), new Vector2(rect.Width, rect.Height), color: color, + Vector2 size = new Vector2(rect.Width, rect.Height); + activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset, + size, color: color, + textureScale: Vector2.One * Scale, depth: depth); - fadeInBrokenSprite?.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)), new Vector2(rect.Width, rect.Height), color: color * fadeInBrokenSpriteAlpha, + fadeInBrokenSprite?.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha, + textureScale: Vector2.One * Scale, depth: depth - 0.000001f); foreach (var decorativeSprite in Prefab.DecorativeSprites) { @@ -280,8 +306,8 @@ namespace Barotrauma } else { - activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y), color, SpriteRotation, Scale, activeSprite.effects, depth); - fadeInBrokenSprite?.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y), color * fadeInBrokenSpriteAlpha, SpriteRotation, Scale, activeSprite.effects, depth - 0.000001f); + activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, SpriteRotation, Scale, activeSprite.effects, depth); + fadeInBrokenSprite?.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, SpriteRotation, Scale, activeSprite.effects, depth - 0.000001f); foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } @@ -302,19 +328,25 @@ namespace Barotrauma if (holdable.Picker.SelectedItems[0] == this) { Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.RightHand); - depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() + depthStep * 2; - foreach (WearableSprite wearableSprite in holdLimb.WearingItems) + if (holdLimb != null) { - if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) { depth = Math.Max(wearableSprite.Sprite.Depth + depthStep, depth); } + depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() + depthStep * 2; + foreach (WearableSprite wearableSprite in holdLimb.WearingItems) + { + if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) { depth = Math.Max(wearableSprite.Sprite.Depth + depthStep, depth); } + } } } else if (holdable.Picker.SelectedItems[1] == this) { Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.LeftHand); - depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() - depthStep * 2; - foreach (WearableSprite wearableSprite in holdLimb.WearingItems) + if (holdLimb != null) { - if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) { depth = Math.Min(wearableSprite.Sprite.Depth - depthStep, depth); } + depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() - depthStep * 2; + foreach (WearableSprite wearableSprite in holdLimb.WearingItems) + { + if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) { depth = Math.Min(wearableSprite.Sprite.Depth - depthStep, depth); } + } } } } @@ -1263,6 +1295,14 @@ namespace Barotrauma inventory = container.Inventory; } } + else if (inventoryOwner == null) + { + DebugConsole.ThrowError($"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of an entity with the ID {inventoryId} (entity not found)"); + } + else + { + DebugConsole.ThrowError($"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{inventoryOwner} ({inventoryOwner.ID})\" (invalid entity, should be an item or a character)"); + } } var item = new Item(itemPrefab, pos, sub) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 1b11c5ead..ec6c72cff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -14,12 +14,14 @@ namespace Barotrauma public readonly float MaxCondition; public readonly Sprite Sprite; public readonly bool FadeIn; + public readonly Point Offset; - public BrokenItemSprite(Sprite sprite, float maxCondition, bool fadeIn) + public BrokenItemSprite(Sprite sprite, float maxCondition, bool fadeIn, Point offset) { Sprite = sprite; MaxCondition = MathHelper.Clamp(maxCondition, 0.0f, 100.0f); FadeIn = fadeIn; + Offset = offset; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs index 03751e45e..c007fc16f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs @@ -60,10 +60,10 @@ namespace Barotrauma var particle = GameMain.ParticleManager.CreateParticle("flame", particlePos, particleVel, 0.0f, hull); - if (particle == null) continue; + if (particle == null) { continue; } //make some of the particles create another firesource when they enter another hull - if (Rand.Int(20) == 1) particle.OnChangeHull = onChangeHull; + if (Rand.Int(20) == 1) { particle.OnChangeHull = onChangeHull; } particle.Size *= MathHelper.Clamp(size.X / 60.0f * Math.Max(hull.Oxygen / hull.Volume, 0.4f), 0.5f, 1.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index ac8c01edf..c9101a9fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Linq; namespace Barotrauma { @@ -24,7 +25,7 @@ namespace Barotrauma public override void Draw(SpriteBatch sb, bool editing, bool back = true) { - if (!GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) + if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) { Vector2 center = new Vector2(WorldRect.X + rect.Width / 2.0f, -(WorldRect.Y - rect.Height / 2.0f)); GUI.DrawLine(sb, center, center + new Vector2(flowForce.X, -flowForce.Y) / 10.0f, GUI.Style.Red); @@ -121,24 +122,36 @@ namespace Barotrauma partial void EmitParticles(float deltaTime) { - if (flowTargetHull == null) return; - + if (flowTargetHull == null) { return; } + + if (linkedTo.Count == 2 && linkedTo[0] is Hull hull1 && linkedTo[1] is Hull hull2) + { + //no flow particles between linked hulls (= rooms consisting of multiple hulls) + if (hull1.linkedTo.Contains(hull2)) { return; } + if (hull1.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { return; } + if (hull2.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { return; } + } + Vector2 pos = Position; if (IsHorizontal) { pos.X += Math.Sign(flowForce.X); - pos.Y = MathHelper.Clamp((higherSurface + lowerSurface) / 2.0f, rect.Y - rect.Height, rect.Y) + 10; + pos.Y = MathHelper.Clamp(Rand.Range(higherSurface, lowerSurface), rect.Y - rect.Height, rect.Y); } else { pos.Y += Math.Sign(flowForce.Y) * rect.Height / 2.0f; } + //spawn less particles when there's already a large number of them + float particleAmountMultiplier = 1.0f - GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles; + particleAmountMultiplier *= particleAmountMultiplier; + //light dripping if (open < 0.2f && LerpedFlowForce.LengthSquared() > 100.0f) { particleTimer += deltaTime; - float particlesPerSec = open * 100.0f; + float particlesPerSec = open * 100.0f * particleAmountMultiplier; float emitInterval = 1.0f / particlesPerSec; while (particleTimer > emitInterval) { @@ -174,12 +187,13 @@ namespace Barotrauma particleTimer += deltaTime; if (IsHorizontal) { - float particlesPerSec = open * rect.Height * 0.1f; + float particlesPerSec = open * rect.Height * 0.1f * particleAmountMultiplier; + if (openedTimer > 0.0f) { particlesPerSec *= 1.0f + openedTimer * 10.0f; } float emitInterval = 1.0f / particlesPerSec; while (particleTimer > emitInterval) { Vector2 velocity = new Vector2( - MathHelper.Clamp(flowForce.X, -5000.0f, 5000.0f) * Rand.Range(0.5f, 0.7f), + MathHelper.Clamp(flowForce.X, -5000.0f, 5000.0f) * Rand.Range(0.5f, 0.7f), flowForce.Y * Rand.Range(0.5f, 0.7f)); if (flowTargetHull.WaterVolume < flowTargetHull.Volume * 0.95f) @@ -191,11 +205,11 @@ namespace Barotrauma if (particle != null) { - particle.Size = particle.Size * Math.Min(Math.Abs(flowForce.X / 1000.0f), 5.0f); + particle.Size *= Math.Min(Math.Abs(flowForce.X / 500.0f), 5.0f); } } - if (Math.Abs(flowForce.X) > 300.0f) + if (Math.Abs(flowForce.X) > 300.0f && flowTargetHull.WaterVolume > flowTargetHull.Volume * 0.1f) { pos.X += Math.Sign(flowForce.X) * 10.0f; if (rect.Height < 32) @@ -211,7 +225,7 @@ namespace Barotrauma GameMain.ParticleManager.CreateParticle( "bubbles", Submarine == null ? pos : pos + Submarine.Position, - flowForce / 10.0f, 0, flowTargetHull); + velocity, 0, flowTargetHull); } particleTimer -= emitInterval; } @@ -220,7 +234,7 @@ namespace Barotrauma { if (Math.Sign(flowTargetHull.Rect.Y - rect.Y) != Math.Sign(lerpedFlowForce.Y)) return; - float particlesPerSec = open * rect.Width * 0.3f; + float particlesPerSec = open * rect.Width * 0.3f * particleAmountMultiplier; float emitInterval = 1.0f / particlesPerSec; while (particleTimer > emitInterval) { @@ -237,7 +251,7 @@ namespace Barotrauma velocity, 0, FlowTargetHull); if (splash != null) splash.Size = splash.Size * MathHelper.Clamp(rect.Width / 50.0f, 0.8f, 4.0f); } - if (Math.Abs(flowForce.Y) > 190.0f && Rand.Range(0.0f, 1.0f) < 0.3f) + if (Math.Abs(flowForce.Y) > 190.0f && Rand.Range(0.0f, 1.0f) < 0.3f && flowTargetHull.WaterVolume > flowTargetHull.Volume * 0.1f) { GameMain.ParticleManager.CreateParticle( "bubbles", diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 038062bc7..69debe79f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -14,7 +14,7 @@ namespace Barotrauma { public const int MaxDecalsPerHull = 10; - private List decals = new List(); + private readonly List decals = new List(); private float serverUpdateDelay; private float remoteWaterVolume, remoteOxygenPercentage; @@ -23,6 +23,8 @@ namespace Barotrauma private bool networkUpdatePending; private float networkUpdateTimer; + private double lastAmbientLightEditTime; + public override bool SelectableInEditor { get @@ -232,33 +234,36 @@ namespace Barotrauma return; } - /*if (!Visible) + if (!ShowHulls && !GameMain.DebugDraw) { return; } + + if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) { return; } + + float alpha = 1.0f; + float hideTimeAfterEdit = 3.0f; + if (lastAmbientLightEditTime > Timing.TotalTime - hideTimeAfterEdit * 2.0f) { - drawRect = - Submarine == null ? rect : new Rectangle((int)(Submarine.DrawPosition.X + rect.X), (int)(Submarine.DrawPosition.Y + rect.Y), rect.Width, rect.Height); - - GUI.DrawRectangle(spriteBatch, - new Vector2(drawRect.X, -drawRect.Y), - new Vector2(rect.Width, rect.Height), - Color.Black, true, - 0, (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f)); - }*/ - - if (!ShowHulls && !GameMain.DebugDraw) return; - - if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) return; + alpha = Math.Min((float)(Timing.TotalTime - lastAmbientLightEditTime) / hideTimeAfterEdit - 1.0f, 1.0f); + } Rectangle drawRect = Submarine == null ? rect : new Rectangle((int)(Submarine.DrawPosition.X + rect.X), (int)(Submarine.DrawPosition.Y + rect.Y), rect.Width, rect.Height); + if ((IsSelected || IsHighlighted) && editing) + { + GUI.DrawRectangle(spriteBatch, + new Vector2(drawRect.X, -drawRect.Y), + new Vector2(rect.Width, rect.Height), + (IsHighlighted ? Color.LightBlue * 0.8f : GUI.Style.Red * 0.5f) * alpha, false, 0, (int)Math.Max(5.0f / Screen.Selected.Cam.Zoom, 1.0f)); + } + GUI.DrawRectangle(spriteBatch, new Vector2(drawRect.X, -drawRect.Y), new Vector2(rect.Width, rect.Height), - Color.Blue, false, (ID % 255) * 0.000001f, (int)Math.Max((1.5f / Screen.Selected.Cam.Zoom), 1.0f)); + Color.Blue * alpha, false, (ID % 255) * 0.000001f, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height), - GUI.Style.Red * ((100.0f - OxygenPercentage) / 400.0f), true, 0, (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f)); + GUI.Style.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); if (GameMain.DebugDraw) { @@ -277,10 +282,12 @@ namespace Barotrauma foreach (FireSource fs in FireSources) { Rectangle fireSourceRect = new Rectangle((int)fs.WorldPosition.X, -(int)fs.WorldPosition.Y, (int)fs.Size.X, (int)fs.Size.Y); - GUI.DrawRectangle(spriteBatch, fireSourceRect, GUI.Style.Orange, false, 0, 5); + GUI.DrawRectangle(spriteBatch, fireSourceRect, GUI.Style.Red, false, 0, 5); + GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUI.Style.Orange, false, 0, 5); //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); for (int i = 0; i < waveY.Length - 1; i++) { @@ -290,24 +297,15 @@ namespace Barotrauma }*/ } - if ((IsSelected || IsHighlighted) && editing) - { - GUI.DrawRectangle(spriteBatch, - new Vector2(drawRect.X + 5, -drawRect.Y + 5), - new Vector2(rect.Width - 10, rect.Height - 10), - IsHighlighted ? Color.LightBlue * 0.5f : GUI.Style.Red * 0.5f, true, 0, (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f)); - } - foreach (MapEntity e in linkedTo) { - if (e is Hull) + if (e is Hull linkedHull) { - Hull linkedHull = (Hull)e; - Rectangle connectedHullRect = e.Submarine == null ? - linkedHull.rect : + Rectangle connectedHullRect = e.Submarine == null ? + linkedHull.rect : new Rectangle( (int)(Submarine.DrawPosition.X + linkedHull.WorldPosition.X), - (int)(Submarine.DrawPosition.Y + linkedHull.WorldPosition.Y), + (int)(Submarine.DrawPosition.Y + linkedHull.WorldPosition.Y), linkedHull.WorldRect.Width, linkedHull.WorldRect.Height); //center of the hull @@ -315,7 +313,7 @@ namespace Barotrauma WorldRect : new Rectangle( (int)(Submarine.DrawPosition.X + WorldPosition.X), - (int)(Submarine.DrawPosition.Y + WorldPosition.Y), + (int)(Submarine.DrawPosition.Y + WorldPosition.Y), WorldRect.Width, WorldRect.Height); GUI.DrawLine(spriteBatch, @@ -326,22 +324,22 @@ namespace Barotrauma } } - public static void UpdateVertices(GraphicsDevice graphicsDevice, Camera cam, WaterRenderer renderer) + public static void UpdateVertices(Camera cam, WaterRenderer renderer) { foreach (EntityGrid entityGrid in EntityGrids) { - if (entityGrid.WorldRect.X > cam.WorldView.Right || entityGrid.WorldRect.Right < cam.WorldView.X) continue; - if (entityGrid.WorldRect.Y - entityGrid.WorldRect.Height > cam.WorldView.Y || entityGrid.WorldRect.Y < cam.WorldView.Y - cam.WorldView.Height) continue; + if (entityGrid.WorldRect.X > cam.WorldView.Right || entityGrid.WorldRect.Right < cam.WorldView.X) { continue; } + if (entityGrid.WorldRect.Y - entityGrid.WorldRect.Height > cam.WorldView.Y || entityGrid.WorldRect.Y < cam.WorldView.Y - cam.WorldView.Height) { continue; } var allEntities = entityGrid.GetAllEntities(); foreach (Hull hull in allEntities) { - hull.UpdateVertices(graphicsDevice, cam, entityGrid, renderer); + hull.UpdateVertices(cam, entityGrid, renderer); } } } - private void UpdateVertices(GraphicsDevice graphicsDevice, Camera cam, EntityGrid entityGrid, WaterRenderer renderer) + private void UpdateVertices(Camera cam, EntityGrid entityGrid, WaterRenderer renderer) { Vector2 submarinePos = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; @@ -554,11 +552,10 @@ namespace Barotrauma remoteOxygenPercentage = message.ReadRangedSingle(0.0f, 100.0f, 8); bool hasFireSources = message.ReadBoolean(); - int fireSourceCount = 0; remoteFireSources = new List(); if (hasFireSources) { - fireSourceCount = message.ReadRangedInteger(0, 16); + int fireSourceCount = message.ReadRangedInteger(0, 16); for (int i = 0; i < fireSourceCount; i++) { remoteFireSources.Add(new Vector3( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index 2a7258acb..f20bd6797 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -18,6 +18,7 @@ namespace Barotrauma foreach (Pair entity in DisplayEntities) { + if (entity.First is CoreEntityPrefab) { continue; } Rectangle drawRect = entity.Second; drawRect = new Rectangle( (int)(drawRect.X * scale) + drawArea.Center.X, (int)((drawRect.Y) * scale) - drawArea.Center.Y, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 1cbf20833..11d7d1736 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -93,9 +93,9 @@ namespace Barotrauma.Lights public static BasicEffect shadowEffect; public static BasicEffect penumbraEffect; - private Segment[] segments = new Segment[4]; - private SegmentPoint[] vertices = new SegmentPoint[4]; - private SegmentPoint[] losVertices = new SegmentPoint[4]; + private readonly Segment[] segments = new Segment[4]; + private readonly SegmentPoint[] vertices = new SegmentPoint[4]; + private readonly SegmentPoint[] losVertices = new SegmentPoint[4]; private readonly bool[] backFacing; private readonly bool[] ignoreEdge; @@ -106,6 +106,8 @@ namespace Barotrauma.Lights public VertexPositionTexture[] PenumbraVertices { get; private set; } public int ShadowVertexCount { get; private set; } + private readonly HashSet overlappingHulls = new HashSet(); + public MapEntity ParentEntity { get; private set; } private bool enabled; @@ -176,7 +178,7 @@ namespace Barotrauma.Lights if (door != null) { isHorizontal = door.IsHorizontal; } } - var chList = HullLists.Find(x => x.Submarine == parent.Submarine); + var chList = HullLists.Find(h => h.Submarine == parent.Submarine); if (chList == null) { chList = new ConvexHullList(parent.Submarine); @@ -194,10 +196,12 @@ namespace Barotrauma.Lights private void MergeOverlappingSegments(ConvexHull ch) { - if (ch == this) return; - + if (ch == this) { return; } + if (isHorizontal == ch.isHorizontal) { + if (BoundingBox == ch.BoundingBox) { return; } + //hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces) float mergeDist = 32; float mergeDistSqr = mergeDist * mergeDist; @@ -206,6 +210,7 @@ namespace Barotrauma.Lights for (int j = 0; j < ch.segments.Length; j++) { if (segments[i].IsHorizontal != ch.segments[j].IsHorizontal) { continue; } + if (ignoreEdge[i] || ch.ignoreEdge[j]) { continue; } //the segments must be at different sides of the convex hulls to be merged //(e.g. the right edge of a wall piece and the left edge of another one) @@ -247,6 +252,7 @@ namespace Barotrauma.Lights p.Y >= ch.BoundingBox.Y && p.Y <= ch.BoundingBox.Bottom) { ignoreEdge[i] = true; + overlappingHulls.Add(ch); } } } @@ -283,11 +289,25 @@ namespace Barotrauma.Lights } else { - losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = - (segment1.Start.Pos + segment2.End.Pos) / 2.0f; - losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = - (segment1.End.Pos + segment2.Start.Pos) / 2.0f; + if (Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.Start.Pos) < + Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.End.Pos)) + { + losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = + (segment1.Start.Pos + segment2.End.Pos) / 2.0f; + losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = + (segment1.End.Pos + segment2.Start.Pos) / 2.0f; + } + else + { + losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = + (segment1.End.Pos + segment2.Start.Pos) / 2.0f; + losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = + (segment1.Start.Pos + segment2.End.Pos) / 2.0f; + } } + + overlappingHulls.Add(segment2.ConvexHull); + segment2.ConvexHull.overlappingHulls.Add(this); } public void Rotate(Vector2 origin, float amount) @@ -328,7 +348,50 @@ namespace Barotrauma.Lights LastVertexChangeTime = (float)Timing.TotalTime; + overlappingHulls.Clear(); + for (int i = 0; i < 4; i++) + { + ignoreEdge[i] = false; + } + CalculateDimensions(); + + if (ParentEntity == null) { return; } + + var chList = HullLists.Find(h => h.Submarine == ParentEntity.Submarine); + if (chList != null) + { + overlappingHulls.Clear(); + foreach (ConvexHull ch in chList.List) + { + MergeOverlappingSegments(ch); + ch.MergeOverlappingSegments(this); + } + } + } + + public static void RecalculateAll(Submarine sub) + { + var chList = HullLists.Find(h => h.Submarine == sub); + if (chList != null) + { + foreach (ConvexHull ch in chList.List) + { + ch.overlappingHulls.Clear(); + for (int i = 0; i < 4; i++) + { + ch.ignoreEdge[i] = false; + } + } + for (int i = 0; i < chList.List.Count; i++) + { + for (int j = i + 1; j < chList.List.Count; j++) + { + chList.List[i].MergeOverlappingSegments(chList.List[j]); + chList.List[j].MergeOverlappingSegments(chList.List[i]); + } + } + } } public void SetVertices(Vector2[] points, Matrix? rotationMatrix = null) @@ -348,6 +411,8 @@ namespace Barotrauma.Lights ignoreEdge[i] = false; } + overlappingHulls.Clear(); + int margin = 0; if (Math.Abs(points[0].X - points[2].X) < Math.Abs(points[0].Y - points[2].Y)) { @@ -381,9 +446,10 @@ namespace Barotrauma.Lights if (ParentEntity == null) return; - var chList = HullLists.Find(x => x.Submarine == ParentEntity.Submarine); + var chList = HullLists.Find(h => h.Submarine == ParentEntity.Submarine); if (chList != null) { + overlappingHulls.Clear(); foreach (ConvexHull ch in chList.List) { MergeOverlappingSegments(ch); @@ -484,8 +550,8 @@ namespace Barotrauma.Lights //find beginning and ending vertices which //belong to the shadow - int startingIndex = 0; - int endingIndex = 0; + int startingIndex = -1; + int endingIndex = -1; for (int i = 0; i < 4; i++) { int currentEdge = i; @@ -498,6 +564,8 @@ namespace Barotrauma.Lights startingIndex = nextEdge; } + if (startingIndex == -1 || endingIndex == -1) { return; } + //nr of vertices that are in the shadow if (endingIndex > startingIndex) ShadowVertexCount = endingIndex - startingIndex + 1; @@ -663,7 +731,7 @@ namespace Barotrauma.Lights public void Remove() { - var chList = HullLists.Find(x => x.Submarine == ParentEntity.Submarine); + var chList = HullLists.Find(h => h.Submarine == ParentEntity.Submarine); if (chList != null) { @@ -672,8 +740,19 @@ namespace Barotrauma.Lights { HullLists.Remove(chList); } + foreach (ConvexHull ch2 in overlappingHulls) + { + for (int i = 0; i < 4; i++) + { + ch2.ignoreEdge[i] = false; + } + ch2.overlappingHulls.Remove(this); + foreach (ConvexHull ch in chList.List) + { + ch.MergeOverlappingSegments(ch2); + } + } } } } - } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index e0ed2a224..dd25fa123 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -11,18 +11,6 @@ namespace Barotrauma.Lights { class LightManager { - private const float AmbientLightUpdateInterval = 0.2f; - private const float AmbientLightFalloff = 0.8f; - - /// - /// Enables a feature that makes lights inside the hull increase the brightness of the entire hull - /// and adjacent ones to some extent, if there are gaps for the lights to pass through. - /// Prevents unnaturally dark looking shadows in otherwise well-lit submarines, but disabled at least for - /// the time being because it makes the lighting behave unpredictably and may cause rooms to appear - /// excessively bright if different lighting conditions aren't tested and accounted for. - /// - private static readonly bool UseHullSpecificAmbientLight = false; - public static Entity ViewTarget { get; set; } private float currLightMapScale; @@ -57,7 +45,7 @@ namespace Barotrauma.Lights public Effect LosEffect { get; private set; } public Effect SolidColorEffect { get; private set; } - private List lights; + private readonly List lights; public bool LosEnabled = true; public LosMode LosMode = LosMode.Transparent; @@ -66,13 +54,8 @@ namespace Barotrauma.Lights public bool ObstructVision; - private Texture2D visionCircle; + private readonly Texture2D visionCircle; - private Dictionary hullAmbientLights; - private Dictionary smoothedHullAmbientLights; - - private float ambientLightUpdateTimer; - public IEnumerable Lights { get { return lights; } @@ -80,7 +63,7 @@ namespace Barotrauma.Lights public LightManager(GraphicsDevice graphics, ContentManager content) { - lights = new List(); + lights = new List(100); AmbientLight = new Color(20, 20, 20, 255); @@ -114,9 +97,6 @@ namespace Barotrauma.Lights }; } }); - - hullAmbientLights = new Dictionary(); - smoothedHullAmbientLights = new Dictionary(); } private void CreateRenderTargets(GraphicsDevice graphics) @@ -167,43 +147,12 @@ namespace Barotrauma.Lights } } - public void Update(float deltaTime) - { - if (UseHullSpecificAmbientLight) - { - if (ambientLightUpdateTimer > 0.0f) - { - ambientLightUpdateTimer -= deltaTime; - } - else - { - CalculateAmbientLights(); - ambientLightUpdateTimer = AmbientLightUpdateInterval; - } - - foreach (Hull hull in hullAmbientLights.Keys) - { - if (!smoothedHullAmbientLights.ContainsKey(hull)) - { - smoothedHullAmbientLights.Add(hull, Color.TransparentBlack); - } - } - - foreach (Hull hull in smoothedHullAmbientLights.Keys.ToList()) - { - Color targetColor = Color.TransparentBlack; - hullAmbientLights.TryGetValue(hull, out targetColor); - smoothedHullAmbientLights[hull] = Color.Lerp(smoothedHullAmbientLights[hull], targetColor, deltaTime); - } - } - } - - private List activeLights = new List(capacity: 100); + private readonly List activeLights = new List(capacity: 100); public void UpdateLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) { - if (!LightingEnabled) return; - + if (!LightingEnabled) { return; } + if (Math.Abs(currLightMapScale - GameMain.Config.LightMapScale) > 0.01f) { //lightmap scale has changed -> recreate render targets @@ -261,16 +210,14 @@ namespace Barotrauma.Lights //draw background lights //--------------------------------------------------------------------------------------------------- graphics.SetRenderTarget(LightMap); - graphics.Clear(Color.Black); + graphics.Clear(AmbientLight); graphics.BlendState = BlendState.Additive; - bool backgroundSpritesDrawn = false; spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); foreach (LightSource light in activeLights) { if (!light.IsBackground) { continue; } light.DrawSprite(spriteBatch, cam); if (light.Color.A > 0 && light.Range > 0.0f) { light.DrawLightVolume(spriteBatch, lightEffect, transform); } - backgroundSpritesDrawn = true; } GameMain.ParticleManager.Draw(spriteBatch, true, null, Particles.ParticleBlendState.Additive); spriteBatch.End(); @@ -278,33 +225,34 @@ namespace Barotrauma.Lights //draw a black rectangle on hulls to hide background lights behind subs //--------------------------------------------------------------------------------------------------- - Dictionary visibleHulls = null; - if (backgroundSpritesDrawn) + if (backgroundObstructor != null) { - if (backgroundObstructor != null) - { - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); - spriteBatch.Draw(backgroundObstructor, new Rectangle(0, 0, - (int)(GameMain.GraphicsWidth * currLightMapScale), (int)(GameMain.GraphicsHeight * currLightMapScale)), Color.Black); - spriteBatch.End(); - } - - visibleHulls = GetVisibleHulls(cam); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, transformMatrix: spriteBatchTransform); - foreach (Rectangle drawRect in visibleHulls.Values) - { - //TODO: draw some sort of smoothed rectangle - GUI.DrawRectangle(spriteBatch, - new Vector2(drawRect.X, -drawRect.Y), - new Vector2(drawRect.Width, drawRect.Height), - Color.Black, true); - } + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); + spriteBatch.Draw(backgroundObstructor, new Rectangle(0, 0, + (int)(GameMain.GraphicsWidth * currLightMapScale), (int)(GameMain.GraphicsHeight * currLightMapScale)), Color.Black); spriteBatch.End(); - - - graphics.BlendState = BlendState.Additive; } + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, transformMatrix: spriteBatchTransform); + Dictionary visibleHulls = GetVisibleHulls(cam); + foreach (KeyValuePair hull in visibleHulls) + { + GUI.DrawRectangle(spriteBatch, + new Vector2(hull.Value.X, -hull.Value.Y), + new Vector2(hull.Value.Width, hull.Value.Height), + hull.Key.AmbientLight == Color.TransparentBlack ? Color.Black : hull.Key.AmbientLight.Multiply(hull.Key.AmbientLight.A / 255.0f), true); + } + spriteBatch.End(); + + SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; + SolidColorEffect.Parameters["color"].SetValue(AmbientLight.ToVector4()); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform, effect: SolidColorEffect); + Submarine.DrawDamageable(spriteBatch, null); + spriteBatch.End(); + + graphics.BlendState = BlendState.Additive; + + //draw the focused item and character to highlight them, //and light sprites (done before drawing the actual light volumes so we can make characters obstruct the highlights and sprites) //--------------------------------------------------------------------------------------------------- @@ -327,33 +275,37 @@ namespace Barotrauma.Lights //draw characters to obstruct the highlighted items/characters and light sprites //--------------------------------------------------------------------------------------------------- - SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; - SolidColorEffect.Parameters["color"].SetValue(Color.Black.ToVector4()); + SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidVertexColor"]; spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); foreach (Character character in Character.CharacterList) { - if (character.CurrentHull == null || !character.Enabled) continue; - if (Character.Controlled?.FocusedCharacter == character) continue; + if (character.CurrentHull == null || !character.Enabled) { continue; } + if (Character.Controlled?.FocusedCharacter == character) { continue; } + Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? + Color.Black : + character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); foreach (Limb limb in character.AnimController.Limbs) { - if (limb.DeformSprite != null) continue; - limb.Draw(spriteBatch, cam, Color.Black); + if (limb.DeformSprite != null) { continue; } + limb.Draw(spriteBatch, cam, lightColor); } } spriteBatch.End(); - DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidColor"]; - DeformableSprite.Effect.Parameters["solidColor"].SetValue(Color.Black.ToVector4()); + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidVertexColor"]; DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform); foreach (Character character in Character.CharacterList) { - if (character.CurrentHull == null || !character.Enabled) continue; - if (Character.Controlled?.FocusedCharacter == character) continue; + if (character.CurrentHull == null || !character.Enabled) { continue; } + if (Character.Controlled?.FocusedCharacter == character) { continue; } + Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? + Color.Black : + character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); foreach (Limb limb in character.AnimController.Limbs) { - if (limb.DeformSprite == null) continue; - limb.Draw(spriteBatch, cam, Color.Black); + if (limb.DeformSprite == null) { continue; } + limb.Draw(spriteBatch, cam, lightColor); } } spriteBatch.End(); @@ -364,8 +316,6 @@ namespace Barotrauma.Lights //--------------------------------------------------------------------------------------------------- spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); - GUI.DrawRectangle(spriteBatch, new Rectangle(cam.WorldView.X, -cam.WorldView.Y, cam.WorldView.Width, cam.WorldView.Height), AmbientLight, isFilled: true); - spriteBatch.Draw(LimbLightMap, new Rectangle(cam.WorldView.X, -cam.WorldView.Y, cam.WorldView.Width, cam.WorldView.Height), Color.White); foreach (ElectricalDischarger discharger in ElectricalDischarger.List) @@ -382,24 +332,7 @@ namespace Barotrauma.Lights lightEffect.World = transform; GameMain.ParticleManager.Draw(spriteBatch, false, null, Particles.ParticleBlendState.Additive); - - if (UseHullSpecificAmbientLight) - { - if (visibleHulls == null) - { - visibleHulls = GetVisibleHulls(cam); - } - foreach (Hull hull in smoothedHullAmbientLights.Keys) - { - if (smoothedHullAmbientLights[hull].A < 0.01f) continue; - if (!visibleHulls.TryGetValue(hull, out Rectangle drawRect)) continue; - GUI.DrawRectangle(spriteBatch, - new Vector2(drawRect.X, -drawRect.Y), - new Vector2(hull.Rect.Width, hull.Rect.Height), - smoothedHullAmbientLights[hull], true); - } - } - + if (Character.Controlled != null) { Vector2 haloDrawPos = Character.Controlled.DrawPosition; @@ -601,7 +534,10 @@ namespace Barotrauma.Lights } } - penumbraVerts.AddRange(convexHull.PenumbraVertices); + if (convexHull.ShadowVertexCount > 0) + { + penumbraVerts.AddRange(convexHull.PenumbraVertices); + } } if (shadowVerts.Count > 0) @@ -622,86 +558,6 @@ namespace Barotrauma.Lights graphics.SetRenderTarget(null); } - - private void CalculateAmbientLights() - { - hullAmbientLights.Clear(); - - foreach (LightSource light in lights) - { - if (light.Color.A < 1f || light.Range < 1.0f || light.IsBackground) continue; - - var newAmbientLights = AmbientLightHulls(light); - foreach (Hull hull in newAmbientLights.Keys) - { - if (hullAmbientLights.ContainsKey(hull)) - { - //hull already lit by some other light source -> add the ambient lights up - hullAmbientLights[hull] = new Color( - hullAmbientLights[hull].R + newAmbientLights[hull].R, - hullAmbientLights[hull].G + newAmbientLights[hull].G, - hullAmbientLights[hull].B + newAmbientLights[hull].B, - hullAmbientLights[hull].A + newAmbientLights[hull].A); - } - else - { - hullAmbientLights.Add(hull, newAmbientLights[hull]); - } - } - } - } - - /// - /// Add ambient light to the hull the lightsource is inside + all adjacent hulls connected by a gap - /// - private Dictionary AmbientLightHulls(LightSource light) - { - Dictionary hullAmbientLight = new Dictionary(); - - var hull = Hull.FindHull(light.WorldPosition); - if (hull == null) return hullAmbientLight; - - return AmbientLightHulls(hull, hullAmbientLight, light.Color * Math.Min(light.Range / 1000.0f, 1.0f)); - } - - /// - /// A flood fill algorithm that adds ambient light to all hulls the starting hull is connected to - /// - private Dictionary AmbientLightHulls(Hull hull, Dictionary hullAmbientLight, Color currColor) - { - if (hullAmbientLight.ContainsKey(hull)) - { - if (hullAmbientLight[hull].A > currColor.A) - return hullAmbientLight; - else - hullAmbientLight[hull] = currColor; - } - else - { - hullAmbientLight.Add(hull, currColor); - } - - Color nextHullLight = currColor * AmbientLightFalloff; - //light getting too dark to notice -> no need to spread further - if (nextHullLight.A < 20) return hullAmbientLight; - - //use hashset to make sure that each hull is only included once - HashSet hulls = new HashSet(); - foreach (Gap g in hull.ConnectedGaps) - { - if (!g.IsRoomToRoom || !g.PassAmbientLight || g.Open < 0.5f) continue; - - hulls.Add((g.linkedTo[0] == hull ? g.linkedTo[1] : g.linkedTo[0]) as Hull); - } - - foreach (Hull h in hulls) - { - hullAmbientLight = AmbientLightHulls(h, hullAmbientLight, nextHullLight); - } - - return hullAmbientLight; - } - public void ClearLights() { lights.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 8a9114105..76bcc63ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Lights public Dictionary SerializableProperties { get; private set; } = new Dictionary(); - [Serialize("1.0,1.0,1.0,1.0", true), Editable] + [Serialize("1.0,1.0,1.0,1.0", true, alwaysUseInstanceValues: true), Editable] public Color Color { get; @@ -25,7 +25,7 @@ namespace Barotrauma.Lights private float range; - [Serialize(100.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] + [Serialize(100.0f, true, alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] public float Range { get { return range; } @@ -331,7 +331,7 @@ namespace Barotrauma.Lights if (lightSourceParams.DeformableLightSpriteElement != null) { - DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement); + DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement, invert: true); } } @@ -342,7 +342,7 @@ namespace Barotrauma.Lights lightSourceParams.Persistent = true; if (lightSourceParams.DeformableLightSpriteElement != null) { - DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement); + DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement, invert: true); } } @@ -952,23 +952,44 @@ namespace Barotrauma.Lights { Vector2 origin = DeformableLightSprite.Origin; Vector2 drawPos = position; - if (ParentSub != null) drawPos += ParentSub.DrawPosition; + if (ParentSub != null) + { + drawPos += ParentSub.DrawPosition; + } + + if (LightSpriteEffect == SpriteEffects.FlipHorizontally) + { + origin.X = DeformableLightSprite.Sprite.SourceRect.Width - origin.X; + } + if (LightSpriteEffect == SpriteEffects.FlipVertically) + { + origin.Y = DeformableLightSprite.Sprite.SourceRect.Height - origin.Y; + } DeformableLightSprite.Draw( cam, new Vector3(drawPos, 0.0f), origin, -Rotation, SpriteScale, new Color(Color, lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f), - LightSpriteEffect == SpriteEffects.FlipHorizontally); + LightSpriteEffect == SpriteEffects.FlipVertically); } if (LightSprite != null) { Vector2 origin = LightSprite.Origin; - if (LightSpriteEffect == SpriteEffects.FlipHorizontally) origin.X = LightSprite.SourceRect.Width - origin.X; - if (LightSpriteEffect == SpriteEffects.FlipVertically) origin.Y = LightSprite.SourceRect.Height - origin.Y; + if (LightSpriteEffect == SpriteEffects.FlipHorizontally) + { + origin.X = LightSprite.SourceRect.Width - origin.X; + } + if (LightSpriteEffect == SpriteEffects.FlipVertically) + { + origin.Y = LightSprite.SourceRect.Height - origin.Y; + } Vector2 drawPos = position; - if (ParentSub != null) drawPos += ParentSub.DrawPosition; + if (ParentSub != null) + { + drawPos += ParentSub.DrawPosition; + } drawPos.Y = -drawPos.Y; LightSprite.Draw( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index a615e62fa..d3e649c98 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 7f244e92f..5bb384020 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -151,7 +151,8 @@ namespace Barotrauma { selectedList.ForEach(e => { - e.Remove(); + //orphaned wires may already have been removed + if (!e.Removed) { e.Remove(); } }); selectedList.Clear(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 71de94cc1..a8b74b8b5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -24,6 +24,10 @@ namespace Barotrauma { get { + if (!GameMain.SubEditorScreen.ShowThalamus && prefab.Category.HasFlag(MapEntityCategory.Thalamus)) + { + return false; + } return HasBody ? ShowWalls : ShowStructures; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 31b597590..cd717dad2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -6,7 +6,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; @@ -31,6 +31,7 @@ namespace Barotrauma Stream = sound.Stream; Range = element.GetAttributeFloat("range", 1000.0f); Volume = element.GetAttributeFloat("volume", 1.0f); + sound.IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); } } @@ -87,7 +88,7 @@ namespace Barotrauma existingSound = GameMain.SoundManager.LoadSound(filename, stream); if (existingSound == null) { return null; } } - catch (FileNotFoundException e) + catch (System.IO.FileNotFoundException e) { string errorMsg = "Failed to load sound file \"" + filename + "\"."; DebugConsole.ThrowError(errorMsg, e); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index b638ec40f..a168a3952 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Xml.Linq; @@ -19,9 +19,9 @@ namespace Barotrauma { try { - using (MemoryStream mem = new MemoryStream(Convert.FromBase64String(previewImageData))) + using (System.IO.MemoryStream mem = new System.IO.MemoryStream(Convert.FromBase64String(previewImageData))) { - var texture = TextureLoader.FromStream(mem, path: FilePath); + var texture = TextureLoader.FromStream(mem, path: FilePath, compress: false); if (texture == null) { throw new Exception("PreviewImage texture returned null"); } PreviewImage = new Sprite(texture, null, null); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 255b39587..8baa8eb61 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -9,7 +9,7 @@ namespace Barotrauma { partial class WayPoint : MapEntity { - private static Dictionary iconSprites; + private static Dictionary iconSprites; private const int WaypointSize = 12, SpawnPointSize = 32; public override bool IsVisible(Rectangle worldView) @@ -56,10 +56,18 @@ namespace Barotrauma Color.White); } - Sprite sprite = iconSprites[SpawnType]; + Sprite sprite = iconSprites[SpawnType.ToString()]; if (spawnType == SpawnType.Human && AssignedJob?.Icon != null) { - sprite = iconSprites[SpawnType.Path]; + sprite = iconSprites["Path"]; + } + else if (ConnectedDoor != null) + { + sprite = iconSprites["Door"]; + } + else if (Ladders != null) + { + sprite = iconSprites["Ladder"]; } sprite.Draw(spriteBatch, drawPos, clr, scale: iconSize / (float)sprite.SourceRect.Width, depth: 0.001f); sprite.RelativeOrigin = Vector2.One * 0.5f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs b/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs index 953104e5f..eaf0966b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using Barotrauma.IO; using System.Collections.Generic; using System.Text; using System.Threading; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs index e0915a5af..4c754132a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.IO.Pipes; using System.Text; using System.Threading; @@ -18,8 +18,8 @@ namespace Barotrauma.Networking public static void Start(ProcessStartInfo processInfo) { - writePipe = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable); - readPipe = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable); + writePipe = new AnonymousPipeServerStream(PipeDirection.Out, System.IO.HandleInheritability.Inheritable); + readPipe = new AnonymousPipeServerStream(PipeDirection.In, System.IO.HandleInheritability.Inheritable); writeStream = writePipe; readStream = readPipe; @@ -38,6 +38,13 @@ namespace Barotrauma.Networking localHandlesDisposed = true; } + public static void ClosePipes() + { + writePipe?.Close(); + readPipe?.Close(); + shutDown = true; + } + public static void ShutDown() { Process?.Kill(); Process = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 76d3e90a0..22a91415b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading; using System.Xml; @@ -100,7 +100,7 @@ namespace Barotrauma.Networking WriteStream = null; } - WriteStream = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None); + WriteStream = File.Open(FilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write); TimeStarted = Environment.TickCount; } @@ -259,7 +259,7 @@ namespace Barotrauma.Networking { newTransfer.OpenStream(); } - catch (IOException e) + catch (System.IO.IOException e) { if (i < maxRetries) { @@ -422,7 +422,7 @@ namespace Barotrauma.Networking } if (string.IsNullOrEmpty(fileName) || - fileName.IndexOfAny(Path.GetInvalidFileNameChars()) > -1) + fileName.IndexOfAny(Path.GetInvalidFileNameChars().ToArray()) > -1) { errorMessage = "Illegal characters in file name ''" + fileName + "''"; return false; @@ -455,7 +455,7 @@ namespace Barotrauma.Networking switch (fileTransfer.FileType) { case FileTransferType.Submarine: - Stream stream; + System.IO.Stream stream; try { stream = SaveUtil.DecompressFiletoStream(fileTransfer.FilePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index d234e9be3..87983a95b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -3,7 +3,7 @@ using Barotrauma.Steam; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.IO.Compression; using System.Linq; using System.Text; @@ -52,7 +52,7 @@ namespace Barotrauma.Networking public GUITickBox EndVoteTickBox; private GUIComponent buttonContainer; - private NetStats netStats; + public readonly NetStats NetStats; protected GUITickBox cameraFollowsSub; @@ -169,9 +169,9 @@ namespace Barotrauma.Networking allowReconnect = true; - netStats = new NetStats(); + NetStats = new NetStats(); - inGameHUD = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null) + inGameHUD = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) { CanBeFocused = false }; @@ -569,14 +569,7 @@ namespace Barotrauma.Networking } } - /*TODO: reimplement - if (ShowNetStats && client?.ServerConnection != null) - { - netStats.AddValue(NetStats.NetStatType.ReceivedBytes, client.ServerConnection.Statistics.ReceivedBytes); - netStats.AddValue(NetStats.NetStatType.SentBytes, client.ServerConnection.Statistics.SentBytes); - netStats.AddValue(NetStats.NetStatType.ResentMessages, client.ServerConnection.Statistics.ResentMessages); - netStats.Update(deltaTime); - }*/ + NetStats.Update(deltaTime); UpdateHUD(deltaTime); @@ -781,7 +774,7 @@ namespace Barotrauma.Networking if (readyToStart && !CoroutineManager.IsCoroutineRunning("WaitForStartRound")) { - CoroutineManager.StartCoroutine(GameMain.NetLobbyScreen.WaitForStartRound(startButton: null, allowCancel: false), "WaitForStartRound"); + CoroutineManager.StartCoroutine(GameMain.NetLobbyScreen.WaitForStartRound(startButton: null), "WaitForStartRound"); } break; case ServerPacketHeader.STARTGAME: @@ -1455,6 +1448,15 @@ namespace Barotrauma.Networking var teamID = i == 0 ? Character.TeamType.Team1 : Character.TeamType.Team2; Submarine.MainSubs[i].TeamID = teamID; + foreach (Item item in Item.ItemList) + { + if (item.Submarine == null) { continue; } + if (item.Submarine != Submarine.MainSubs[i] && !Submarine.MainSubs[i].DockedTo.Contains(item.Submarine)) { continue; } + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = Submarine.MainSubs[i].TeamID; + } + } foreach (Submarine sub in Submarine.MainSubs[i].DockedTo) { sub.TeamID = teamID; @@ -1891,8 +1893,8 @@ namespace Barotrauma.Networking DebugConsole.ThrowError("Writing object data to \"crashreport_object.bin\", please send this file to us at http://github.com/Regalis11/Barotrauma/issues"); - using (FileStream fl = File.Open("crashreport_object.bin", FileMode.Create)) - using (BinaryWriter sw = new BinaryWriter(fl)) + using (FileStream fl = File.Open("crashreport_object.bin", System.IO.FileMode.Create)) + using (System.IO.BinaryWriter sw = new System.IO.BinaryWriter(fl)) { sw.Write(inc.Buffer, (int)(prevBytePos - prevByteLength), (int)(prevByteLength)); } @@ -2759,15 +2761,15 @@ namespace Barotrauma.Networking if (!ShowNetStats) return; - netStats.Draw(spriteBatch, new Rectangle(300, 10, 300, 150)); + NetStats.Draw(spriteBatch, new Rectangle(300, 10, 300, 150)); + /* TODO: reimplement int width = 200, height = 300; int x = GameMain.GraphicsWidth - width, y = (int)(GameMain.GraphicsHeight * 0.3f); GUI.DrawRectangle(spriteBatch, new Rectangle(x, y, width, height), Color.Black * 0.7f, true); GUI.Font.DrawString(spriteBatch, "Network statistics:", new Vector2(x + 10, y + 10), Color.White); - /* TODO: reimplement if (client.ServerConnection != null) { GUI.Font.DrawString(spriteBatch, "Ping: " + (int)(client.ServerConnection.AverageRoundtripTime * 1000.0f) + " ms", new Vector2(x + 10, y + 25), Color.White); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs index 90cd0a00e..eb34d3cdd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs @@ -14,10 +14,10 @@ namespace Barotrauma.Networking ResentMessages = 2 } - private Graph[] graphs; + private readonly Graph[] graphs; - private float[] totalValue; - private float[] lastValue; + private readonly float[] totalValue; + private readonly float[] lastValue; const float UpdateInterval = 0.1f; float updateTimer; @@ -37,9 +37,7 @@ namespace Barotrauma.Networking public void AddValue(NetStatType statType, float value) { float valueChange = value - lastValue[(int)statType]; - totalValue[(int)statType] += valueChange; - lastValue[(int)statType] = value; } @@ -51,7 +49,6 @@ namespace Barotrauma.Networking for (int i = 0; i < 3; i++) { - graphs[i].Update(totalValue[i] / UpdateInterval); totalValue[i] = 0.0f; } @@ -64,23 +61,22 @@ namespace Barotrauma.Networking GUI.DrawRectangle(spriteBatch, rect, Color.Black * 0.4f, true); graphs[(int)NetStatType.ReceivedBytes].Draw(spriteBatch, rect, null, 0.0f, Color.Cyan); - graphs[(int)NetStatType.SentBytes].Draw(spriteBatch, rect, null, 0.0f, GUI.Style.Orange); - - graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, null, 0.0f, GUI.Style.Red); + if (graphs[(int)NetStatType.ResentMessages].Average() > 0) + { + graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, null, 0.0f, GUI.Style.Red); + GUI.SmallFont.DrawString(spriteBatch, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", + new Vector2(rect.Right + 10, rect.Y + 50), GUI.Style.Red); + } GUI.SmallFont.DrawString(spriteBatch, "Peak received: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.ReceivedBytes].LargestValue()) + "/s " + "Avg received: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.ReceivedBytes].Average()) + "/s", new Vector2(rect.Right + 10, rect.Y + 10), Color.Cyan); - GUI.SmallFont.DrawString(spriteBatch, "Peak sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].LargestValue()) + "/s " + "Avg sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].Average()) + "/s", new Vector2(rect.Right + 10, rect.Y + 30), GUI.Style.Orange); - - GUI.SmallFont.DrawString(spriteBatch, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", - new Vector2(rect.Right + 10, rect.Y + 50), GUI.Style.Red); #if DEBUG /*int y = 10; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index e104aa388..58cf2f388 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -97,6 +97,9 @@ namespace Barotrauma.Networking incomingLidgrenMessages.Clear(); netClient.ReadMessages(incomingLidgrenMessages); + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, netClient.Statistics.ReceivedBytes); + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, netClient.Statistics.SentBytes); + foreach (NetIncomingMessage inc in incomingLidgrenMessages) { if (inc.SenderConnection != (ServerConnection as LidgrenConnection).NetConnection) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index a2dd50d8f..ff72730d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -18,6 +18,8 @@ namespace Barotrauma.Networking private double timeout; private double heartbeatTimer; + private long sentBytes, receivedBytes; + private List incomingInitializationMessages; private List incomingDataMessages; @@ -63,6 +65,7 @@ namespace Barotrauma.Networking outMsg.Write((byte)ConnectionInitialization.ConnectionStarted); Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; initializationStep = ConnectionInitialization.SteamTicketAndVersion; @@ -99,11 +102,11 @@ namespace Barotrauma.Networking if (isConnectionInitializationStep) { ulong low = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8); - ulong high = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8+32); + ulong high = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8 + 32); ulong lobbyId = low + (high << 32); Steam.SteamManager.JoinLobby(lobbyId, false); - IReadMessage inc = new ReadOnlyMessage(data, false, 1+8, dataLength - 9, ServerConnection); + IReadMessage inc = new ReadOnlyMessage(data, false, 1 + 8, dataLength - 9, ServerConnection); if (initializationStep != ConnectionInitialization.Success) { incomingInitializationMessages.Add(inc); @@ -137,16 +140,20 @@ namespace Barotrauma.Networking timeout -= deltaTime; heartbeatTimer -= deltaTime; - for (int i=0;i<100;i++) + for (int i = 0; i < 100; i++) { if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } var packet = Steamworks.SteamNetworking.ReadP2PPacket(); if (packet.HasValue) { OnP2PData(packet?.SteamId ?? 0, packet?.Data, packet?.Data.Length ?? 0, 0); + receivedBytes += packet?.Data.Length ?? 0; } } + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes); + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes); + if (heartbeatTimer < 0.0) { IWriteMessage outMsg = new WriteOnlyMessage(); @@ -154,6 +161,7 @@ namespace Barotrauma.Networking outMsg.Write((byte)PacketHeader.IsHeartbeatMessage); Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Unreliable); + sentBytes += outMsg.LengthBytes; heartbeatTimer = 5.0; } @@ -227,6 +235,7 @@ namespace Barotrauma.Networking heartbeatTimer = 5.0; Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; break; case ConnectionInitialization.ContentPackageOrder: if (initializationStep == ConnectionInitialization.SteamTicketAndVersion || @@ -254,7 +263,7 @@ namespace Barotrauma.Networking } Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - + sentBytes += outMsg.LengthBytes; break; case ConnectionInitialization.Password: if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { initializationStep = ConnectionInitialization.Password; } @@ -334,6 +343,7 @@ namespace Barotrauma.Networking private void Send(byte[] buf, int length, Steamworks.P2PSend sendType) { bool successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, buf, length + 4, 0, sendType); + sentBytes += length + 4; if (!successSend) { if (sendType != Steamworks.P2PSend.Reliable) @@ -341,6 +351,7 @@ namespace Barotrauma.Networking DebugConsole.Log("WARNING: message couldn't be sent unreliably, forcing reliable send (" + length.ToString() + " bytes)"); sendType = Steamworks.P2PSend.Reliable; successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, buf, length + 4, 0, sendType); + sentBytes += length + 4; } if (!successSend) { @@ -364,6 +375,7 @@ namespace Barotrauma.Networking heartbeatTimer = 5.0; Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; } public override void Close(string msg = null) @@ -380,6 +392,7 @@ namespace Barotrauma.Networking outMsg.Write(msg ?? "Disconnected"); Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; Thread.Sleep(100); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index cf5534b9e..5bc6e9ec2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -13,7 +13,9 @@ namespace Barotrauma.Networking private bool isActive; private ConnectionInitialization initializationStep; - private UInt64 selfSteamID; + private readonly UInt64 selfSteamID; + + private long sentBytes, receivedBytes; class RemotePeer { @@ -204,22 +206,24 @@ namespace Barotrauma.Networking } } - for (int i=0;i<100;i++) + for (int i = 0; i < 100; i++) { if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } var packet = Steamworks.SteamNetworking.ReadP2PPacket(); if (packet.HasValue) { OnP2PData(packet?.SteamId ?? 0, packet?.Data, packet?.Data.Length ?? 0, 0); + receivedBytes += packet?.Data.Length ?? 0; } } + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes); + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes); + while (ChildServerRelay.Read(out byte[] incBuf)) { ChildServerRelay.DisposeLocalHandles(); - IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, ServerConnection); - HandleDataMessage(inc); } } @@ -295,6 +299,7 @@ namespace Barotrauma.Networking } bool successSend = Steamworks.SteamNetworking.SendP2PPacket(recipientSteamId, p2pData, p2pData.Length, 0, sendType); + sentBytes += p2pData.Length; if (!successSend) { @@ -303,6 +308,7 @@ namespace Barotrauma.Networking DebugConsole.Log("WARNING: message couldn't be sent unreliably, forcing reliable send (" + p2pData.Length.ToString() + " bytes)"); sendType = Steamworks.P2PSend.Reliable; successSend = Steamworks.SteamNetworking.SendP2PPacket(recipientSteamId, p2pData, p2pData.Length, 0, sendType); + sentBytes += p2pData.Length; } if (!successSend) { @@ -336,7 +342,6 @@ namespace Barotrauma.Networking byte[] msgToSend = (byte[])outMsg.Buffer.Clone(); Array.Resize(ref msgToSend, outMsg.LengthBytes); ChildServerRelay.Write(msgToSend); - return; } else @@ -369,6 +374,7 @@ namespace Barotrauma.Networking outMsg.Write(msg); Steamworks.SteamNetworking.SendP2PPacket(peer.SteamID, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; } else { @@ -405,7 +411,7 @@ namespace Barotrauma.Networking ClosePeerSession(remotePeers[i]); } - ChildServerRelay.ShutDown(); + ChildServerRelay.ClosePipes(); OnDisconnect?.Invoke(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs index 93b64d6ae..0e1f02597 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs @@ -26,10 +26,13 @@ namespace Barotrauma.Networking public void CreateLogFrame() { - LogFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + LogFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) LogFrame = null; return true; } }; + + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, LogFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + new GUIButton(new RectTransform(Vector2.One, LogFrame.RectTransform), "", style: null).OnClicked += (btn, userData) => { LogFrame = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index c299c8380..22f0dc0a1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -266,7 +266,9 @@ namespace Barotrauma.Networking } //background frame - settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null, color: Color.Black * 0.5f); + settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, settingsFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + new GUIButton(new RectTransform(Vector2.One, settingsFrame.RectTransform), "", style: null).OnClicked += (btn, userData) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) { ToggleSettingsFrame(btn, userData); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 81286c1c7..d603cf0a1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -2,13 +2,12 @@ using RestSharp; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using RestSharp.Contrib; using System.Xml.Linq; -using System.Xml; using Color = Microsoft.Xna.Framework.Color; using System.Runtime.InteropServices; @@ -250,7 +249,8 @@ namespace Barotrauma.Steam } }; - Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery().FilterDistanceWorldwide(); + //TODO: find a better strategy to fetch all lobbies, this is gonna take forever if we actually have 10000 lobbies + Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery().FilterDistanceWorldwide().WithMaxResults(10000); TaskPool.Add(Task.Run(async () => { @@ -578,7 +578,7 @@ namespace Barotrauma.Steam if (!isInitialized) return; var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) - .RankedByTotalUniqueSubscriptions() + .RankedByTrend() .WithLongDescription(); if (requireTags != null) query.WithTags(requireTags); @@ -754,7 +754,7 @@ namespace Barotrauma.Steam if (!CheckWorkshopItemEnabled(existingItem)) { - if (!EnableWorkShopItem(existingItem, false, out string errorMsg)) + if (!EnableWorkShopItem(existingItem, out string errorMsg)) { DebugConsole.NewMessage(errorMsg, Color.Red); new GUIMessageBox( @@ -881,6 +881,10 @@ namespace Barotrauma.Steam DebugConsole.NewMessage("Published workshop item " + item?.Title + " successfully.", Microsoft.Xna.Framework.Color.LightGreen); contentPackage.SteamWorkshopUrl = $"http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={task.Result.FileId.Value}"; + //NOTE: This sets InstallTime one hour into the future to guarantee + //that the published content package won't be autoupdated incorrectly. + //Change if it causes issues. + contentPackage.InstallTime = DateTime.UtcNow + TimeSpan.FromHours(1); contentPackage.Save(contentPackage.Path); SubscribeToWorkshopItem(task.Result.FileId); @@ -892,7 +896,7 @@ namespace Barotrauma.Steam /// /// Enables a workshop item by moving it to the game folder. /// - public static bool EnableWorkShopItem(Steamworks.Ugc.Item? item, bool allowFileOverwrite, out string errorMsg, bool selectContentPackage = false, bool suppressInstallNotif = false) + public static bool EnableWorkShopItem(Steamworks.Ugc.Item? item, out string errorMsg, bool selectContentPackage = false, bool suppressInstallNotif = false) { if (!(item?.IsInstalled ?? false)) { @@ -916,7 +920,8 @@ namespace Barotrauma.Steam }; string newContentPackagePath = GetWorkshopItemContentPackagePath(contentPackage); - if (ContentPackage.List.Any(cp => cp.Path.CleanUpPath() == newContentPackagePath.CleanUpPath())) + List existingPackages = ContentPackage.List.Where(cp => cp.Path.CleanUpPath() == newContentPackagePath.CleanUpPath()).ToList(); + if (existingPackages.Any()) { if (item?.Owner.Id != Steamworks.SteamClient.SteamId) { @@ -952,15 +957,9 @@ namespace Barotrauma.Steam { if (modCopiesInProgress.ContainsKey(item.Value.Id)) { - if (!modCopiesInProgress[item.Value.Id].IsCompleted && - !modCopiesInProgress[item.Value.Id].IsFaulted && - !modCopiesInProgress[item.Value.Id].IsCanceled) - { - errorMsg = ""; return true; - } - modCopiesInProgress.Remove(item.Value.Id); + errorMsg = ""; return true; } - newTask = CopyWorkShopItemAsync(item, contentPackage, newContentPackagePath, metaDataFilePath, allowFileOverwrite); + newTask = CopyWorkShopItemAsync(item, contentPackage, newContentPackagePath, metaDataFilePath); modCopiesInProgress.Add(item.Value.Id, newTask); } @@ -968,67 +967,85 @@ namespace Barotrauma.Steam contentPackage, (task, cp) => { - if (task.IsFaulted || task.IsCanceled) + try { - DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\"", task.Exception); - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); - return; - } - if (!string.IsNullOrWhiteSpace(task.Result)) - { - DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\": {task.Result}"); - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); - return; - } - - GameMain.Config.SuppressModFolderWatcher = true; - - var newPackage = new ContentPackage(cp.Path, newContentPackagePath) - { - SteamWorkshopUrl = item?.Url, - InstallTime = item?.Updated > item?.Created ? item?.Updated : item?.Created - }; - - foreach (ContentFile contentFile in newPackage.Files) - { - contentFile.Path = CorrectContentFilePath(contentFile.Path, cp, true); - } - - if (!Directory.Exists(Path.GetDirectoryName(newContentPackagePath))) - { - Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); - } - newPackage.Save(newContentPackagePath); - ContentPackage.List.Add(newPackage); - - if (selectContentPackage) - { - if (newPackage.CorePackage) + if (task.IsFaulted || task.IsCanceled) { - GameMain.Config.SelectCorePackage(newPackage); + DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\"", task.Exception); + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); + return; } - else + if (!string.IsNullOrWhiteSpace(task.Result)) { - GameMain.Config.SelectContentPackage(newPackage); + DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\": {task.Result}"); + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); + return; } - GameMain.Config.SaveNewPlayerConfig(); - GameMain.Config.WarnIfContentPackageSelectionDirty(); + GameMain.Config.SuppressModFolderWatcher = true; - if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) + var newPackage = new ContentPackage(cp.Path, newContentPackagePath) { - SubmarineInfo.RefreshSavedSubs(); + SteamWorkshopUrl = item?.Url, + InstallTime = item?.Updated > item?.Created ? item?.Updated : item?.Created + }; + + foreach (ContentFile contentFile in newPackage.Files) + { + contentFile.Path = CorrectContentFilePath(contentFile.Path, contentFile.Type, cp, true); } + + foreach (ContentFile file in existingPackages.SelectMany(p => p.Files)) + { + string path = CorrectContentFilePath(file.Path, file.Type, cp, true).CleanUpPath(); + if (newPackage.Files.Any(f => f.Path.CleanUpPath() == path)) { continue; } + newPackage.AddFile(path, file.Type); + } + + if (!Directory.Exists(Path.GetDirectoryName(newContentPackagePath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); + } + newPackage.Save(newContentPackagePath); + ContentPackage.List.Add(newPackage); + + if (selectContentPackage) + { + if (newPackage.CorePackage) + { + GameMain.Config.SelectCorePackage(newPackage); + } + else + { + GameMain.Config.SelectContentPackage(newPackage); + } + GameMain.Config.SaveNewPlayerConfig(); + + GameMain.Config.WarnIfContentPackageSelectionDirty(); + + if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) + { + SubmarineInfo.RefreshSavedSubs(); + } + } + else if (!suppressInstallNotif) + { + GameMain.MainMenuScreen?.SetEnableModsNotification(true); + } + + GameMain.Config.SuppressModFolderWatcher = false; + + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Green); + } - else if (!suppressInstallNotif) + catch { - GameMain.MainMenuScreen?.SetEnableModsNotification(true); + throw; + } + finally + { + modCopiesInProgress.Remove(item.Value.Id); } - - GameMain.Config.SuppressModFolderWatcher = false; - - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Green); - }); errorMsg = ""; @@ -1039,7 +1056,7 @@ namespace Barotrauma.Steam /// Asynchronously copies a Workshop item into the Mods folder. /// /// Returns an empty string on success, otherwise returns an error message. - private async static Task CopyWorkShopItemAsync(Steamworks.Ugc.Item? item, ContentPackage contentPackage, string newContentPackagePath, string metaDataFilePath, bool allowFileOverwrite) + private async static Task CopyWorkShopItemAsync(Steamworks.Ugc.Item? item, ContentPackage contentPackage, string newContentPackagePath, string metaDataFilePath) { await Task.Yield(); @@ -1052,13 +1069,13 @@ namespace Barotrauma.Steam Directory.CreateDirectory(targetPath); File.WriteAllText(copyingPath, "TEMPORARY FILE"); - SaveUtil.CopyFolder(item?.Directory, targetPath, copySubDirs: true, overwriteExisting: item?.Owner.Id != Steamworks.SteamClient.SteamId); + SaveUtil.CopyFolder(item?.Directory, targetPath, copySubDirs: true, overwriteExisting: false); File.Delete(copyingPath); return ""; } - var allPackageFiles = Directory.GetFiles(item?.Directory, "*", SearchOption.AllDirectories); + var allPackageFiles = Directory.GetFiles(item?.Directory, "*", System.IO.SearchOption.AllDirectories); List nonContentFiles = new List(); foreach (string file in allPackageFiles) { @@ -1069,27 +1086,24 @@ namespace Barotrauma.Steam nonContentFiles.Add(relativePath); } - if (!allowFileOverwrite) + /*if (File.Exists(newContentPackagePath) && !CheckFileEquality(newContentPackagePath, metaDataFilePath)) { - if (File.Exists(newContentPackagePath) && !CheckFileEquality(newContentPackagePath, metaDataFilePath)) + errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, newContentPackagePath }); + DebugConsole.NewMessage(errorMsg, Color.Red); + return errorMsg; + } + + foreach (ContentFile contentFile in contentPackage.Files) + { + string sourceFile = Path.Combine(item?.Directory, contentFile.Path); + + if (File.Exists(sourceFile) && File.Exists(contentFile.Path) && !CheckFileEquality(sourceFile, contentFile.Path)) { - errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, newContentPackagePath }); + errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, contentFile.Path }); DebugConsole.NewMessage(errorMsg, Color.Red); return errorMsg; } - - foreach (ContentFile contentFile in contentPackage.Files) - { - string sourceFile = Path.Combine(item?.Directory, contentFile.Path); - - if (File.Exists(sourceFile) && File.Exists(contentFile.Path) && !CheckFileEquality(sourceFile, contentFile.Path)) - { - errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, contentFile.Path }); - DebugConsole.NewMessage(errorMsg, Color.Red); - return errorMsg; - } - } - } + }*/ Directory.CreateDirectory(targetPath); File.WriteAllText(copyingPath, "TEMPORARY FILE"); @@ -1107,7 +1121,7 @@ namespace Barotrauma.Steam } } - contentFile.Path = CorrectContentFilePath(contentFile.Path, contentPackage, + contentFile.Path = CorrectContentFilePath(contentFile.Path, contentFile.Type, contentPackage, contentFile.Type != ContentType.Submarine); //path not allowed -> the content file must be a reference to an external file (such as some vanilla file outside the Mods folder) @@ -1145,16 +1159,16 @@ namespace Barotrauma.Steam //make sure the destination directory exists Directory.CreateDirectory(Path.GetDirectoryName(contentFile.Path)); - CorrectContentFileCopy(contentPackage, sourceFile, contentFile.Path, overwrite: item?.Owner.Id != Steamworks.SteamClient.SteamId); + CorrectContentFileCopy(contentPackage, sourceFile, contentFile.Path, overwrite: false); } foreach (string nonContentFile in nonContentFiles) { string sourceFile = Path.Combine(item?.Directory, nonContentFile); if (!File.Exists(sourceFile)) { continue; } - string destinationPath = CorrectContentFilePath(nonContentFile, contentPackage, false); + string destinationPath = CorrectContentFilePath(nonContentFile, ContentType.None, contentPackage, false); Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - CorrectContentFileCopy(contentPackage, sourceFile, destinationPath, overwrite: item?.Owner.Id != Steamworks.SteamClient.SteamId); + CorrectContentFileCopy(contentPackage, sourceFile, destinationPath, overwrite: false); } File.Delete(copyingPath); @@ -1271,7 +1285,7 @@ namespace Barotrauma.Steam string metaDataPath = Path.Combine(item?.Directory, MetadataFileName); if (!File.Exists(metaDataPath)) { - throw new FileNotFoundException("Metadata file for the Workshop item \"" + item?.Title + "\" not found. The file may be corrupted."); + throw new System.IO.FileNotFoundException("Metadata file for the Workshop item \"" + item?.Title + "\" not found. The file may be corrupted."); } ContentPackage contentPackage = new ContentPackage(metaDataPath); @@ -1294,7 +1308,7 @@ namespace Barotrauma.Steam { metaDataPath = Path.Combine(item?.Directory, MetadataFileName); } - catch (ArgumentException e) + catch (ArgumentException) { string errorMessage = "Metadata file for the Workshop item \"" + item?.Title + "\" not found. Could not combine path (" + (item?.Directory ?? "directory name empty") + ")."; @@ -1354,6 +1368,8 @@ namespace Barotrauma.Steam public static async Task AutoUpdateWorkshopItemsAsync() { + await Task.Yield(); + if (!isInitialized) { return false; } var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) @@ -1381,7 +1397,7 @@ namespace Barotrauma.Steam string errorMsg; if (!CheckWorkshopItemEnabled(item)) { - installedSuccessfully = EnableWorkShopItem(item, true, out errorMsg); + installedSuccessfully = EnableWorkShopItem(item, out errorMsg); } else if (!CheckWorkshopItemUpToDate(item)) { @@ -1442,10 +1458,12 @@ namespace Barotrauma.Steam { while (updateNotifications.Count > 0) { + float width = updateNotifications.Max(notif => GUI.Font.MeasureString(notif).X) * 1.25f; + int notificationsPerMsgBox = 20; new GUIMessageBox("", string.Join('\n', updateNotifications.Take(notificationsPerMsgBox)), - relativeSize: new Microsoft.Xna.Framework.Vector2(0.5f, 0.0f), - minSize: new Microsoft.Xna.Framework.Point(600, 0)); + relativeSize: new Microsoft.Xna.Framework.Vector2(0.25f, 0.0f), + minSize: new Microsoft.Xna.Framework.Point((int)width, 0)); updateNotifications.RemoveRange(0, Math.Min(notificationsPerMsgBox, updateNotifications.Count)); } }); @@ -1465,12 +1483,12 @@ namespace Barotrauma.Steam { errorMsg = ""; if (!(item?.IsInstalled ?? false)) { return false; } + bool reenable = GameMain.Config.SelectedContentPackages.Any(p => !string.IsNullOrEmpty(p.SteamWorkshopUrl) && GetWorkshopItemIDFromUrl(p.SteamWorkshopUrl) == item?.Id); if (item?.Owner.Id != Steamworks.SteamClient.SteamId) { if (!DisableWorkShopItem(item, false, out errorMsg)) { return false; } } - if (!EnableWorkShopItem(item, allowFileOverwrite: false, errorMsg: out errorMsg)) { return false; } - + if (!EnableWorkShopItem(item, errorMsg: out errorMsg, selectContentPackage: reenable)) { return false; } return true; } @@ -1495,7 +1513,9 @@ namespace Barotrauma.Steam attr.Name.ToString() == "characterfile") && attr.Value.CleanUpPath().Contains("/")) { - attr.Value = CorrectContentFilePath(attr.Value, package, true); + ContentType type = ContentType.None; + Enum.TryParse(attr.Name.LocalName, true, out type); + attr.Value = CorrectContentFilePath(attr.Value, type, package, true); } } @@ -1515,12 +1535,12 @@ namespace Barotrauma.Steam if (doc != null) { CorrectXMLFilePaths(package, doc.Root); - using (MemoryStream stream = new MemoryStream()) + using (System.IO.MemoryStream stream = new System.IO.MemoryStream()) { - XmlWriterSettings settings = new XmlWriterSettings(); + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings(); settings.Indent = true; settings.Encoding = new System.Text.UTF8Encoding(false); - using (var xmlWriter = XmlWriter.Create(stream, settings)) + using (var xmlWriter = System.Xml.XmlWriter.Create(stream, settings)) { doc.WriteTo(xmlWriter); xmlWriter.Flush(); @@ -1540,15 +1560,24 @@ namespace Barotrauma.Steam } } - private static string CorrectContentFilePath(string contentFilePath, ContentPackage package, bool checkIfFileExists = false) + private static string CorrectContentFilePath(string contentFilePath, ContentType type, ContentPackage package, bool checkIfFileExists = false) { string packageName = Path.GetDirectoryName(GetWorkshopItemContentPackagePath(package)); contentFilePath = contentFilePath.CleanUpPathCrossPlatform(); - if (checkIfFileExists && File.Exists(contentFilePath)) + if (checkIfFileExists) { - return contentFilePath; + bool exists = File.Exists(contentFilePath); + if (type == ContentType.Executable || + type == ContentType.ServerExecutable) + { + exists |= File.Exists(contentFilePath + ".dll"); + } + if (exists) + { + return contentFilePath; + } } string[] splitPath = contentFilePath.Split('/'); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index a241aaf76..c73bde30c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -275,14 +275,6 @@ namespace Barotrauma.Networking } } - public override void Write(IWriteMessage msg) - { - lock (buffers) - { - base.Write(msg); - } - } - public override void Dispose() { Instance = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index f2ee5ffa5..c1f64c6cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -10,6 +10,8 @@ namespace Barotrauma.Particles { private ParticlePrefab prefab; + private string debugName = "Particle (uninitialized)"; + public delegate void OnChangeHullHandler(Vector2 position, Hull currentHull); public OnChangeHullHandler OnChangeHull; @@ -92,10 +94,16 @@ namespace Barotrauma.Particles { get { return prefab; } } - + + public override string ToString() + { + return debugName; + } + public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false) { this.prefab = prefab; + debugName = $"Particle ({prefab.Name})"; spriteIndex = Rand.Int(prefab.Sprites.Count); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 6a140118c..ab81a8534 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -89,7 +89,21 @@ namespace Barotrauma.Particles { public readonly string Name; - public readonly ParticlePrefab ParticlePrefab; + private string particlePrefabName; + + private ParticlePrefab particlePrefab; + public ParticlePrefab ParticlePrefab + { + get + { + if (particlePrefab == null && particlePrefabName != null) + { + particlePrefab = GameMain.ParticleManager?.FindPrefab(particlePrefabName); + if (particlePrefab == null) { particlePrefabName = null; } + } + return particlePrefab; + } + } public readonly float AngleMin, AngleMax; @@ -114,8 +128,7 @@ namespace Barotrauma.Particles public ParticleEmitterPrefab(XElement element) { Name = element.Name.ToString(); - - ParticlePrefab = GameMain.ParticleManager.FindPrefab(element.GetAttributeString("particle", "")); + particlePrefabName = element.GetAttributeString("particle", ""); if (element.Attribute("startrotation") == null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 4666696e9..68e4cc78c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -120,13 +120,13 @@ namespace Barotrauma.Particles return CreateParticle(prefabName, position, new Vector2((float)Math.Cos(angle), (float)-Math.Sin(angle)) * speed, angle, hullGuess); } - public Particle CreateParticle(string prefabName, Vector2 position, Vector2 velocity, float rotation=0.0f, Hull hullGuess = null) + public Particle CreateParticle(string prefabName, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null) { ParticlePrefab prefab = FindPrefab(prefabName); if (prefab == null) { - DebugConsole.ThrowError("Particle prefab \"" + prefabName+"\" not found!"); + DebugConsole.ThrowError("Particle prefab \"" + prefabName + "\" not found!"); return null; } @@ -135,7 +135,7 @@ namespace Barotrauma.Particles public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false) { - if (particleCount >= MaxParticles || prefab == null) return null; + if (particleCount >= MaxParticles || prefab == null || prefab.Sprites.Count == 0) { return null; } Vector2 particleEndPos = prefab.CalculateEndPosition(position, velocity); @@ -144,8 +144,8 @@ namespace Barotrauma.Particles Rectangle expandedViewRect = MathUtils.ExpandRect(cam.WorldView, MaxOutOfViewDist); - if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) return null; - if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) return null; + if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } + if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } if (particles[particleCount] == null) particles[particleCount] = new Particle(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 9a0cb6a6c..e1a6b2b06 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -255,6 +255,11 @@ namespace Barotrauma.Particles } } + if (Sprites.Count == 0) + { + DebugConsole.ThrowError($"Particle prefab \"{Name}\" in the file \"{file}\" has no sprites defined!"); + } + //if velocity change in water is not given, it defaults to the normal velocity change if (element.Attribute("velocitychangewater") == null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index c362ef365..9d444cd03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -17,7 +17,7 @@ namespace Barotrauma get { return bodyShapeTexture; } } - public void Draw(DeformableSprite deformSprite, Camera cam, Vector2 scale, Color color, bool mirror = false) + public void Draw(DeformableSprite deformSprite, Camera cam, Vector2 scale, Color color, bool invert = false) { if (!Enabled) return; UpdateDrawPosition(); @@ -25,7 +25,7 @@ namespace Barotrauma new Vector3(DrawPosition, MathHelper.Clamp(deformSprite.Sprite.Depth, 0, 1)), deformSprite.Origin, -DrawRotation, - scale, color, Dir < 0, mirror); + scale, color, Dir < 0, invert); } public void Draw(SpriteBatch spriteBatch, Sprite sprite, Color color, float? depth = null, float scale = 1.0f, bool mirrorX = false, bool mirrorY = false) diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index 4957b69c5..f4a86f565 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -25,6 +25,18 @@ namespace Barotrauma public class KeyOrMouse { public Keys Key { get; private set; } + + private string name; + + public string Name + { + get + { + if (name == null) { name = GetName(); } + return name; + } + } + public MouseButton MouseButton { get; private set; } public KeyOrMouse(Keys keyBinding) @@ -133,6 +145,30 @@ namespace Barotrauma hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode((int)MouseButton); return hashCode; } + + public string GetName() + { + if (PlayerInput.NumberKeys.Contains(Key)) + { + return Key.ToString().Substring(1, 1); + } + if (MouseButton != MouseButton.None) + { + switch (MouseButton) + { + case MouseButton.PrimaryMouse: + return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse"); + case MouseButton.SecondaryMouse: + return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse"); + default: + return TextManager.Get("input." + MouseButton.ToString().ToLowerInvariant()); + } + } + else + { + return Key.ToString(); + } + } } public static class PlayerInput @@ -155,6 +191,8 @@ namespace Barotrauma static bool allowInput; static bool wasWindowActive; + public static readonly List NumberKeys = new List { Keys.D0, Keys.D1, Keys.D2, Keys.D3, Keys.D4, Keys.D5, Keys.D6, Keys.D7, Keys.D8, Keys.D9 }; + #if WINDOWS [DllImport("user32.dll")] static extern int GetSystemMetrics(int smIndex); @@ -408,6 +446,12 @@ namespace Barotrauma return (AllowInput && oldKeyboardState.IsKeyDown(button) && keyboardState.IsKeyUp(button)); } + public static bool InventoryKeyHit(int index) + { + if (index == -1) return false; + return AllowInput && GameMain.Config.InventoryKeyBind(index).IsHit(); + } + public static bool KeyDown(Keys button) { return (AllowInput && keyboardState.IsKeyDown(button)); @@ -425,7 +469,11 @@ namespace Barotrauma public static bool IsCtrlDown() { +#if !OSX return KeyDown(Keys.LeftControl) || KeyDown(Keys.RightControl); +#else + return KeyDown(Keys.LeftWindows) || KeyDown(Keys.RightWindows); +#endif } public static void Update(double deltaTime) @@ -462,8 +510,9 @@ namespace Barotrauma doubleClicked = false; if (PrimaryMouseButtonClicked()) { - if (timeSinceClick < DoubleClickDelay && - (mouseState.Position - lastClickPosition).ToVector2().Length() < MaxDoubleClickDistance) + float dist = (mouseState.Position - lastClickPosition).ToVector2().Length(); + + if (timeSinceClick < DoubleClickDelay && dist < MaxDoubleClickDistance) { doubleClicked = true; timeSinceClick = DoubleClickDelay; @@ -472,16 +521,15 @@ namespace Barotrauma { lastClickPosition = mouseState.Position; } - - timeSinceClick = 0.0; + if (!doubleClicked && dist < MaxDoubleClickDistance) + { + timeSinceClick = 0.0; + } } if (PrimaryMouseButtonDown()) { - if (timeSinceClick > DoubleClickDelay) - { - lastClickPosition = mouseState.Position; - } + lastClickPosition = mouseState.Position; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 686787aaf..e928d9dc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -1,7 +1,7 @@ #region Using Statements using System; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using GameAnalyticsSDK.Net; @@ -104,8 +104,6 @@ namespace Barotrauma exeHash = new Md5Hash(stream); } - StreamWriter sw = new StreamWriter(filePath); - StringBuilder sb = new StringBuilder(); sb.AppendLine("Barotrauma Client crash report (generated on " + DateTime.Now + ")"); sb.AppendLine("\n"); @@ -235,8 +233,7 @@ namespace Barotrauma string crashReport = sb.ToString(); - sw.WriteLine(crashReport); - sw.Close(); + File.WriteAllText(filePath, crashReport); if (GameSettings.SaveDebugConsoleLogs) DebugConsole.SaveLogs(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs index 37e835f56..776304554 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 67b360ed4..b2af0381d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -2,13 +2,17 @@ using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Graphics; using System; -using System.IO; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; using FarseerPhysics; using FarseerPhysics.Dynamics; +#if DEBUG +using System.IO; +#else +using Barotrauma.IO; +#endif namespace Barotrauma.CharacterEditor { @@ -278,7 +282,7 @@ namespace Barotrauma.CharacterEditor return TextManager.Get(screenTextTag + tag); } - #region Main methods +#region Main methods public override void AddToGUIUpdateList() { rightArea.AddToGUIUpdateList(); @@ -662,9 +666,6 @@ namespace Barotrauma.CharacterEditor } if (!isFrozen) { - Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); - Submarine.MainSub.Update((float)deltaTime); - foreach (PhysicsBody body in PhysicsBody.List) { body.SetPrevTransform(body.SimPosition, body.Rotation); @@ -991,9 +992,9 @@ namespace Barotrauma.CharacterEditor } spriteBatch.End(); } - #endregion +#endregion - #region Ragdoll Manipulation +#region Ragdoll Manipulation private void UpdateJointCreation() { if (jointCreationMode == JointCreationMode.None) @@ -1320,9 +1321,9 @@ namespace Barotrauma.CharacterEditor } RecreateRagdoll(); } - #endregion +#endregion - #region Endless runner +#region Endless runner private int min; private int max; private void CalculateMovementLimits() @@ -1425,9 +1426,9 @@ namespace Barotrauma.CharacterEditor AllWalls.ForEach(w => w.SetCollisionCategory(collisionCategory)); GameMain.World.ProcessChanges(); } - #endregion +#endregion - #region Character spawning +#region Character spawning private int characterIndex = -1; private string currentCharacterConfig; private string selectedJob = null; @@ -1749,7 +1750,11 @@ namespace Barotrauma.CharacterEditor { Directory.CreateDirectory(mainFolder); } +#if DEBUG doc.Save(configFilePath); +#else + doc.SaveSafe(configFilePath); +#endif // Add to the selected content package contentPackage.AddFile(configFilePath, ContentType.Character); contentPackage.Save(contentPackage.Path); @@ -1830,9 +1835,9 @@ namespace Barotrauma.CharacterEditor { character.Inventory?.Items.ForEachMod(i => i?.Unequip(character)); } - #endregion +#endregion - #region GUI +#region GUI private static Vector2 innerScale = new Vector2(0.95f, 0.95f); private GUILayoutGroup rightArea, leftArea; @@ -3147,9 +3152,9 @@ namespace Barotrauma.CharacterEditor fileEditPanel.RectTransform.MinSize = new Point(0, (int)(layoutGroup.RectTransform.Children.Sum(c => c.MinSize.Y + layoutGroup.AbsoluteSpacing) * 1.2f)); } - #endregion +#endregion - #region ToggleButtons +#region ToggleButtons private enum Direction { Left, @@ -3211,9 +3216,9 @@ namespace Barotrauma.CharacterEditor } } - #endregion +#endregion - #region Params +#region Params private CharacterParams CharacterParams => character.Params; private List AnimParams => character.AnimController.AllAnimParams; private AnimationParams CurrentAnimation => character.AnimController.CurrentAnimationParams; @@ -3464,9 +3469,9 @@ namespace Barotrauma.CharacterEditor } } } - #endregion +#endregion - #region Helpers +#region Helpers private Vector2 ScreenToSim(float x, float y) => ScreenToSim(new Vector2(x, y)); private Vector2 ScreenToSim(Vector2 p) => ConvertUnits.ToSimUnits(Cam.ScreenToWorld(p)) + Submarine.MainSub.SimPosition; private Vector2 SimToScreen(float x, float y) => SimToScreen(new Vector2(x, y)); @@ -3707,9 +3712,9 @@ namespace Barotrauma.CharacterEditor SetToggle(spritesheetToggle, true); } } - #endregion +#endregion - #region Animation Controls +#region Animation Controls private void DrawAnimationControls(SpriteBatch spriteBatch, float deltaTime) { var collider = character.AnimController.Collider; @@ -4302,9 +4307,9 @@ namespace Barotrauma.CharacterEditor } } } - #endregion +#endregion - #region Ragdoll +#region Ragdoll private Vector2[] corners = new Vector2[4]; private Vector2[] GetLimbPhysicRect(Limb limb) { @@ -4626,9 +4631,9 @@ namespace Barotrauma.CharacterEditor } return otherLimbs; } - #endregion +#endregion - #region Spritesheet +#region Spritesheet private List textures; private List Textures { @@ -5223,9 +5228,9 @@ namespace Barotrauma.CharacterEditor CalculateSpritesheetZoom(); spriteSheetZoomBar.BarScroll = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(spriteSheetMinZoom, spriteSheetMaxZoom, spriteSheetZoom)); } - #endregion +#endregion - #region Widgets as methods +#region Widgets as methods private void DrawRadialWidget(SpriteBatch spriteBatch, Vector2 drawPos, float value, string toolTip, Color color, Action onClick, float circleRadius = 30, int widgetSize = 10, float rotationOffset = 0, bool clockWise = true, bool displayAngle = true, bool? autoFreeze = null, bool wrapAnglePi = false, bool holdPosition = false, int rounding = 1) { @@ -5334,9 +5339,9 @@ namespace Barotrauma.CharacterEditor } } } - #endregion +#endregion - #region Widgets as classes +#region Widgets as classes private Dictionary animationWidgets = new Dictionary(); private Dictionary jointSelectionWidgets = new Dictionary(); private Dictionary limbEditWidgets = new Dictionary(); @@ -5490,6 +5495,6 @@ namespace Barotrauma.CharacterEditor return w; } } - #endregion +#endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 8f6eb15ce..cdb039970 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -1,6 +1,6 @@ using Microsoft.Xna.Framework; using System; -using System.IO; +using Barotrauma.IO; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index c35d3caae..d3c8f91f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -151,7 +151,10 @@ namespace Barotrauma GameMain.ParticleManager.UpdateTransforms(); - GameMain.LightManager.ObstructVision = Character.Controlled != null && Character.Controlled.ObstructVision; + GameMain.LightManager.ObstructVision = + Character.Controlled != null && + Character.Controlled.ObstructVision && + (Character.Controlled.ViewTarget == Character.Controlled || Character.Controlled.ViewTarget == null); if (Character.Controlled != null) { @@ -261,7 +264,7 @@ namespace Barotrauma graphics.SetRenderTarget(renderTargetFinal); WaterRenderer.Instance.ResetBuffers(); - Hull.UpdateVertices(graphics, cam, WaterRenderer.Instance); + Hull.UpdateVertices(cam, WaterRenderer.Instance); WaterRenderer.Instance.RenderWater(spriteBatch, renderTargetWater, cam); WaterRenderer.Instance.RenderAir(graphics, cam, renderTarget, Cam.ShaderTransform); graphics.DepthStencilState = DepthStencilState.None; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index a56ac3f40..8dcd69bc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -3,10 +3,14 @@ using Barotrauma.RuinGeneration; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.IO; using System.Linq; -using System.Xml; using System.Xml.Linq; +#if DEBUG +using System.IO; +using System.Xml; +#else +using Barotrauma.IO; +#endif namespace Barotrauma { @@ -459,7 +463,7 @@ namespace Barotrauma Submarine.Draw(spriteBatch, false); Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); - GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.White, thickness: (int)(1.0f / cam.Zoom)); + GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.Gray, thickness: (int)(1.0f / cam.Zoom)); spriteBatch.End(); if (lightingEnabled.Selected) @@ -497,7 +501,7 @@ namespace Barotrauma private void SerializeAll() { - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true @@ -578,7 +582,7 @@ namespace Barotrauma if (elementFound) { - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true @@ -595,7 +599,7 @@ namespace Barotrauma } - #region LevelObject Wizard +#region LevelObject Wizard private class Wizard { private LevelObjectPrefab newPrefab; @@ -676,8 +680,8 @@ namespace Barotrauma } newPrefab.Name = nameBox.Text; - - XmlWriterSettings settings = new XmlWriterSettings { Indent = true }; + + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true }; foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs)) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); @@ -710,6 +714,6 @@ namespace Barotrauma } } - #endregion +#endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index fdea888d7..bccff1f52 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -10,7 +10,7 @@ using RestSharp; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Net; using System.Threading; @@ -409,10 +409,9 @@ namespace Barotrauma this.game = game; - menuTabs[(int)Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null, color: Color.Black * 0.5f) - { - CanBeFocused = false - }; + menuTabs[(int)Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, menuTabs[(int)Tab.Credits].RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[(int)Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); @@ -1010,11 +1009,12 @@ namespace Barotrauma GUI.Draw(Cam, spriteBatch); #if !UNSTABLE - GUI.Font.DrawString(spriteBatch, "Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")", new Vector2(10, GameMain.GraphicsHeight - 20), Color.White * 0.7f); + string versionString = "Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"; + GUI.SmallFont.DrawString(spriteBatch, versionString, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUI.SmallFont.MeasureString(versionString).Y - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); #endif if (selectedTab != Tab.Credits) { - Vector2 textPos = new Vector2(GameMain.GraphicsWidth - 10, GameMain.GraphicsHeight - 10); + Vector2 textPos = new Vector2(GameMain.GraphicsWidth - HUDLayoutSettings.Padding, GameMain.GraphicsHeight - HUDLayoutSettings.Padding * 0.75f); for (int i = legalCrap.Length - 1; i >= 0; i--) { Vector2 textSize = GUI.SmallFont.MeasureString(legalCrap[i]); @@ -1069,7 +1069,7 @@ namespace Barotrauma { File.Copy(selectedSub.FilePath, Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub"), true); } - catch (IOException e) + catch (System.IO.IOException e) { DebugConsole.ThrowError("Copying the file \"" + selectedSub.FilePath + "\" failed. The file may have been deleted or in use by another process. Try again or select another submarine.", e); GameAnalyticsManager.AddErrorEventOnce( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index d184e3ae9..480744dd2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -668,7 +668,7 @@ namespace Barotrauma OnClicked = (btn, obj) => { GameMain.Client.RequestStartRound(); - CoroutineManager.StartCoroutine(WaitForStartRound(StartButton, allowCancel: false), "WaitForStartRound"); + CoroutineManager.StartCoroutine(WaitForStartRound(StartButton), "WaitForStartRound"); return true; } }; @@ -786,6 +786,26 @@ namespace Barotrauma }; var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Submarine"), font: GUI.SubHeadingFont); + + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subHolder.RectTransform), isHorizontal: true) + { + Stretch = true + }; + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnTextChanged += (textBox, text) => + { + foreach (GUIComponent child in subList.Content.Children) + { + if (!(child.UserData is SubmarineInfo sub)) { continue; } + child.Visible = string.IsNullOrEmpty(text) ? true : sub.DisplayName.ToLower().Contains(text.ToLower()); + } + return true; + }; + subList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)) { OnSelected = VotableClicked @@ -1173,25 +1193,11 @@ namespace Barotrauma GUI.ClearCursorWait(); } - public IEnumerable WaitForStartRound(GUIButton startButton, bool allowCancel) + public IEnumerable WaitForStartRound(GUIButton startButton) { GUI.SetCursorWaiting(); string headerText = TextManager.Get("RoundStartingPleaseWait"); - var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), - allowCancel ? new string[] { TextManager.Get("Cancel") } : new string[0]); - - if (allowCancel) - { - msgBox.Buttons[0].OnClicked = (btn, userdata) => - { - startButton.Enabled = true; - GameMain.Client.RequestRoundEnd(); - CoroutineManager.StopCoroutines("WaitForStartRound"); - GUI.ClearCursorWait(); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - } + var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), new string[0]); if (startButton != null) { @@ -1939,13 +1945,15 @@ namespace Barotrauma public bool SelectPlayer(Client selectedClient) { bool myClient = selectedClient.ID == GameMain.Client.ID; + bool hasManagePermissions = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions); - PlayerFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + PlayerFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) ClosePlayerFrame(btn, userdata); return true; } }; - Vector2 frameSize = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions) ? new Vector2(.28f, .5f) : new Vector2(.28f, .24f); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, PlayerFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + Vector2 frameSize = hasManagePermissions ? new Vector2(.28f, .5f) : new Vector2(.28f, .15f); var playerFrameInner = new GUIFrame(new RectTransform(frameSize, PlayerFrame.RectTransform, Anchor.Center) { MinSize = new Point(550, 0) }); var paddedPlayerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.88f), playerFrameInner.RectTransform, Anchor.Center)) @@ -1954,31 +1962,16 @@ namespace Barotrauma RelativeSpacing = 0.03f }; - var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, hasManagePermissions ? 0.1f : 0.25f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - var nameText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), headerContainer.RectTransform), + var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerContainer.RectTransform), text: selectedClient.Name, font: GUI.LargeFont); - nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width); + nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, (int)(nameText.Rect.Width * 0.95f)); - if (selectedClient.SteamID != 0 && Steam.SteamManager.IsInitialized) - { - var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), headerContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) }, - TextManager.Get("ViewSteamProfile")) - { - UserData = selectedClient - }; - viewSteamProfileButton.TextBlock.AutoScaleHorizontal = true; - viewSteamProfileButton.OnClicked = (bt, userdata) => - { - Steamworks.SteamFriends.OpenWebOverlay("https://steamcommunity.com/profiles/" + selectedClient.SteamID.ToString()); - return true; - }; - } - - if (GameMain.Client.HasPermission(ClientPermissions.ManagePermissions)) + if (hasManagePermissions) { PlayerFrame.UserData = selectedClient; @@ -2166,7 +2159,7 @@ namespace Barotrauma } var buttonAreaTop = myClient ? null : new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true); - var buttonAreaLower = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true); + var buttonAreaLower = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); if (!myClient) { @@ -2214,32 +2207,66 @@ namespace Barotrauma kickButton.OnClicked += ClosePlayerFrame; } - GUITextBlock.AutoScaleAndNormalize( - buttonAreaTop.Children.Select(c => ((GUIButton)c).TextBlock).Concat(buttonAreaLower.Children.Select(c => ((GUIButton)c).TextBlock))); + if (buttonAreaTop.CountChildren > 0) + { + GUITextBlock.AutoScaleAndNormalize(buttonAreaTop.Children.Select(c => ((GUIButton)c).TextBlock).Concat(buttonAreaLower.Children.Select(c => ((GUIButton)c).TextBlock))); + } - new GUITickBox(new RectTransform(new Vector2(0.25f, 1.0f), buttonAreaTop.RectTransform, Anchor.TopRight), + new GUITickBox(new RectTransform(new Vector2(0.175f, 1.0f), headerContainer.RectTransform, Anchor.TopRight), TextManager.Get("Mute")) { - IgnoreLayoutGroups = true, Selected = selectedClient.MutedLocally, OnSelected = (tickBox) => { selectedClient.MutedLocally = tickBox.Selected; return true; } }; } - var closeButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaLower.RectTransform, Anchor.TopRight), + if (selectedClient.SteamID != 0 && Steam.SteamManager.IsInitialized) + { + var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), headerContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) }, + TextManager.Get("ViewSteamProfile")) + { + UserData = selectedClient + }; + viewSteamProfileButton.TextBlock.AutoScaleHorizontal = true; + viewSteamProfileButton.OnClicked = (bt, userdata) => + { + Steamworks.SteamFriends.OpenWebOverlay("https://steamcommunity.com/profiles/" + selectedClient.SteamID.ToString()); + return true; + }; + } + + var closeButton = new GUIButton(new RectTransform(new Vector2(0f, 1.0f), buttonAreaLower.RectTransform, Anchor.CenterRight), TextManager.Get("Close")) { IgnoreLayoutGroups = true, OnClicked = ClosePlayerFrame }; + float xSize = 1f / buttonAreaLower.CountChildren; + for (int i = 0; i < buttonAreaLower.CountChildren; i++) + { + buttonAreaLower.GetChild(i).RectTransform.RelativeSize = new Vector2(xSize, 1f); + } + buttonAreaLower.RectTransform.NonScaledSize = new Point(buttonAreaLower.Rect.Width, buttonAreaLower.RectTransform.Children.Max(c => c.NonScaledSize.Y)); if (buttonAreaTop != null) { - buttonAreaTop.RectTransform.NonScaledSize = - buttonAreaLower.RectTransform.NonScaledSize = - new Point(buttonAreaLower.Rect.Width, Math.Max(buttonAreaLower.RectTransform.NonScaledSize.Y, buttonAreaTop.RectTransform.Children.Max(c => c.NonScaledSize.Y))); + if (buttonAreaTop.CountChildren == 0) + { + paddedPlayerFrame.RemoveChild(buttonAreaTop); + } + else + { + for (int i = 0; i < buttonAreaTop.CountChildren; i++) + { + buttonAreaTop.GetChild(i).RectTransform.RelativeSize = new Vector2(1f / 3f, 1f); + } + + buttonAreaTop.RectTransform.NonScaledSize = + buttonAreaLower.RectTransform.NonScaledSize = + new Point(buttonAreaLower.Rect.Width, Math.Max(buttonAreaLower.RectTransform.NonScaledSize.Y, buttonAreaTop.RectTransform.Children.Max(c => c.NonScaledSize.Y))); + } } return false; @@ -3086,7 +3113,7 @@ namespace Barotrauma StartRound = () => { GameMain.Client.RequestStartRound(); - CoroutineManager.StartCoroutine(WaitForStartRound(campaignUI.StartButton, allowCancel: true), "WaitForStartRound"); + CoroutineManager.StartCoroutine(WaitForStartRound(campaignUI.StartButton), "WaitForStartRound"); } }; @@ -3145,8 +3172,8 @@ namespace Barotrauma { if (!(button.UserData is Pair jobPrefab)) { return false; } - JobInfoFrame = jobPrefab.First.CreateInfoFrame(jobPrefab.Second); - GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), JobInfoFrame.GetChild(2).GetChild(0).RectTransform, Anchor.BottomRight), + JobInfoFrame = jobPrefab.First.CreateInfoFrame(out GUIComponent buttonContainer); + GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Close")) { OnClicked = CloseJobInfo @@ -3304,7 +3331,8 @@ namespace Barotrauma { CreateSubPreview(sub); } - if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && System.IO.File.Exists(sub.FilePath)) + + if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && Barotrauma.IO.File.Exists(sub.FilePath)) { return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 08d8d6193..5d7eba886 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -4,9 +4,14 @@ using Barotrauma.Particles; using System; using System.Collections.Generic; using System.Xml.Linq; -using System.Xml; using System.Text; using Barotrauma.Extensions; +#if DEBUG +using System.IO; +using System.Xml; +#else +using Barotrauma.IO; +#endif namespace Barotrauma { @@ -242,7 +247,7 @@ namespace Barotrauma } } - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, OmitXmlDeclaration = true, @@ -262,7 +267,7 @@ namespace Barotrauma #if WINDOWS if (prefab == null) { return; } - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, OmitXmlDeclaration = true, @@ -295,7 +300,7 @@ namespace Barotrauma } StringBuilder sb = new StringBuilder(); - using (var writer = XmlWriter.Create(sb, settings)) + using (var writer = System.Xml.XmlWriter.Create(sb, settings)) { originalElement.WriteTo(writer); writer.Flush(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs index 4b0fcf608..08266e537 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs @@ -14,10 +14,11 @@ namespace Barotrauma { if (frame == null) { - frame = new GUIFrame(new RectTransform(Vector2.One, GUICanvas.Instance), style: null) + frame = new GUIFrame(new RectTransform(GUICanvas.Instance.RelativeSize, GUICanvas.Instance), style: null) { CanBeFocused = false }; + } return frame; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 9f0ab6a38..4f0ab96eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -6,7 +6,7 @@ using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; @@ -683,7 +683,21 @@ namespace Barotrauma if (!File.Exists(file)) { return; } XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc == null) { return; } + if (doc == null) + { + DebugConsole.NewMessage("Failed to load file \"" + file + "\". Attempting to recreate the file..."); + try + { + doc = new XDocument(new XElement("servers")); + doc.Save(file); + DebugConsole.NewMessage("Recreated \"" + file + "\"."); + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to recreate the file \"" + file + "\".", e); + } + return; + } foreach (XElement element in doc.Root.Elements()) { @@ -705,7 +719,7 @@ namespace Barotrauma rootElement.Add(info.ToXElement()); } - doc.Save(file); + doc.SaveSafe(file); } public ServerInfo UpdateServerInfoWithServerSettings(object endpoint, ServerSettings serverSettings) @@ -1670,7 +1684,7 @@ namespace Barotrauma { CanBeFocused = false, Selected = - serverInfo.GameVersion == GameMain.Version.ToString() && + (NetworkMember.IsCompatible(GameMain.Version.ToString(), serverInfo.GameVersion) ?? true) && serverInfo.ContentPackagesMatch(GameMain.SelectedPackages), UserData = "compatible" }; @@ -1696,6 +1710,14 @@ namespace Barotrauma serverName.Text = ToolBox.LimitString(serverName.Text, serverName.Font, serverName.Rect.Width); }; + if (serverInfo.ContentPackageNames.Any()) + { + if (serverInfo.ContentPackageNames.Any(cp => !cp.Equals(GameMain.VanillaContent.Name, StringComparison.OrdinalIgnoreCase))) + { + serverName.TextColor = new Color(219, 125, 217); + } + } + new GUITickBox(new RectTransform(new Vector2(columnRelativeWidth[3], 0.9f), serverContent.RectTransform, Anchor.Center), label: "") { ToolTip = TextManager.Get((serverInfo.GameStarted) ? "ServerListRoundStarted" : "ServerListRoundNotStarted"), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 6fda6eb90..932d9a409 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -3,10 +3,14 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +#if DEBUG +using System.IO; +#else +using Barotrauma.IO; +#endif namespace Barotrauma { @@ -415,7 +419,11 @@ namespace Barotrauma { string xmlPath = doc.ParseContentPathFromUri(); xmlPathText.Text += "\n" + xmlPath; +#if DEBUG doc.Save(xmlPath); +#else + doc.SaveSafe(xmlPath); +#endif } xmlPathText.TextColor = GUI.Style.Green; return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 1d7663c4b..71584a385 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -15,6 +15,7 @@ namespace Barotrauma { private GUIFrame menu; private GUIListBox subscribedItemList, topItemList; + private GUITextBox subscribedItemFilter, topItemFilter; private GUIListBox publishedItemList, myItemList; @@ -26,7 +27,7 @@ namespace Barotrauma //listbox that shows the files included in the item being created private GUIListBox createItemFileList; - private FileSystemWatcher createItemWatcher; + private System.IO.FileSystemWatcher createItemWatcher; private readonly List tabButtons = new List(); @@ -135,6 +136,8 @@ namespace Barotrauma } }; + subscribedItemFilter = CreateFilterBox(modsContainer, subscribedItemList); + modsPreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 1.0f), tabs[(int)Tab.Mods].RectTransform, Anchor.TopRight), style: null); //------------------------------------------------------------------------------- @@ -163,6 +166,8 @@ namespace Barotrauma } }; + topItemFilter = CreateFilterBox(listContainer, topItemList); + new GUIButton(new RectTransform(new Vector2(1.0f, 0.02f), listContainer.RectTransform), TextManager.Get("FindModsButton"), style: "GUIButtonSmall") { OnClicked = (btn, userdata) => @@ -235,6 +240,31 @@ namespace Barotrauma subscribedCoroutine = CoroutineManager.StartCoroutine(PollSubscribedItems()); } + private GUITextBox CreateFilterBox(GUIComponent parent, GUIListBox listbox) + { + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true) + { + Stretch = true + }; + filterContainer.RectTransform.SetAsFirstChild(); + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnTextChanged += (textBox, text) => + { + foreach (GUIComponent child in listbox.Content.Children) + { + if (!(child.UserData is Steamworks.Ugc.Item item)) { continue; } + child.Visible = string.IsNullOrEmpty(text) ? true : (item.Title?.ToLower().Contains(text.ToLower()) ?? false); + } + return true; + }; + + return searchBox; + } + public override void Select() { base.Select(); @@ -448,6 +478,18 @@ namespace Barotrauma return; } + string text = string.Empty; + if (listBox == subscribedItemList) + { + text = subscribedItemFilter.Text; + } + else if (listBox == topItemList) + { + text = topItemFilter.Text; + } + + bool visible = string.IsNullOrEmpty(text) ? true : (item?.Title?.ToLower().Contains(text.ToLower()) ?? false); + int prevIndex = -1; var existingFrame = listBox.Content.FindChild((component) => { return (component.UserData is Steamworks.Ugc.Item?) && (component.UserData as Steamworks.Ugc.Item?)?.Id == item?.Id; }); if (existingFrame != null) @@ -459,7 +501,8 @@ namespace Barotrauma var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), style: "ListBoxElement") { - UserData = item + UserData = item, + Visible = visible }; if (prevIndex > -1) { @@ -583,7 +626,7 @@ namespace Barotrauma } else { - installed = SteamManager.EnableWorkShopItem(item, true, out string errorMsg, Selected == this); + installed = SteamManager.EnableWorkShopItem(item, out string errorMsg, Selected == this); if (!installed) { DebugConsole.NewMessage(errorMsg, Color.Red); @@ -643,7 +686,7 @@ namespace Barotrauma { bool reselect = GameMain.Config.SelectedContentPackages.Any(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == item?.Url); if (!SteamManager.DisableWorkShopItem(item, false, out string errorMsg) || - !SteamManager.EnableWorkShopItem(item, true, out errorMsg, reselect, true)) + !SteamManager.EnableWorkShopItem(item, out errorMsg, reselect, true)) { DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\": {errorMsg}", null, true); elem.Flash(GUI.Style.Red); @@ -837,7 +880,7 @@ namespace Barotrauma item?.Download(onInstalled: () => { - if (SteamManager.EnableWorkShopItem(item, false, out _)) + if (SteamManager.EnableWorkShopItem(item, out _)) { textBlock.Text = TextManager.Get("workshopiteminstalled"); frame.Flash(GUI.Style.Green); @@ -1038,7 +1081,7 @@ namespace Barotrauma string previewImagePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName)); try { - using (Stream s = File.Create(previewImagePath)) + using (System.IO.Stream s = File.Create(previewImagePath)) { sub.PreviewImage.Texture.SaveAsPng(s, (int)sub.PreviewImage.size.X, (int)sub.PreviewImage.size.Y); itemEditor = itemEditor?.WithPreviewFile(previewImagePath); @@ -1290,10 +1333,10 @@ namespace Barotrauma }; createItemFileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.35f), createItemContent.RectTransform)); createItemWatcher?.Dispose(); - createItemWatcher = new FileSystemWatcher(Path.GetDirectoryName(itemContentPackage.Path)) + createItemWatcher = new System.IO.FileSystemWatcher(Path.GetDirectoryName(itemContentPackage.Path)) { Filter = "*", - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName + NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName }; createItemWatcher.Created += OnFileSystemChanges; createItemWatcher.Deleted += OnFileSystemChanges; @@ -1551,7 +1594,7 @@ namespace Barotrauma volatile bool refreshFileList = false; - private void OnFileSystemChanges(object sender, FileSystemEventArgs e) + private void OnFileSystemChanges(object sender, System.IO.FileSystemEventArgs e) { refreshFileList = true; } @@ -1564,14 +1607,26 @@ namespace Barotrauma List files = itemContentPackage.Files.ToList(); - foreach (ContentFile contentFile in files) + for (int i = files.Count - 1; i >= 0; i--) { + ContentFile contentFile = files[i]; + bool fileExists = File.Exists(contentFile.Path); - if (!fileExists) { itemContentPackage.Files.Remove(contentFile); continue; } + if (contentFile.Type == ContentType.Executable || + contentFile.Type == ContentType.ServerExecutable) + { + fileExists |= File.Exists(contentFile.Path + ".dll"); + } + + if (!fileExists) + { + itemContentPackage.Files.Remove(contentFile); + files.RemoveAt(i); + } } - List allFiles = Directory.GetFiles(Path.GetDirectoryName(itemContentPackage.Path), "*", SearchOption.AllDirectories) + List allFiles = Directory.GetFiles(Path.GetDirectoryName(itemContentPackage.Path), "*", System.IO.SearchOption.AllDirectories) .Select(f => new ContentFile(f, ContentType.None)) .Where(file => Path.GetFileName(file.Path) != SteamManager.MetadataFileName && Path.GetFileName(file.Path) != SteamManager.PreviewImageName) @@ -1579,22 +1634,31 @@ namespace Barotrauma for (int i=0;i string.Equals(Path.GetFullPath(f.Path).CleanUpPath(), - Path.GetFullPath(file.Path).CleanUpPath(), - StringComparison.InvariantCultureIgnoreCase)); + ContentFile otherFile = files.Find(f => string.Equals(Path.GetFullPath(f.Path).CleanUpPath(), + Path.GetFullPath(file.Path).CleanUpPath(), + StringComparison.InvariantCultureIgnoreCase)); if (otherFile != null) { //replace the generated ContentFile object with the one that's present in the //content package to determine which tickboxes should already be checked allFiles[i] = otherFile; + files.Remove(otherFile); } } + allFiles.AddRange(files); + foreach (ContentFile contentFile in allFiles) { bool illegalPath = !ContentPackage.IsModFilePathAllowed(contentFile); bool fileExists = File.Exists(contentFile.Path); + if (contentFile.Type == ContentType.Executable || + contentFile.Type == ContentType.ServerExecutable) + { + fileExists |= File.Exists(contentFile.Path + ".dll"); + } + var fileFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.12f), createItemFileList.Content.RectTransform) { MinSize = new Point(0, 20) }, style: "ListBoxElement") { @@ -1662,36 +1726,39 @@ namespace Barotrauma return true; }; - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), content.RectTransform), TextManager.Get("Delete"), style: "GUIButtonSmall") + if (!files.Contains(contentFile)) //this prevents deletion of files not contained in the mod's path (i.e. vanilla content) { - OnClicked = (btn, userdata) => + new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), content.RectTransform), TextManager.Get("Delete"), style: "GUIButtonSmall") { - var msgBox = new GUIMessageBox(TextManager.Get("ConfirmFileDeletionHeader"), - TextManager.GetWithVariable("ConfirmFileDeletion", "[file]", contentFile.Path), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + OnClicked = (btn, userdata) => { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (applyButton, obj) => - { - try + var msgBox = new GUIMessageBox(TextManager.Get("ConfirmFileDeletionHeader"), + TextManager.GetWithVariable("ConfirmFileDeletion", "[file]", contentFile.Path), + new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) { - File.Delete(contentFile.Path); - if (contentFile.Type == ContentType.Submarine) { SubmarineInfo.RefreshSavedSub(contentFile.Path); } - } - catch (Exception e) + UserData = "verificationprompt" + }; + msgBox.Buttons[0].OnClicked = (applyButton, obj) => { - DebugConsole.ThrowError($"Failed to delete \"${contentFile.Path}\".", e); - } - //RefreshCreateItemFileList(); - RefreshMyItemList(); + try + { + File.Delete(contentFile.Path); + if (contentFile.Type == ContentType.Submarine) { SubmarineInfo.RefreshSavedSub(contentFile.Path); } + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to delete \"${contentFile.Path}\".", e); + } + //RefreshCreateItemFileList(); + RefreshMyItemList(); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked = msgBox.Close; return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - return true; - } - }; + } + }; + } content.Recalculate(); fileFrame.RectTransform.MinSize = @@ -1714,7 +1781,7 @@ namespace Barotrauma var workshopPublishStatus = SteamManager.StartPublishItem(itemContentPackage, itemEditor); if (workshopPublishStatus != null) { - if (!itemEditor.Value.Tags.Contains("unstable")) { itemEditor.Value.Tags.Add("unstable"); } + if (!(itemEditor?.HasTag("unstable") ?? false)) { itemEditor = itemEditor?.WithTag("unstable"); } CoroutineManager.StartCoroutine(WaitForPublish(workshopPublishStatus), "WaitForPublish"); } msgBox.Close(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 60d71e339..544e271a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -4,11 +4,15 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading; using System.Xml.Linq; using Microsoft.Xna.Framework.Input; +#if DEBUG +using System.IO; +#else +using Barotrauma.IO; +#endif // ReSharper disable AccessToModifiedClosure, PossibleLossOfFraction, RedundantLambdaParameterType, UnusedVariable @@ -50,7 +54,7 @@ namespace Barotrauma private GUITextBlock subNameLabel; - private bool showThalamus = true; + public bool ShowThalamus { get; private set; } = true; private bool entityMenuOpen = true; private float entityMenuOpenState = 1.0f; @@ -183,7 +187,7 @@ namespace Barotrauma private void CreateUI() { - TopPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), GUI.Canvas) { MinSize = new Point(0, 35) }, "GUIFrameTop"); + TopPanel = new GUIFrame(new RectTransform(new Vector2(GUI.Canvas.RelativeSize.X, 0.01f), GUI.Canvas) { MinSize = new Point(0, 35) }, "GUIFrameTop"); GUILayoutGroup paddedTopPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.8f), TopPanel.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) @@ -480,8 +484,8 @@ namespace Barotrauma new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("mapentitycategory.thalamus")) { UserData = "thalamus", - Selected = showThalamus, - OnSelected = (GUITickBox obj) => { showThalamus = obj.Selected; return true; }, + Selected = ShowThalamus, + OnSelected = (GUITickBox obj) => { ShowThalamus = obj.Selected; return true; }, }; showEntitiesTickBoxes.AddRange(paddedShowEntitiesPanel.Children.Select(c => c as GUITickBox)); @@ -747,10 +751,15 @@ namespace Barotrauma frame.RectTransform.MinSize = new Point(0, frame.Rect.Width); frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); - - string name = legacy ? ep.Name + " (legacy)" : ep.Name; + string name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; frame.ToolTip = string.IsNullOrEmpty(ep.Description) ? name : name + '\n' + ep.Description; + if (ep.HideInMenus) + { + frame.Color = Color.Red; + name = "[HIDDEN] " + name; + } + GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { Stretch = true, @@ -787,7 +796,11 @@ namespace Barotrauma if (ep is ItemAssemblyPrefab itemAssemblyPrefab) { new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.75f), - paddedFrame.RectTransform, Anchor.TopCenter), onDraw: itemAssemblyPrefab.DrawIcon) + paddedFrame.RectTransform, Anchor.TopCenter), onDraw: (sb, customComponent) => + { + if (GUIImage.LoadingTextures) { return; } + itemAssemblyPrefab.DrawIcon(sb, customComponent); + }) { HideElementsOutsideFrame = true, ToolTip = frame.RawToolTip @@ -795,7 +808,7 @@ namespace Barotrauma } GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), - text: ep.Name, textAlignment: Alignment.Center, font: GUI.SmallFont) + text: name, textAlignment: Alignment.Center, font: GUI.SmallFont) { CanBeFocused = false }; @@ -1169,7 +1182,7 @@ namespace Barotrauma if (!Directory.Exists(filePath)) { var e = Directory.CreateDirectory(filePath); - e.Attributes = FileAttributes.Directory | FileAttributes.Hidden; + e.Attributes = System.IO.FileAttributes.Directory | System.IO.FileAttributes.Hidden; if (!e.Exists) { return; } } @@ -1221,15 +1234,33 @@ namespace Barotrauma return false; } - Submarine.MainSub.Info.Name = name; - string savePath = name + ".sub"; string prevSavePath = null; if (!string.IsNullOrEmpty(Submarine.MainSub?.Info.FilePath) && Submarine.MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { prevSavePath = Submarine.MainSub.Info.FilePath.CleanUpPath(); - savePath = Path.Combine(Path.GetDirectoryName(Submarine.MainSub.Info.FilePath), savePath).CleanUpPath(); + string prevDir = Path.GetDirectoryName(Submarine.MainSub.Info.FilePath).CleanUpPath(); + string[] subDirs = prevDir.Split('/'); + bool forceToSubFolder = Steam.SteamManager.IsInitialized; + bool isInSubFolder = subDirs.Length > 0 && subDirs[0].Equals("Submarines", StringComparison.InvariantCultureIgnoreCase); + if (forceToSubFolder && subDirs.Length > 1 && subDirs[0].Equals("Mods", StringComparison.InvariantCultureIgnoreCase)) + { + string modName = subDirs[1]; + ContentPackage contentPackage = ContentPackage.List.Find(p => p.Name.Equals(modName, StringComparison.InvariantCultureIgnoreCase)); + if (contentPackage != null) + { + Steamworks.Data.PublishedFileId packageId = Steam.SteamManager.GetWorkshopItemIDFromUrl(contentPackage.SteamWorkshopUrl); + Steamworks.Ugc.Item? item = Steamworks.Ugc.Item.GetAsync(packageId).Result; + if (item?.Owner.Id == Steam.SteamManager.GetSteamID()) + { + forceToSubFolder = false; + contentPackage.Files.Add(new ContentFile(Path.Combine(prevDir, savePath).CleanUpPath(), ContentType.Submarine)); + contentPackage.Save(contentPackage.Path); + } + } + } + savePath = Path.Combine(forceToSubFolder && !isInSubFolder ? SubmarineInfo.SavePath : prevDir, savePath).CleanUpPath(); } else { @@ -1255,7 +1286,7 @@ namespace Barotrauma if (previewImage?.Sprite?.Texture != null) { bool savePreviewImage = true; - using MemoryStream imgStream = new MemoryStream(); + using System.IO.MemoryStream imgStream = new System.IO.MemoryStream(); try { previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height); @@ -1298,11 +1329,13 @@ namespace Barotrauma SetMode(Mode.Default); } - saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) saveFrame = null; return true; } }; + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.5f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 400) }); var paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; @@ -1465,10 +1498,10 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { - using (MemoryStream imgStream = new MemoryStream()) + using (System.IO.MemoryStream imgStream = new System.IO.MemoryStream()) { CreateImage(defaultPreviewImageSize.X, defaultPreviewImageSize.Y, imgStream); - previewImage.Sprite = new Sprite(TextureLoader.FromStream(imgStream), null, null); + previewImage.Sprite = new Sprite(TextureLoader.FromStream(imgStream, compress: false), null, null); if (Submarine.MainSub != null) { Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; @@ -1611,11 +1644,13 @@ namespace Barotrauma { SetMode(Mode.Default); - saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) saveFrame = null; return true; } }; + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.3f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 300) }); GUILayoutGroup paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { @@ -1703,10 +1738,24 @@ namespace Barotrauma } } - var hideInMenusTickBox = nameBox.Parent.GetChildByUserData("hideinmenus") as GUITickBox; - bool hideInMenus = hideInMenusTickBox == null ? false : hideInMenusTickBox.Selected; - - string saveFolder = Path.Combine("Content", "Items", "Assemblies"); + bool hideInMenus = !(nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox) ? false : hideInMenusTickBox.Selected; +#if DEBUG + string saveFolder = ItemAssemblyPrefab.VanillaSaveFolder; +#else + string saveFolder = ItemAssemblyPrefab.SaveFolder; + if (!Directory.Exists(saveFolder)) + { + try + { + Directory.CreateDirectory(saveFolder); + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to create a directory for the item assmebly.", e); + return false; + } + } +#endif string filePath = Path.Combine(saveFolder, nameBox.Text + ".xml"); if (File.Exists(filePath)) @@ -1729,8 +1778,11 @@ namespace Barotrauma void Save() { XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList, nameBox.Text, descriptionBox.Text, hideInMenus)); +#if DEBUG doc.Save(filePath); - +#else + doc.SaveSafe(filePath); +#endif new ItemAssemblyPrefab(filePath); UpdateEntityList(); } @@ -1744,11 +1796,13 @@ namespace Barotrauma CloseItem(); SetMode(Mode.Default); - loadFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + loadFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) loadFrame = null; return true; }, }; + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, loadFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.5f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); var paddedLoadFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; @@ -3025,6 +3079,7 @@ namespace Barotrauma } hullVolumeFrame.Visible = MapEntity.SelectedList.Any(s => s is Hull); + hullVolumeFrame.RectTransform.AbsoluteOffset = new Point(Math.Max(showEntitiesPanel.Rect.Right, previouslyUsedPanel.Rect.Right), 0); saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0; var offset = cam.WorldView.Top - cam.ScreenToWorld(new Vector2(0, GameMain.GraphicsHeight - EntityMenu.Rect.Top)).Y; @@ -3547,7 +3602,7 @@ namespace Barotrauma } Submarine.DrawBack(spriteBatch, true, e => e is Structure s && - (showThalamus || !s.prefab.Category.HasFlag(MapEntityCategory.Thalamus)) && + (ShowThalamus || !s.prefab.Category.HasFlag(MapEntityCategory.Thalamus)) && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); spriteBatch.End(); @@ -3567,15 +3622,15 @@ namespace Barotrauma Submarine.DrawBack(spriteBatch, true, e => (!(e is Structure) || e.SpriteDepth < 0.9f) && - (showThalamus || !e.prefab.Category.HasFlag(MapEntityCategory.Thalamus))); + (ShowThalamus || !e.prefab.Category.HasFlag(MapEntityCategory.Thalamus))); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawDamageable(spriteBatch, null, editing: true, e => showThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); + Submarine.DrawDamageable(spriteBatch, null, editing: true, e => ShowThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawFront(spriteBatch, editing: true, e => showThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); + Submarine.DrawFront(spriteBatch, editing: true, e => ShowThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); if (!WiringMode && !IsMouseOnEditorGUI()) { MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam); @@ -3636,7 +3691,7 @@ namespace Barotrauma spriteBatch.End(); } - private void CreateImage(int width, int height, Stream stream) + private void CreateImage(int width, int height, System.IO.Stream stream) { MapEntity.SelectedList.Clear(); @@ -3688,7 +3743,7 @@ namespace Barotrauma public void SaveScreenShot(int width, int height, string filePath) { - Stream stream = File.OpenWrite(filePath); + System.IO.Stream stream = File.OpenWrite(filePath); CreateImage(width, height, stream); stream.Dispose(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 26f301498..31c7fbd53 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -392,22 +392,40 @@ namespace Barotrauma public GUIComponent CreateBoolField(ISerializableEntity entity, SerializableProperty property, bool value, string displayName, string toolTip) { - GUITickBox propertyTickBox = new GUITickBox(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), displayName) + var editableAttribute = property.GetAttribute(); + if (editableAttribute.ReadOnly) { - Font = GUI.SmallFont, - Selected = value, - ToolTip = toolTip, - OnSelected = (tickBox) => + var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) { - if (SetPropertyValue(property, entity, tickBox.Selected)) + ToolTip = toolTip + }; + var valueField = new GUITextBlock(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), value.ToString()) + { + ToolTip = toolTip, + Font = GUI.SmallFont + }; + return valueField; + } + else + { + GUITickBox propertyTickBox = new GUITickBox(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), displayName) + { + Font = GUI.SmallFont, + Selected = value, + ToolTip = toolTip, + OnSelected = (tickBox) => { - TrySendNetworkUpdate(entity, property); + if (SetPropertyValue(property, entity, tickBox.Selected)) + { + TrySendNetworkUpdate(entity, property); + } + return true; } - return true; - } - }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { propertyTickBox }); } - return propertyTickBox; + }; + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { propertyTickBox }); } + return propertyTickBox; + } } public GUIComponent CreateIntField(ISerializableEntity entity, SerializableProperty property, int value, string displayName, string toolTip) @@ -453,8 +471,11 @@ namespace Barotrauma public GUIComponent CreateFloatField(ISerializableEntity entity, SerializableProperty property, float value, string displayName, string toolTip) { - var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - 1, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent) + { + CanBeFocused = false + }; + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) { ToolTip = toolTip }; @@ -630,7 +651,14 @@ namespace Barotrauma for (int i = 1; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.vectorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + + string componentLabel = GUI.vectorComponentLabels[i]; + if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) + { + componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); + } + + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) { @@ -683,7 +711,13 @@ namespace Barotrauma for (int i = 1; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.vectorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + + string componentLabel = GUI.vectorComponentLabels[i]; + if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) + { + componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); + } + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { @@ -738,7 +772,14 @@ namespace Barotrauma for (int i = 2; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.33f, 1), inputArea.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.vectorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + + string componentLabel = GUI.vectorComponentLabels[i]; + if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) + { + componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); + } + + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { @@ -797,7 +838,14 @@ namespace Barotrauma for (int i = 3; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.vectorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + + string componentLabel = GUI.vectorComponentLabels[i]; + if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) + { + componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); + } + + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { @@ -1044,15 +1092,16 @@ namespace Barotrauma private void MultiSetProperties(SerializableProperty property, object parentObject, object value) { if (!(Screen.Selected is SubEditorScreen) || MapEntity.SelectedList.Count <= 1) { return; } - if (!(parentObject is ItemComponent || parentObject is Item || parentObject is Structure)) { return; } + if (!(parentObject is ItemComponent || parentObject is Item || parentObject is Structure || parentObject is Hull)) { return; } foreach (var entity in MapEntity.SelectedList.Where(entity => entity != parentObject)) { switch (parentObject) { + case Hull _: case Structure _: case Item _: - { + { if (entity.GetType() == parentObject.GetType()) { property.PropertyInfo.SetValue(entity, value); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index b6d19a664..26080fec0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -1,7 +1,7 @@ using System; using OpenAL; using Microsoft.Xna.Framework; -using System.IO; +using Barotrauma.IO; using System.Xml.Linq; namespace Barotrauma.Sounds @@ -76,6 +76,8 @@ namespace Barotrauma.Sounds protected set; } + public bool IgnoreMuffling { get; set; } + /// /// How many instances of the same sound clip can be playing at the same time /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index e33862767..3c669361d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -5,7 +5,7 @@ using System.Xml.Linq; using OpenAL; using Microsoft.Xna.Framework; using System.Linq; -using System.IO; +using Barotrauma.IO; namespace Barotrauma.Sounds { @@ -286,7 +286,7 @@ namespace Barotrauma.Sounds if (!File.Exists(filename)) { - throw new FileNotFoundException("Sound file \"" + filename + "\" doesn't exist!"); + throw new System.IO.FileNotFoundException("Sound file \"" + filename + "\" doesn't exist!"); } Sound newSound = new OggSound(this, filename, stream, null); @@ -304,7 +304,7 @@ namespace Barotrauma.Sounds string filePath = overrideFilePath ?? element.GetAttributeString("file", ""); if (!File.Exists(filePath)) { - throw new FileNotFoundException("Sound file \"" + filePath + "\" doesn't exist!"); + throw new System.IO.FileNotFoundException("Sound file \"" + filePath + "\" doesn't exist!"); } var newSound = new OggSound(this, filePath, stream, xElement: element); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index aa19c5ba6..ba6395a32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -3,7 +3,7 @@ using Barotrauma.Sounds; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -260,7 +260,7 @@ namespace Barotrauma break; } } - catch (FileNotFoundException e) + catch (System.IO.FileNotFoundException e) { DebugConsole.ThrowError("Error while initializing SoundPlayer.", e); } @@ -377,6 +377,8 @@ namespace Barotrauma private static void UpdateWaterAmbience(float ambienceVolume, float deltaTime) { + if (GameMain.SoundManager.Disabled) { return; } + //how fast the sub is moving, scaled to 0.0 -> 1.0 float movementSoundVolume = 0.0f; @@ -453,17 +455,25 @@ namespace Barotrauma Vector2 listenerPos = new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y); foreach (Gap gap in Gap.GapList) { - if (gap.Open < 0.01f) continue; - float gapFlow = Math.Abs(gap.LerpedFlowForce.X) + Math.Abs(gap.LerpedFlowForce.Y) * 2.5f; - - if (gapFlow < 10.0f) continue; - - int flowSoundIndex = (int)Math.Floor(MathHelper.Clamp(gapFlow / MaxFlowStrength, 0, FlowSounds.Count)); - flowSoundIndex = Math.Min(flowSoundIndex, FlowSounds.Count - 1); - Vector2 diff = gap.WorldPosition - listenerPos; if (Math.Abs(diff.X) < FlowSoundRange && Math.Abs(diff.Y) < FlowSoundRange) { + if (gap.Open < 0.01f) { continue; } + float gapFlow = Math.Abs(gap.LerpedFlowForce.X) + Math.Abs(gap.LerpedFlowForce.Y) * 2.5f; + if (!gap.IsRoomToRoom) { gapFlow *= 2.0f; } + if (gapFlow < 10.0f) { continue; } + + if (gap.linkedTo.Count == 2 && gap.linkedTo[0] is Hull hull1 && gap.linkedTo[1] is Hull hull2) + { + //no flow sounds between linked hulls (= rooms consisting of multiple hulls) + if (hull1.linkedTo.Contains(hull2)) { continue; } + if (hull1.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { continue; } + if (hull2.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { continue; } + } + + int flowSoundIndex = (int)Math.Floor(MathHelper.Clamp(gapFlow / MaxFlowStrength, 0, FlowSounds.Count)); + flowSoundIndex = Math.Min(flowSoundIndex, FlowSounds.Count - 1); + float dist = diff.Length(); float distFallOff = dist / FlowSoundRange; if (distFallOff >= 0.99f) continue; @@ -484,10 +494,10 @@ namespace Barotrauma { flowVolumeLeft[i] = (targetFlowLeft[i] < flowVolumeLeft[i]) ? Math.Max(targetFlowLeft[i], flowVolumeLeft[i] - deltaTime) : - Math.Min(targetFlowLeft[i], flowVolumeLeft[i] + deltaTime); + Math.Min(targetFlowLeft[i], flowVolumeLeft[i] + deltaTime * 10.0f); flowVolumeRight[i] = (targetFlowRight[i] < flowVolumeRight[i]) ? Math.Max(targetFlowRight[i], flowVolumeRight[i] - deltaTime) : - Math.Min(targetFlowRight[i], flowVolumeRight[i] + deltaTime); + Math.Min(targetFlowRight[i], flowVolumeRight[i] + deltaTime * 10.0f); if (flowVolumeLeft[i] < 0.05f && flowVolumeRight[i] < 0.05f) { @@ -634,8 +644,12 @@ namespace Barotrauma float far = range ?? sound.BaseFar; - if (Vector2.DistanceSquared(new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y), position) > far * far) return null; - return sound.Play(volume ?? sound.BaseGain, far, position, muffle: ShouldMuffleSound(Character.Controlled, position, far, hullGuess)); + if (Vector2.DistanceSquared(new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y), position) > far * far) + { + return null; + } + bool muffle = !sound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, far, hullGuess); + return sound.Play(volume ?? sound.BaseGain, far, position, muffle: muffle); } private static void UpdateMusic(float deltaTime) @@ -921,20 +935,22 @@ namespace Barotrauma PlayDamageSound(damageType, damage, bodyPosition, 800.0f); } + private static readonly List tempList = new List(); public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable tags = null) { damage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f); - var sounds = damageSounds.FindAll(s => - (s.damageRange == Vector2.Zero || - (damage >= s.damageRange.X && damage <= s.damageRange.Y)) && - s.damageType == damageType && - (tags == null ? string.IsNullOrEmpty(s.requiredTag) : tags.Contains(s.requiredTag))); - - if (!sounds.Any()) return; - - int selectedSound = Rand.Int(sounds.Count); - sounds[selectedSound].sound.Play(1.0f, range, position, muffle: ShouldMuffleSound(Character.Controlled, position, range, null)); - } - + tempList.Clear(); + foreach (var s in damageSounds) + { + if ((s.damageRange == Vector2.Zero || + (damage >= s.damageRange.X && damage <= s.damageRange.Y)) && + string.Equals(s.damageType, damageType, StringComparison.OrdinalIgnoreCase) && + (tags == null ? string.IsNullOrEmpty(s.requiredTag) : tags.Contains(s.requiredTag))) + { + tempList.Add(s); + } + } + tempList.GetRandom().sound?.Play(1.0f, range, position, muffle: ShouldMuffleSound(Character.Controlled, position, range, null)); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs index e75074efb..91175d95a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs @@ -109,12 +109,12 @@ namespace Barotrauma.SpriteDeformations } } - protected override void GetDeformation(out Vector2[,] deformation, out float multiplier) + protected override void GetDeformation(out Vector2[,] deformation, out float multiplier, bool inverse) { deformation = Deformation; multiplier = CustomDeformationParams.Frequency <= 0.0f ? CustomDeformationParams.Amplitude : - (float)Math.Sin(phase) * CustomDeformationParams.Amplitude; + (float)Math.Sin(inverse ? -phase : phase) * CustomDeformationParams.Amplitude; multiplier *= Params.Strength; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs index e0ebfe766..6b7d599dd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs @@ -54,7 +54,7 @@ namespace Barotrauma.SpriteDeformations phase = Rand.Range(0.0f, MathHelper.TwoPi); } - protected override void GetDeformation(out Vector2[,] deformation, out float multiplier) + protected override void GetDeformation(out Vector2[,] deformation, out float multiplier, bool inverse) { deformation = this.deformation; multiplier = InflateParams.Frequency <= 0.0f ? InflateParams.Scale : (float)(Math.Sin(phase) + 1.0f) / 2.0f * InflateParams.Scale; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/JointBendDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/JointBendDeformation.cs index c94d6a148..0c6d1058f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/JointBendDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/JointBendDeformation.cs @@ -56,7 +56,7 @@ namespace Barotrauma.SpriteDeformations public JointBendDeformation(XElement element) : base(element, new JointBendDeformationParams(element)) { } - protected override void GetDeformation(out Vector2[,] deformation, out float multiplier) + protected override void GetDeformation(out Vector2[,] deformation, out float multiplier, bool inverse) { deformation = Deformation; multiplier = 1.0f;// this.multiplier; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs index 7df0d7ad5..3830daaa1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs @@ -47,7 +47,7 @@ namespace Barotrauma.SpriteDeformations } } - protected override void GetDeformation(out Vector2[,] deformation, out float multiplier) + protected override void GetDeformation(out Vector2[,] deformation, out float multiplier, bool inverse) { deformation = Deformation; multiplier = NoiseDeformationParams.Amplitude * Params.Strength; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs index a211ca8b8..6b38a7826 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs @@ -100,7 +100,7 @@ namespace Barotrauma.SpriteDeformations } } - protected override void GetDeformation(out Vector2[,] deformation, out float multiplier) + protected override void GetDeformation(out Vector2[,] deformation, out float multiplier, bool inverse) { deformation = Deformation; multiplier = 1.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs index d88669f5a..118734f9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs @@ -195,11 +195,12 @@ namespace Barotrauma.SpriteDeformations Deformation = new Vector2[Params.Resolution.X, Params.Resolution.Y]; } - protected abstract void GetDeformation(out Vector2[,] deformation, out float multiplier); + protected abstract void GetDeformation(out Vector2[,] deformation, out float multiplier, bool inverse); public abstract void Update(float deltaTime); - public static Vector2[,] GetDeformation(IEnumerable animations, Vector2 scale) + private static readonly List yValues = new List(); + public static Vector2[,] GetDeformation(IEnumerable animations, Vector2 scale, bool inverseY = false) { foreach (SpriteDeformation animation in animations) { @@ -221,8 +222,16 @@ namespace Barotrauma.SpriteDeformations Vector2[,] deformation = new Vector2[resolution.X, resolution.Y]; foreach (SpriteDeformation animation in animations) { - animation.GetDeformation(out Vector2[,] animDeformation, out float multiplier); - + yValues.Clear(); + for (int y = 0; y < resolution.Y; y++) + { + yValues.Add(y); + } + if (inverseY && animation is CustomDeformation) + { + yValues.Reverse(); + } + animation.GetDeformation(out Vector2[,] animDeformation, out float multiplier, inverseY); for (int x = 0; x < resolution.X; x++) { for (int y = 0; y < resolution.Y; y++) @@ -230,13 +239,13 @@ namespace Barotrauma.SpriteDeformations switch (animation.Params.BlendMode) { case DeformationBlendMode.Override: - deformation[x,y] = animDeformation[x,y] * scale * multiplier; + deformation[x, yValues[y]] = animDeformation[x, y] * scale * multiplier; break; case DeformationBlendMode.Add: - deformation[x, y] += animDeformation[x, y] * scale * multiplier; + deformation[x, yValues[y]] += animDeformation[x, y] * scale * multiplier; break; case DeformationBlendMode.Multiply: - deformation[x, y] *= animDeformation[x, y] * multiplier; + deformation[x, yValues[y]] *= animDeformation[x, y] * multiplier; break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs index 6d54711f8..bde2b3cb8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs @@ -33,10 +33,12 @@ namespace Barotrauma get { return effect; } } + public bool Invert { get; set; } + private Point spritePos; private Point spriteSize; - partial void InitProjSpecific(XElement element, int? subdivisionsX, int? subdivisionsY, bool lazyLoad) + partial void InitProjSpecific(XElement element, int? subdivisionsX, int? subdivisionsY, bool lazyLoad, bool invert) { if (effect == null) { @@ -47,6 +49,8 @@ namespace Barotrauma effect = GameMain.Instance.Content.Load("Effects/deformshader_opengl"); #endif } + + Invert = invert; //use subdivisions configured in the xml if the arguments passed to the method are null Vector2 subdivisionsInXml = element.GetAttributeVector2("subdivisions", Vector2.One); @@ -121,6 +125,12 @@ namespace Barotrauma uvBottomRight = Vector2.Divide((pos + size).ToVector2(), textureSize); uvTopLeftFlipped = Vector2.Divide(new Vector2(pos.X + size.X, pos.Y), textureSize); uvBottomRightFlipped = Vector2.Divide(new Vector2(pos.X, pos.Y + size.Y), textureSize); + if (Invert) + { + var temp = uvBottomRightFlipped; + uvBottomRightFlipped = uvTopLeftFlipped; + uvTopLeftFlipped = temp; + } for (int i = 0; i < 2; i++) { @@ -267,7 +277,7 @@ namespace Barotrauma Matrix.CreateTranslation(pos); } - public void Draw(Camera cam, Vector3 pos, Vector2 origin, float rotate, Vector2 scale, Color color, bool flip = false, bool mirror = false) + public void Draw(Camera cam, Vector3 pos, Vector2 origin, float rotate, Vector2 scale, Color color, bool mirror = false, bool invert = false) { if (Sprite.Texture == null) { return; } if (!initialized) { Init(); } @@ -291,13 +301,13 @@ namespace Barotrauma effect.Parameters["deformArray"].SetValue(deformAmount); effect.Parameters["deformArrayWidth"].SetValue(deformArrayWidth); effect.Parameters["deformArrayHeight"].SetValue(deformArrayHeight); - if (mirror) + if (invert) { - flip = !flip; + mirror = !mirror; } - effect.Parameters["uvTopLeft"].SetValue(flip ? uvTopLeftFlipped : uvTopLeft); - effect.Parameters["uvBottomRight"].SetValue(flip ? uvBottomRightFlipped : uvBottomRight); - effect.GraphicsDevice.SetVertexBuffer(flip ? flippedVertexBuffer : vertexBuffer); + effect.Parameters["uvTopLeft"].SetValue(mirror ? uvTopLeftFlipped : uvTopLeft); + effect.Parameters["uvBottomRight"].SetValue(mirror ? uvBottomRightFlipped : uvBottomRight); + effect.GraphicsDevice.SetVertexBuffer(mirror ? flippedVertexBuffer : vertexBuffer); effect.GraphicsDevice.Indices = indexBuffer; effect.CurrentTechnique.Passes[0].Apply(); effect.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, triangleCount); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 334662613..812b00617 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Collections.Generic; @@ -47,7 +47,7 @@ namespace Barotrauma partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn) { - texture = LoadTexture(this.FilePath, out Sprite reusedSprite); + texture = LoadTexture(this.FilePath, out Sprite reusedSprite, Compress); if (reusedSprite != null) { FilePath = string.Intern(reusedSprite.FilePath); @@ -70,7 +70,21 @@ namespace Barotrauma Vector4 sourceVector = Vector4.Zero; bool temp2 = false; - LoadTexture(ref sourceVector, ref temp2); + int maxLoadRetries = 3; + for (int i = 0; i <= maxLoadRetries; i++) + { + try + { + LoadTexture(ref sourceVector, ref temp2); + } + catch (System.IO.IOException) + { + if (i == maxLoadRetries || !File.Exists(FilePath)) { throw; } + DebugConsole.NewMessage("Loading sprite \"" + FilePath + "\" failed, retrying in 250 ms..."); + System.Threading.Thread.Sleep(500); + } + } + if (sourceRect.Width == 0 && sourceRect.Height == 0) { sourceRect = new Rectangle((int)sourceVector.X, (int)sourceVector.Y, (int)sourceVector.Z, (int)sourceVector.W); @@ -90,7 +104,7 @@ namespace Barotrauma public void ReloadTexture(IEnumerable spritesToUpdate) { texture.Dispose(); - texture = TextureLoader.FromFile(FilePath); + texture = TextureLoader.FromFile(FilePath, Compress); foreach (Sprite sprite in spritesToUpdate) { sprite.texture = texture; @@ -107,7 +121,7 @@ namespace Barotrauma return LoadTexture(file, out _); } - public static Texture2D LoadTexture(string file, out Sprite reusedSprite) + public static Texture2D LoadTexture(string file, out Sprite reusedSprite, bool compress = true) { reusedSprite = null; if (string.IsNullOrWhiteSpace(file)) @@ -119,10 +133,10 @@ namespace Barotrauma }); return t; } - file = Path.GetFullPath(file); + string fullPath = Path.GetFullPath(file); foreach (Sprite s in LoadedSprites) { - if (s.FullPath == file && s.texture != null && !s.texture.IsDisposed) + if (s.FullPath == fullPath && s.texture != null && !s.texture.IsDisposed) { reusedSprite = s; return s.texture; @@ -131,8 +145,13 @@ namespace Barotrauma if (File.Exists(file)) { - ToolBox.IsProperFilenameCase(file); - return TextureLoader.FromFile(file); + if (!ToolBox.IsProperFilenameCase(file)) + { +#if DEBUG + DebugConsole.ThrowError("Texture file \"" + file + "\" has incorrect case!"); +#endif + } + return TextureLoader.FromFile(file, compress); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 9ccf7dc16..0e3b95f8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -15,7 +15,7 @@ namespace Barotrauma private static HashSet ActiveLoopingSounds = new HashSet(); private static double LastMuffleCheckTime; - private List sounds = new List(); + private readonly List sounds = new List(); private SoundSelectionMode soundSelectionMode; private SoundChannel soundChannel; private Entity soundEmitter; @@ -76,7 +76,7 @@ namespace Barotrauma } else { - int selectedSoundIndex = 0; + int selectedSoundIndex; if (soundSelectionMode == SoundSelectionMode.ItemSpecific && entity is Item item) { selectedSoundIndex = item.ID % sounds.Count; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs index 42d8d7f84..deed55665 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs @@ -17,6 +17,7 @@ namespace Barotrauma public static void Init() { + List.Clear(); var files = GameMain.Instance.GetFilesOfType(ContentType.TraitorMissions); foreach (ContentFile file in files) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs index f6affb2cb..f6c015be6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs @@ -34,7 +34,7 @@ using System; using System.Collections.Generic; using System.Configuration; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Text; #if NET_4_0 using System.Web.Configuration; @@ -225,7 +225,7 @@ namespace RestSharp.Contrib if (String.IsNullOrEmpty(value)) return value; - MemoryStream result = new MemoryStream(); + System.IO.MemoryStream result = new System.IO.MemoryStream(); int length = value.Length; for (int i = 0; i < length; i++) UrlPathEncodeChar(value[i], result); @@ -248,7 +248,7 @@ namespace RestSharp.Contrib if (count < 0 || count > blen - offset) throw new ArgumentOutOfRangeException("count"); - MemoryStream result = new MemoryStream(count); + System.IO.MemoryStream result = new System.IO.MemoryStream(count); int end = offset + count; for (int i = offset; i < end; i++) UrlEncodeChar((char)bytes[i], result, false); @@ -575,7 +575,7 @@ namespace RestSharp.Contrib ); } - internal static void UrlEncodeChar(char c, Stream result, bool isUnicode) + internal static void UrlEncodeChar(char c, System.IO.Stream result, bool isUnicode) { if (c > 255) { @@ -632,7 +632,7 @@ namespace RestSharp.Contrib result.WriteByte((byte)c); } - internal static void UrlPathEncodeChar(char c, Stream result) + internal static void UrlPathEncodeChar(char c, System.IO.Stream result) { if (c < 33 || c > 126) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs index 1ba481dc2..9f0ee6910 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs @@ -34,7 +34,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Security.Permissions; using System.Text; @@ -76,6 +76,7 @@ namespace RestSharp.Contrib #region Methods + /* public static void HtmlAttributeEncode(string s, TextWriter output) { if (output == null) @@ -92,6 +93,7 @@ namespace RestSharp.Contrib output.Write(HttpEncoder.HtmlAttributeEncode(s)); #endif } + */ public static string HtmlAttributeEncode(string s) { @@ -113,7 +115,7 @@ namespace RestSharp.Contrib return UrlDecode(str, Encoding.UTF8); } - static char[] GetChars(MemoryStream b, Encoding e) + static char[] GetChars(System.IO.MemoryStream b, Encoding e) { return e.GetChars(b.GetBuffer(), 0, (int)b.Length); } @@ -260,7 +262,7 @@ namespace RestSharp.Contrib throw new ArgumentOutOfRangeException("count"); StringBuilder output = new StringBuilder(); - MemoryStream acc = new MemoryStream(); + System.IO.MemoryStream acc = new System.IO.MemoryStream(); int end = count + offset; int xchar; @@ -354,7 +356,7 @@ namespace RestSharp.Contrib if (count < 0 || offset > len - count) throw new ArgumentOutOfRangeException("count"); - MemoryStream result = new MemoryStream(); + System.IO.MemoryStream result = new System.IO.MemoryStream(); int end = offset + count; for (int i = offset; i < end; i++) { @@ -492,7 +494,7 @@ namespace RestSharp.Contrib if (str.Length == 0) return new byte[0]; - MemoryStream result = new MemoryStream(str.Length); + System.IO.MemoryStream result = new System.IO.MemoryStream(str.Length); foreach (char c in str) { HttpEncoder.UrlEncodeChar(c, result, true); @@ -525,6 +527,7 @@ namespace RestSharp.Contrib /// /// The HTML string to decode /// The TextWriter output stream containing the decoded string. + /* public static void HtmlDecode(string s, TextWriter output) { if (output == null) @@ -545,6 +548,7 @@ namespace RestSharp.Contrib #endif } } + */ public static string HtmlEncode(string s) { @@ -566,6 +570,7 @@ namespace RestSharp.Contrib /// /// The string to encode. /// The TextWriter output stream containing the encoded string. + /* public static void HtmlEncode(string s, TextWriter output) { if (output == null) @@ -586,6 +591,8 @@ namespace RestSharp.Contrib #endif } } + */ + #if NET_4_0 public static string HtmlEncode (object value) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index d67ad68b2..f98cdc0dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -1,7 +1,7 @@ #if DEBUG using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Text; using System.Text.RegularExpressions; using System.Linq; @@ -41,7 +41,7 @@ namespace Barotrauma if (Directory.Exists(conversationsPath + $"/{languageNoWhitespace}")) { - string[] conversationFileArray = Directory.GetFiles(conversationsPath + $"/{languageNoWhitespace}", "*.csv", SearchOption.AllDirectories); + IEnumerable conversationFileArray = Directory.GetFiles(conversationsPath + $"/{languageNoWhitespace}", "*.csv", System.IO.SearchOption.AllDirectories); if (conversationFileArray != null) { @@ -58,7 +58,7 @@ namespace Barotrauma if (Directory.Exists(infoTextPath + $"/{languageNoWhitespace}")) { - string[] infoTextFileArray = Directory.GetFiles(infoTextPath + $"/{languageNoWhitespace}", "*.csv", SearchOption.AllDirectories); + IEnumerable infoTextFileArray = Directory.GetFiles(infoTextPath + $"/{languageNoWhitespace}", "*.csv", System.IO.SearchOption.AllDirectories); if (infoTextFileArray != null) { @@ -145,7 +145,7 @@ namespace Barotrauma } else if (split[0].Contains(".") && !split[0].Any(char.IsUpper)) // An empty field { - xmlContent.Add($"<{split[0]}>"); + xmlContent.Add($"<{split[0]}>"); } else // A header { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs index 05e0e93dc..a921f129e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs @@ -1,7 +1,9 @@ +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.IO; +using Barotrauma.IO; using System.Threading.Tasks; +using Lidgren.Network; using Color = Microsoft.Xna.Framework.Color; namespace Barotrauma @@ -34,36 +36,186 @@ namespace Barotrauma }); } - public static Texture2D FromFile(string path, bool mipmap=false) + private static byte[] CompressDxt5(byte[] data, int width, int height) { - path = path.CleanUpPath(); - try + using (System.IO.MemoryStream mstream = new System.IO.MemoryStream()) { - using (Stream fileStream = File.OpenRead(path)) + for (int y = 0; y < height; y += 4) { - return FromStream(fileStream, path, mipmap); + for (int x = 0; x < width; x += 4) + { + int offset = x * 4 + y * 4 * width; + CompressDxt5Block(data, offset, width, mstream); + } } - - } - catch (Exception e) - { - DebugConsole.ThrowError("Loading texture \"" + path + "\" failed!", e); - return null; + return mstream.ToArray(); } } - public static Texture2D FromStream(Stream fileStream, string path=null, bool mipmap=false) + private static void CompressDxt5Block(byte[] data, int offset, int width, System.IO.Stream output) + { + int r1 = 255, g1 = 255, b1 = 255, a1 = 255; + int r2 = 0, g2 = 0, b2 = 0, a2 = 0; + + //determine the two colors to interpolate between: + //color 1 represents lowest luma, color 2 represents highest luma + for (int i = 0; i < 16; i++) + { + int pixelOffset = offset + (4 * ((i % 4) + (width * (i >> 2)))); + int r, g, b, a; + r = data[pixelOffset + 0]; + g = data[pixelOffset + 1]; + b = data[pixelOffset + 2]; + a = data[pixelOffset + 3]; + if (r * 299 + g * 587 + b * 114 < r1 * 299 + g1 * 587 + b1 * 114) + { + r1 = r; g1 = g; b1 = b; + } + if (r * 299 + g * 587 + b * 114 > r2 * 299 + g2 * 587 + b2 * 114) + { + r2 = r; g2 = g; b2 = b; + } + if (a < a1) { a1 = a; } + if (a > a2) { a2 = a; } + } + + //convert the colors to rgb565 (16-bit rgb) + int r1_565 = (r1 * 0x1f) / 0xff; if (r1_565 > 0x1f) { r1_565 = 0x1f; } + int g1_565 = (g1 * 0x3f) / 0xff; if (g1_565 > 0x3f) { g1_565 = 0x3f; } + int b1_565 = (b1 * 0x1f) / 0xff; if (b1_565 > 0x1f) { b1_565 = 0x1f; } + + int r2_565 = (r2 * 0x1f) / 0xff; if (r2_565 > 0x1f) { r2_565 = 0x1f; } + int g2_565 = (g2 * 0x3f) / 0xff; if (g2_565 > 0x3f) { g2_565 = 0x3f; } + int b2_565 = (b2 * 0x1f) / 0xff; if (b2_565 > 0x1f) { b2_565 = 0x1f; } + + //luma is also used to determine which color on the palette + //most closely resembles each pixel to compress, so we + //calculate this here + int y1 = r1 * 299 + g1 * 587 + b1 * 114; + int y2 = r2 * 299 + g2 * 587 + b2 * 114; + + byte[] newData = new byte[16]; + for (int i = 0; i < 16; i++) + { + int pixelOffset = offset + (4 * ((i % 4) + (width * (i >> 2)))); + int r, g, b, a; + r = data[pixelOffset + 0]; + g = data[pixelOffset + 1]; + b = data[pixelOffset + 2]; + a = data[pixelOffset + 3]; + + if (a1 < a2) + { + a -= a1; + a = (a * 0x7) / (a2 - a1); + if (a > 0x7) { a = 0x7; } + + switch (a) + { + case 0: + a = 1; + break; + case 1: + a = 7; + break; + case 2: + a = 6; + break; + case 3: + a = 5; + break; + case 4: + a = 4; + break; + case 5: + a = 3; + break; + case 6: + a = 2; + break; + case 7: + a = 0; + break; + } + } + else + { + a = 0; + } + + NetBitWriter.WriteUInt32((uint)a, 3, newData, 16 + (i * 3)); + + int y = r * 299 + g * 587 + b * 114; + + int max = y2 - y1; + int diffY = y - y1; + + int paletteIndex; + if (diffY < max / 4) + { + paletteIndex = 0; + } + else if (diffY < max / 2) + { + paletteIndex = 2; + } + else if (diffY < max * 3 / 4) + { + paletteIndex = 3; + } + else + { + paletteIndex = 1; + } + newData[12 + (i / 4)] |= (byte)(paletteIndex << (2 * (i % 4))); + } + + newData[0] = (byte)a2; + newData[1] = (byte)a1; + + newData[9] = (byte)((r1_565 << 3) | (g1_565 >> 3)); + newData[8] = (byte)((g1_565 << 5) | b1_565); + newData[11] = (byte)((r2_565 << 3) | (g2_565 >> 3)); + newData[10] = (byte)((g2_565 << 5) | b2_565); + + output.Write(newData, 0, 16); + } + + public static Texture2D FromFile(string path, bool compress = true, bool mipmap = false) + { + using (FileStream fileStream = File.OpenRead(path)) + { + return FromStream(fileStream, path, compress, mipmap); + } + } + + public static Texture2D FromStream(System.IO.Stream stream, string path = null, bool compress = true, bool mipmap = false) { try { - int width = 0; int height = 0; int channels = 0; + path = path.CleanUpPath(); byte[] textureData = null; - textureData = Texture2D.TextureDataFromStream(fileStream, out width, out height, out channels); + textureData = Texture2D.TextureDataFromStream(stream, out int width, out int height, out int channels); + + SurfaceFormat format = SurfaceFormat.Color; + if (GameMain.Config.TextureCompressionEnabled && compress) + { + if (((width & 0x03) == 0) && ((height & 0x03) == 0)) + { + textureData = CompressDxt5(textureData, width, height); + format = SurfaceFormat.Dxt5; + mipmap = false; + } + else + { + DebugConsole.NewMessage($"Could not compress a texture because the dimensions aren't a multiple of 4 (path: {path ?? "null"}, size: {width}x{height})", Color.Orange); + } + } Texture2D tex = null; CrossThread.RequestExecutionOnMainThread(() => { - tex = new Texture2D(_graphicsDevice, width, height, mipmap, SurfaceFormat.Color); + tex = new Texture2D(_graphicsDevice, width, height, mipmap, format); tex.SetData(textureData); }); return tex; @@ -74,7 +226,8 @@ namespace Barotrauma if (e is SharpDX.SharpDXException) { throw; } #endif - DebugConsole.ThrowError("Loading texture from stream failed!", e); + DebugConsole.ThrowError(string.IsNullOrEmpty(path) ? "Loading texture from stream failed!" : + "Loading texture \"" + path + "\" failed!", e); return null; } } diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb index f01c765ee132b5679693dce98dc41dac8bcfd521..e40446c86289399c960ebfb2bdf7c878daa7697a 100644 GIT binary patch delta 153 zcmV;K0A~M}K+Qx9SWZHB1prMV0004ckqk@-HZCwOF*Y(akzXthy&(VqO-Dvp2m!?H z>mQMsAOQriy4nsI0Q>;}07O_qLwX6@<&*(7m^sz_#y0y}qLI^%0soU?0i6Q&50e1` z!?TnRLJ$E1lh+?WD;z{+W^ZzBQ)ppiWpYz*Y-waxWpZ?7ctdY&Z*l+u1yEsgb1?t` H0{{R36xA}_ delta 66 zcmV-I0KNasM3+DeSWZHB1ppTy0004bkqk@+H!d+QF*rGqU@Q)Z9{>PNM@Co(0n3wb YY>}EE0RyqS+77dy5ke3F0+Zq)Ktu!**8l(j diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb index abe594f0174e0ef435416cb1d5c9c18660c4f60d..507c1f1e38c66a1e07e0fa2baf77e564d6dbae58 100644 GIT binary patch delta 138 zcmbQ@wA_^^!p|v%m0`IO0|R5#L>?awGd%-6Lo*}OiSaso;y_Vfcee-*hKbFW-%gzA z$il+l&p6qENowPdRs75hAyBqDm`6!p|v%l|e+2fq}7dB99Ndxt^h(p@rqd1RcIs1qKFRcee-*hUoSfm5H+) jS(q9887Dh3No_tLpw75ifvK5qGnbH|03*|68zlz-?U)jo diff --git a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb index 33684d3939b97e13271adcd0d89e54417c4c5ce2..469f0e2c45ccd5ccf8023b6b9db2228c53d696a2 100644 GIT binary patch delta 344 zcmcc2_g9D~!q2Ikm7$iCfq}7VB99M;nVx~3p_!5C#CRR4w;T)%zV2=j9E`UO9V{3b z6d0Hofa(Mom^UsJVXFVb$iUzd;pAM!dHd-UMza}~SN|L{`yI6iD9iwoV*%0zK)eHp zLxB7fKGXW0PcgD?Zr|Njv{N(00OlOUD`10#b9ln}yp1W1g^_u(EW4zQKyZFeW=dFUQAuirbAC>K5d$M@ aKw@#RA;W*5jSPHXX|RM-PAQTk$e{q4GE27r delta 100 zcmew>beWGQ!q2Ikl_8j&fq}7dB99Ndxt^h(p@rqd1Rbd(Yzz#(?rsqrjG_xe7#SH9 z7?>D<$^;mgHZBuk+I)m1nMIt5D>y$VGsQVSC%=e+ku@N(xY&^4Kf~l$P8k+PplSep C5f)nj diff --git a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb index d27af47815f4dc9ccb134f3c24d3e053c821fa13..ab1d6c89721aafb3490a58e8b20294c58bef369a 100644 GIT binary patch delta 251 zcmcc3{fLhz!p|v%l_80pfq}7VB99M;nVx~3p_!5C#CRRPOKc1bzV2=j91Pn#*KeLU z(}!?+=jY@XS@Uu!r=+H3=B0w< z%M29C5OR~}G0RWRVOHZYR45Bct#HoIFG?|(yqj5S@<&DqM#ITmOp4NF20#;PdAWGG z7#V0zF)*?QBo-GNGW-WA7XV9yr52Td Sj0ZXgAH>(T_$Rq%TE=ZaH delta 102 zcmaFFcbl6h!p|v%mEkrk0|R5_L>?b@b3H>nLkr7^2|9c}KtW%3w+Idf!S|Xr6K5GQ zGHpCt$;iaaJn=%-W=WRMOpHvEdD$gp`GWIvGEBarotrauma FakeFish, Undertow Games Barotrauma - 0.9.9.1 + 0.9.10.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index b86c42530..ebad2035d 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.9.1 + 0.9.10.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/Shaders/deformshader.fx b/Barotrauma/BarotraumaClient/Shaders/deformshader.fx index f7db6f974..d48e7cded 100644 --- a/Barotrauma/BarotraumaClient/Shaders/deformshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/deformshader.fx @@ -75,6 +75,11 @@ float4 mainPS(VertexShaderOutput input) : COLOR return xTexture.Sample(TextureSampler, input.TexCoords) * input.Color; } +float4 solidVertexColorPS(VertexShaderOutput input) : COLOR +{ + return input.Color * xTexture.Sample(TextureSampler, input.TexCoords).a; +} + float4 solidColorPS(VertexShaderOutput input) : COLOR { return solidColor * xTexture.Sample(TextureSampler, input.TexCoords).a; @@ -96,4 +101,13 @@ technique DeformShaderSolidColor VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 solidColorPS(); } +} + +technique DeformShaderSolidVertexColor +{ + pass Pass1 + { + VertexShader = compile vs_4_0_level_9_1 mainVS(); + PixelShader = compile ps_4_0_level_9_1 solidVertexColorPS(); + } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx index 909d1f89e..1c151ad17 100644 --- a/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx @@ -75,6 +75,11 @@ float4 mainPS(VertexShaderOutput input) : COLOR return xTexture.Sample(TextureSampler, input.TexCoords) * input.Color; } +float4 solidVertexColorPS(VertexShaderOutput input) : COLOR +{ + return input.Color * xTexture.Sample(TextureSampler, input.TexCoords).a; +} + float4 solidColorPS(VertexShaderOutput input) : COLOR { return solidColor * xTexture.Sample(TextureSampler, input.TexCoords).a; @@ -96,4 +101,13 @@ technique DeformShaderSolidColor VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 solidColorPS(); } +} + +technique DeformShaderSolidVertexColor +{ + pass Pass1 + { + VertexShader = compile vs_3_0 mainVS(); + PixelShader = compile ps_3_0 solidVertexColorPS(); + } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx b/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx index f1fc3c52b..464851d70 100644 --- a/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx +++ b/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx @@ -10,6 +10,12 @@ float4 solidColor(float4 position : POSITION0, float4 clr : COLOR0, float2 texCo return color * a; } +float4 solidVertexColor(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float a = tex2D(TextureSampler, texCoord).a; + return clr * a; +} + float4 solidColorBlur(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float sample; @@ -22,13 +28,20 @@ float4 solidColorBlur(float4 position : POSITION0, float4 clr : COLOR0, float2 t return color * sample; } -technique SolidColor +technique SolidColor { pass Pass1 { PixelShader = compile ps_4_0_level_9_1 solidColor(); } } +technique SolidVertexColor +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 solidVertexColor(); + } +} technique SolidColorBlur { pass Pass1 diff --git a/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx index f62aeda9e..40ad68196 100644 --- a/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx @@ -1,4 +1,4 @@ -Texture xTexture; +Texture2D xTexture; sampler TextureSampler = sampler_state { Texture = ; }; float blurDistance; @@ -10,6 +10,12 @@ float4 solidColor(float4 position : POSITION0, float4 clr : COLOR0, float2 texCo return color * a; } +float4 solidVertexColor(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float a = tex2D(TextureSampler, texCoord).a; + return clr * a; +} + float4 solidColorBlur(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float sample; @@ -22,13 +28,20 @@ float4 solidColorBlur(float4 position : POSITION0, float4 clr : COLOR0, float2 t return color * sample; } -technique SolidColor +technique SolidColor { pass Pass1 { PixelShader = compile ps_3_0 solidColor(); } } +technique SolidVertexColor +{ + pass Pass1 + { + PixelShader = compile ps_3_0 solidVertexColor(); + } +} technique SolidColorBlur { pass Pass1 diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 8fc87ca50..2f9b13cd3 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.9.1 + 0.9.10.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -25,6 +25,9 @@ TRACE;DEBUG;CLIENT;WINDOWS;X64;USE_STEAM x64 ..\bin\$(Configuration)Windows\ + full + true + Auto @@ -43,12 +46,16 @@ TRACE;CLIENT;WINDOWS;X64;USE_STEAM x64 ..\bin\$(Configuration)Windows\ + full + true TRACE;CLIENT;WINDOWS;X64;USE_STEAM x64 ..\bin\$(Configuration)Windows\ + full + true diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 333809549..aa65697c0 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.9.1 + 0.9.10.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index acda0390e..c30bd01aa 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.9.1 + 0.9.10.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index f6c374049..390b4cb98 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -416,6 +416,7 @@ namespace Barotrauma } } + private List severedJointIndices = new List(); private void WriteStatus(IWriteMessage msg) { msg.Write(IsDead); @@ -426,33 +427,32 @@ namespace Barotrauma { msg.Write(CauseOfDeath.Affliction.Identifier); } - - if (AnimController?.LimbJoints == null) - { - //0 limbs severed - msg.Write((byte)0); - } - else - { - List severedJointIndices = new List(); - for (int i = 0; i < AnimController.LimbJoints.Length; i++) - { - if (AnimController.LimbJoints[i] != null && AnimController.LimbJoints[i].IsSevered) - { - severedJointIndices.Add(i); - } - } - msg.Write((byte)severedJointIndices.Count); - foreach (int jointIndex in severedJointIndices) - { - msg.Write((byte)jointIndex); - } - } } else { CharacterHealth.ServerWrite(msg); } + if (AnimController?.LimbJoints == null) + { + //0 limbs severed + msg.Write((byte)0); + } + else + { + severedJointIndices.Clear(); + for (int i = 0; i < AnimController.LimbJoints.Length; i++) + { + if (AnimController.LimbJoints[i] != null && AnimController.LimbJoints[i].IsSevered) + { + severedJointIndices.Add(i); + } + } + msg.Write((byte)severedJointIndices.Count); + foreach (int jointIndex in severedJointIndices) + { + msg.Write((byte)jointIndex); + } + } } public void WriteSpawnData(IWriteMessage msg, UInt16 entityId, bool restrictMessageSize) diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 58d82a5c9..688831efc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -7,7 +7,7 @@ using System.ComponentModel; using FarseerPhysics; using Barotrauma.Items.Components; using System.Threading; -using System.IO; +using Barotrauma.IO; using System.Text; using System.Diagnostics; @@ -487,7 +487,7 @@ namespace Barotrauma if (GameMain.Server == null) return; if (args.Length < 1) { - NewMessage("giveperm [id]: Grants administrative permissions to the player with the specified client ID.", Color.Cyan); + NewMessage("giveperm [id/steamid/endpoint/name]: Grants administrative permissions to the player with the specified client.", Color.Cyan); return; } @@ -522,7 +522,7 @@ namespace Barotrauma if (GameMain.Server == null) return; if (args.Length < 1) { - NewMessage("revokeperm [id]: Revokes administrative permissions to the player with the specified client ID.", Color.Cyan); + NewMessage("revokeperm [id/steamid/endpoint/name]: Revokes administrative permissions to the player with the specified client.", Color.Cyan); return; } @@ -604,7 +604,7 @@ namespace Barotrauma if (GameMain.Server == null) return; if (args.Length < 1) { - NewMessage("givecommandperm [id]: Gives the player with the specified client ID the permission to use the specified console commands.", Color.Cyan); + NewMessage("givecommandperm [id/steamid/endpoint/name]: Gives the specified client the permission to use the specified console commands.", Color.Cyan); return; } @@ -615,28 +615,45 @@ namespace Barotrauma return; } - ShowQuestionPrompt("Console command permissions to grant to \"" + client.Name + "\"? You may enter multiple commands separated with a space.", (commandsStr) => + ShowQuestionPrompt("Console command permissions to grant to \"" + client.Name + "\"? You may enter multiple commands separated with a space, or \"all\" to allow using any console command.", (commandsStr) => { string[] splitCommands = commandsStr.Split(' '); + bool giveAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + List grantedCommands = new List(); - for (int i = 0; i < splitCommands.Length; i++) + if (giveAll) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); - if (matchingCommand == null) + grantedCommands.AddRange(commands); + } + else + { + for (int i = 0; i < splitCommands.Length; i++) { - ThrowError("Could not find the command \"" + splitCommands[i] + "\"!"); - } - else - { - grantedCommands.Add(matchingCommand); + splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); + Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + if (matchingCommand == null) + { + ThrowError("Could not find the command \"" + splitCommands[i] + "\"!"); + } + else + { + grantedCommands.Add(matchingCommand); + } } } client.GivePermission(ClientPermissions.ConsoleCommands); client.SetPermissions(client.Permissions, client.PermittedConsoleCommands.Union(grantedCommands).Distinct().ToList()); GameMain.Server.UpdateClientPermissions(client); - NewMessage("Gave the client \"" + client.Name + "\" the permission to use console commands " + string.Join(", ", grantedCommands.Select(c => c.names[0])) + ".", Color.White); + if (giveAll) + { + NewMessage("Gave the client \"" + client.Name + "\" the permission to use all console commands.", Color.White); + } + else if (grantedCommands.Count > 0) + { + NewMessage("Gave the client \"" + client.Name + "\" the permission to use console commands " + string.Join(", ", grantedCommands.Select(c => c.names[0])) + ".", Color.White); + } + }, args, 1); }); @@ -645,7 +662,7 @@ namespace Barotrauma if (GameMain.Server == null) return; if (args.Length < 1) { - NewMessage("revokecommandperm [id]: Revokes permission to use the specified console commands from the player with the specified client ID.", Color.Cyan); + NewMessage("revokecommandperm [id/steamid/endpoint/name]: Revokes permission to use the specified console commands from the specified client.", Color.Cyan); return; } @@ -665,23 +682,39 @@ namespace Barotrauma { string[] splitCommands = commandsStr.Split(' '); List revokedCommands = new List(); - for (int i = 0; i < splitCommands.Length; i++) + bool revokeAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + if (revokeAll) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); - if (matchingCommand == null) - { - ThrowError("Could not find the command \"" + splitCommands[i] + "\"!"); - } - else - { - revokedCommands.Add(matchingCommand); - } + revokedCommands.AddRange(commands); } + else + { + for (int i = 0; i < splitCommands.Length; i++) + { + splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); + Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + if (matchingCommand == null) + { + ThrowError("Could not find the command \"" + splitCommands[i] + "\"!"); + } + else + { + revokedCommands.Add(matchingCommand); + } + } + } client.SetPermissions(client.Permissions, client.PermittedConsoleCommands.Except(revokedCommands).ToList()); GameMain.Server.UpdateClientPermissions(client); - NewMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", Color.White); + if (revokeAll) + { + NewMessage("Revoked \"" + client.Name + "\"'s permission to use console commands.", Color.White); + } + else if (revokedCommands.Any()) + { + NewMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", Color.White); + } + }, args, 1); }); @@ -690,7 +723,7 @@ namespace Barotrauma if (GameMain.Server == null) return; if (args.Length < 1) { - NewMessage("showperm [id]: Shows the current administrative permissions of the client with the specified client ID.", Color.Cyan); + NewMessage("showperm [id/steamid/endpoint/name]: Shows the current administrative permissions of the specified client.", Color.Cyan); return; } @@ -1791,7 +1824,7 @@ namespace Barotrauma var client = FindClient(args[0]); if (client == null) { - ThrowError("Client \"" + args[0] + "\" not found."); + GameMain.Server.SendConsoleMessage("Client \"" + args[0] + "\" not found.", senderClient); return; } if (client.Connection == GameMain.Server.OwnerConnection) @@ -1800,27 +1833,42 @@ namespace Barotrauma return; } - string[] splitCommands = args.Skip(1).ToArray(); List grantedCommands = new List(); - for (int i = 0; i < splitCommands.Length; i++) + string[] splitCommands = args.Skip(1).ToArray(); + bool giveAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + if (giveAll) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); - if (matchingCommand == null) + grantedCommands.AddRange(commands); + } + else + { + for (int i = 0; i < splitCommands.Length; i++) { - GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient); - } - else - { - grantedCommands.Add(matchingCommand); + splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); + Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + if (matchingCommand == null) + { + GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient); + } + else + { + grantedCommands.Add(matchingCommand); + } } } client.GivePermission(ClientPermissions.ConsoleCommands); client.SetPermissions(client.Permissions, client.PermittedConsoleCommands.Union(grantedCommands).Distinct().ToList()); + GameMain.Server.UpdateClientPermissions(client); - GameMain.Server.SendConsoleMessage("Gave the client \"" + client.Name + "\" the permission to use the console commands " + string.Join(", ", grantedCommands.Select(c => c.names[0])) + ".", senderClient); - NewMessage("Gave the client \"" + client.Name + "\" the permission to use the console commands " + string.Join(", ", grantedCommands.Select(c => c.names[0])) + ".", Color.White); + if (giveAll) + { + GameMain.Server.SendConsoleMessage("Gave the client \"" + client.Name + "\" the permission to use all console commands.", senderClient); + } + else if (grantedCommands.Count > 0) + { + GameMain.Server.SendConsoleMessage("Gave the client \"" + client.Name + "\" the permission to use console commands " + string.Join(", ", grantedCommands.Select(c => c.names[0])) + ".", senderClient); + } } ); @@ -1828,7 +1876,7 @@ namespace Barotrauma "revokecommandperm", (Client senderClient, Vector2 cursorWorldPos, string[] args) => { - if (args.Length < 2) return; + if (args.Length < 2) { return; } var client = FindClient(args[0]); if (client == null) @@ -1841,28 +1889,43 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("Cannot revoke command permissions from the server owner!", senderClient); return; } - - string[] splitCommands = args.Skip(1).ToArray(); List revokedCommands = new List(); - for (int i = 0; i < splitCommands.Length; i++) + string[] splitCommands = args.Skip(1).ToArray(); + bool revokeAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + if (revokeAll) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); - if (matchingCommand == null) + revokedCommands.AddRange(commands); + client.RemovePermission(ClientPermissions.ConsoleCommands); + } + else + { + for (int i = 0; i < splitCommands.Length; i++) { - GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient); - } - else - { - revokedCommands.Add(matchingCommand); + splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); + Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + if (matchingCommand == null) + { + GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient); + } + else + { + revokedCommands.Add(matchingCommand); + } } + client.GivePermission(ClientPermissions.ConsoleCommands); } - client.GivePermission(ClientPermissions.ConsoleCommands); client.SetPermissions(client.Permissions, client.PermittedConsoleCommands.Except(revokedCommands).ToList()); GameMain.Server.UpdateClientPermissions(client); GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", senderClient); - NewMessage(senderClient.Name + " revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", Color.White); + if (revokeAll) + { + GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use console commands.", senderClient); + } + else if (revokedCommands.Count > 0) + { + GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", senderClient); + } } ); @@ -1941,6 +2004,27 @@ namespace Barotrauma } ); + AssignOnClientRequestExecute( + "money", + (Client senderClient, Vector2 cursorWorldPos, string[] args) => + { + if (args.Length == 0) { return; } + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)) + { + GameMain.Server.SendConsoleMessage("No campaign active!", senderClient); + return; + } + if (int.TryParse(args[0], out int money)) + { + campaign.Money += money; + campaign.LastUpdateID++; + } + else + { + GameMain.Server.SendConsoleMessage($"\"{args[0]}\" is not a valid numeric value.", senderClient); + } + } + ); AssignOnClientRequestExecute( "campaigndestination|setcampaigndestination", (Client senderClient, Vector2 cursorWorldPos, string[] args) => diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs index 9ee80f0f8..7909a789e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs @@ -9,7 +9,9 @@ namespace Barotrauma msg.Write((ushort)items.Count); foreach (Item item in items) { - item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? 0); + item.WriteSpawnData(msg, + itemIDs[item], + parentInventoryIDs.ContainsKey(item) ? parentInventoryIDs[item] : Entity.NullEntityID); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 1ceed8427..d9c626d8d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using System; using System.Collections.Generic; namespace Barotrauma @@ -7,6 +8,9 @@ namespace Barotrauma { private bool usedExistingItem; + private UInt16 originalItemID; + private UInt16 originalInventoryID; + private readonly List> executedEffectIndices = new List>(); public override void ServerWriteInitial(IWriteMessage msg, Client c) @@ -14,11 +18,11 @@ namespace Barotrauma msg.Write(usedExistingItem); if (usedExistingItem) { - msg.Write(item.ID); + msg.Write(originalItemID); } else { - item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? 0); + item.WriteSpawnData(msg, originalItemID, originalInventoryID); } msg.Write((byte)executedEffectIndices.Count); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 3ce575eb9..008a79402 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -6,7 +6,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Reflection; using System.Threading; @@ -105,6 +105,8 @@ namespace Barotrauma MapGenerationParams.Init(); LevelGenerationParams.LoadPresets(); ScriptedEventSet.LoadPrefabs(); + Order.Init(); + EventManagerSettings.Init(); AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); @@ -180,7 +182,7 @@ namespace Barotrauma bool enableUpnp = false; int maxPlayers = 10; - int ownerKey = 0; + int? ownerKey = null; UInt64 steamId = 0; XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); @@ -197,7 +199,7 @@ namespace Barotrauma password = doc.Root.GetAttributeString("password", ""); enableUpnp = doc.Root.GetAttributeBool("enableupnp", false); maxPlayers = doc.Root.GetAttributeInt("maxplayers", 10); - ownerKey = 0; + ownerKey = null; } #if DEBUG @@ -244,7 +246,10 @@ namespace Barotrauma i++; break; case "-ownerkey": - int.TryParse(CommandLineArgs[i + 1], out ownerKey); + if (int.TryParse(CommandLineArgs[i + 1], out int key)) + { + ownerKey = key; + } i++; break; case "-steamid": diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 0d321ad08..d731a30bd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.IO; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -306,7 +307,7 @@ namespace Barotrauma } try { - characterDataDoc.Save(characterDataPath); + characterDataDoc.SaveSafe(characterDataPath); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs index 08c4eed64..7072e5b2b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components isOpen = open; //opening a partially stuck door makes it less stuck - if (isOpen) stuck = MathHelper.Clamp(stuck - 30.0f, 0.0f, 100.0f); + if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } if (sendNetworkMessage) { @@ -28,6 +28,7 @@ namespace Barotrauma.Items.Components base.ServerWrite(msg, c, extraData); msg.Write(isOpen); + msg.Write(isBroken); msg.Write(extraData.Length == 3 ? (bool)extraData[2] : false); //forced open msg.WriteRangedSingle(stuck, 0.0f, 100.0f, 8); msg.Write(lastUser == null ? (UInt16)0 : lastUser.ID); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs index 4b6f10240..90d4be060 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs @@ -13,6 +13,7 @@ namespace Barotrauma.Items.Components msg.Write(Attached); msg.Write(body.SimPosition.X); msg.Write(body.SimPosition.Y); + msg.Write(item.Submarine?.ID ?? Entity.NullEntityID); } public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) @@ -26,7 +27,7 @@ namespace Barotrauma.Items.Components simPosition = c.Character.SimPosition + offset; Drop(false, null); - item.SetTransform(simPosition, 0.0f); + item.SetTransform(simPosition, 0.0f, findNewHull: false); AttachToWall(); item.CreateServerEvent(this); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs index 7bdbac370..5de5d4360 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components { public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - msg.Write(state); + msg.Write(State); msg.Write(user == null ? (ushort)0 : user.ID); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs index 58c896d2e..4d15cc9e2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs @@ -24,7 +24,7 @@ namespace Barotrauma.Items.Components } CustomInterfaceElement clickedButton = null; - if (item.CanClientAccess(c)) + if ((c.Character != null && DrawHudWhenEquipped && item.ParentInventory?.Owner == c.Character) || item.CanClientAccess(c)) { for (int i = 0; i < customInterfaceElementList.Count; i++) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 0574ec775..6695c336d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -101,6 +101,9 @@ namespace Barotrauma if (!prevItems.Contains(item) && !item.CanClientAccess(c)) { +#if DEBUG || UNSTABLE + DebugConsole.NewMessage($"Client {c.Name} failed to pick up item \"{item}\" (parent inventory: {(item.ParentInventory?.Owner.ToString() ?? "null")}). No access.", Color.Yellow); +#endif if (item.body != null && !c.PendingPositionUpdates.Contains(item)) { c.PendingPositionUpdates.Enqueue(item); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 216026bfc..e02c6ba3f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Net; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index e2468f5cc..08279cba7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -30,7 +30,8 @@ namespace Barotrauma.Networking if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) { - DebugConsole.ThrowError("Invalid order message from client \"" + c.Name + "\" - order index out of bounds."); + DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order index out of bounds ({orderIndex}, {orderOptionIndex})."); + if (NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { c.LastSentChatMsgID = ID; } return; } @@ -44,7 +45,7 @@ namespace Barotrauma.Networking txt = msg.ReadString() ?? ""; } - if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) return; + if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { return; } c.LastSentChatMsgID = ID; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs index 1a1bbb96b..6226d0b2c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.IO.Pipes; using System.Text; using System.Threading; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index 74704c129..c2746718e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading; @@ -93,7 +93,7 @@ namespace Barotrauma.Networking { data = File.ReadAllBytes(filePath); } - catch (IOException e) + catch (System.IO.IOException e) { if (i >= maxRetries) { throw; } DebugConsole.NewMessage("Failed to initiate a file transfer {" + e.Message + "}, retrying in 250 ms...", Color.Red); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 8ebab1945..ee5a57d39 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -8,7 +8,7 @@ using System.Diagnostics; using System.Linq; using System.Text; using System.IO.Compression; -using System.IO; +using Barotrauma.IO; using Barotrauma.Steam; using System.Xml.Linq; using System.Threading; @@ -1917,6 +1917,15 @@ namespace Barotrauma.Networking var teamID = n == 0 ? Character.TeamType.Team1 : Character.TeamType.Team2; Submarine.MainSubs[n].TeamID = teamID; + foreach (Item item in Item.ItemList) + { + if (item.Submarine == null) { continue; } + if (item.Submarine != Submarine.MainSubs[n] && !Submarine.MainSubs[n].DockedTo.Contains(item.Submarine)) { continue; } + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = Submarine.MainSubs[n].TeamID; + } + } foreach (Submarine sub in Submarine.MainSubs[n].DockedTo) { sub.TeamID = teamID; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index c8b48a8dd..1f74943b3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -344,13 +344,21 @@ namespace Barotrauma.Networking if (!isCompatibleVersion) { RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, - $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={version}"); + $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={version}"); GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); DebugConsole.NewMessage(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); return; } + Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), name.ToLower())); + if (nameTaken != null) + { + RemovePendingClient(pendingClient, DisconnectReason.NameTaken, ""); + GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); + return; + } + int contentPackageCount = inc.ReadVariableInt32(); List clientContentPackages = new List(); for (int i = 0; i < contentPackageCount; i++) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 41b828eff..62898a9c2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -306,13 +306,21 @@ namespace Barotrauma.Networking if (!isCompatibleVersion) { RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, - $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={version}"); + $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={version}"); GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); DebugConsole.NewMessage(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); return; } + Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), name.ToLower())); + if (nameTaken != null) + { + RemovePendingClient(pendingClient, DisconnectReason.NameTaken, ""); + GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); + return; + } + int contentPackageCount = (int)inc.ReadVariableUInt32(); List clientContentPackages = new List(); for (int i = 0; i < contentPackageCount; i++) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index b2997a2f1..1493e987b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -1,10 +1,9 @@ -using Microsoft.Xna.Framework; +using Barotrauma.IO; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; -using System.Xml; using System.Xml.Linq; namespace Barotrauma.Networking @@ -204,7 +203,7 @@ namespace Barotrauma.Networking SerializableProperty.SerializeProperties(this, doc.Root, true); - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true @@ -212,7 +211,7 @@ namespace Barotrauma.Networking using (var writer = XmlWriter.Create(SettingsFile, settings)) { - doc.Save(writer); + doc.SaveSafe(writer); } if (KarmaPreset == "custom") @@ -521,13 +520,13 @@ namespace Barotrauma.Networking try { - XmlWriterSettings settings = new XmlWriterSettings(); + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings(); settings.Indent = true; settings.NewLineOnAttributes = true; using (var writer = XmlWriter.Create(ClientPermissionsFile, settings)) { - doc.Save(writer); + doc.SaveSafe(writer); } } catch (Exception e) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs index 34d605887..aaaa0894f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Net; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 561146d66..d2aa25c0e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -3,7 +3,7 @@ using Barotrauma.Steam; using GameAnalyticsSDK.Net; using System; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Threading; @@ -85,8 +85,6 @@ namespace Barotrauma filePath = Path.GetFileNameWithoutExtension(originalFilePath) + " (" + (existingFiles + 1) + ")" + Path.GetExtension(originalFilePath); } - StreamWriter sw = new StreamWriter(filePath); - StringBuilder sb = new StringBuilder(); sb.AppendLine("Barotrauma Dedicated Server crash report (generated on " + DateTime.Now + ")"); sb.AppendLine("\n"); @@ -142,8 +140,7 @@ namespace Barotrauma Console.ForegroundColor = ConsoleColor.Red; Console.Write(crashReport); - sw.WriteLine(sb.ToString()); - sw.Close(); + File.WriteAllText(filePath,sb.ToString()); if (GameSettings.SendUserStatistics) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs index 551158b07..17e4ae552 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs @@ -38,7 +38,9 @@ namespace Barotrauma if (item.Submarine == null) { - if (!(item.ParentInventory?.Owner is Character)) { continue; } + //items outside the sub don't count as destroyed if they're still in the traitor's inventory + bool carriedByTraitor = Traitors.Any(traitor => item.IsOwnedBy(traitor.Character)); + if (!carriedByTraitor) { continue; } } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs index 5b26a8573..d51c0f9da 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs @@ -5,7 +5,7 @@ using System; using Barotrauma.Networking; using Lidgren.Network; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Security.Cryptography; using Barotrauma.Extensions; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 3d56e324e..6ac37ca59 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.9.1 + 0.9.10.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -24,6 +24,8 @@ TRACE;DEBUG;SERVER;WINDOWS;X64;USE_STEAM x64 ..\bin\$(Configuration)Windows\ + full + true @@ -42,12 +44,16 @@ TRACE;SERVER;WINDOWS;X64;USE_STEAM x64 ..\bin\$(Configuration)Windows\ + full + true TRACE;SERVER;WINDOWS;X64;USE_STEAM x64 ..\bin\$(Configuration)Windows\ + full + true diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 62415487a..454583cdf 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -53,6 +53,7 @@ + @@ -75,6 +76,8 @@ + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index c60855d1a..b0bdc41e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Barotrauma { - public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive } + public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect } abstract partial class AIController : ISteerable { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 4116a18e1..53c08c77e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -40,6 +40,7 @@ namespace Barotrauma private readonly float avoidLookAheadDistance; + private IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; private SteeringManager outsideSteering, insideSteering; private float updateTargetsTimer; @@ -85,8 +86,9 @@ namespace Barotrauma private readonly float colliderWidth; private readonly float colliderLength; private readonly int requiredHoleCount; - private readonly bool canAttackSub; - private readonly bool canAttackCharacters; + private bool canAttackWalls; + private bool canAttackDoors; + private bool canAttackCharacters; private readonly float priorityFearIncreasement = 2; private readonly float memoryFadeTime = 0.5f; @@ -94,9 +96,13 @@ namespace Barotrauma private float avoidTimer; + public bool StayInsideLevel = true; + public LatchOntoAI LatchOntoAI { get; private set; } public SwarmBehavior SwarmBehavior { get; private set; } + public CharacterParams.TargetParams SelectedTargetingParams { get { return selectedTargetingParams; } } + public bool AttackHumans { get @@ -186,24 +192,9 @@ namespace Barotrauma } } - bool canBreakDoors = false; - if (GetTarget("room")?.Priority > 0.0f) - { - var currentContexts = Character.GetAttackContexts(); - foreach (Limb limb in Character.AnimController.Limbs) - { - if (limb.attack == null) { continue; } - if (!limb.attack.IsValidTarget(AttackTarget.Structure)) { continue; } - if (limb.attack.IsValidContext(currentContexts) && limb.attack.StructureDamage > 0.0f) - { - canBreakDoors = true; - break; - } - } - } - + ReevaluateAttacks(); outsideSteering = new SteeringManager(this); - insideSteering = new IndoorsSteeringManager(this, false, canBreakDoors); + insideSteering = new IndoorsSteeringManager(this, false, canAttackDoors); steeringManager = outsideSteering; State = AIState.Idle; @@ -213,9 +204,6 @@ namespace Barotrauma requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); avoidLookAheadDistance = Math.Max(colliderWidth * 3, 1.5f); - - canAttackSub = Character.AnimController.CanAttackSubmarine; - canAttackCharacters = Character.AnimController.CanAttackCharacters; } private CharacterParams.AIParams AIParams => Character.Params.AI; @@ -226,31 +214,32 @@ namespace Barotrauma public void SelectTarget(AITarget target, float priority) { SelectedAiTarget = target; - selectedTargetMemory = GetTargetMemory(target); + selectedTargetMemory = GetTargetMemory(target, true); selectedTargetMemory.Priority = priority; } - private float escapeMargin; + private float movementMargin; public override void Update(float deltaTime) { if (DisableEnemyAI) { return; } - base.Update(deltaTime); - bool ignorePlatforms = (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); - if (steeringManager is IndoorsSteeringManager) + bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); + if (steeringManager == insideSteering) { - var currPath = ((IndoorsSteeringManager)steeringManager).CurrentPath; + var currPath = PathSteering.CurrentPath; if (currPath != null && currPath.CurrentNode != null) { if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y) { - ignorePlatforms = true; + // Don't allow to jump from too high. + float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2; + float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y); + ignorePlatforms = height < allowedJumpHeight; } } } - Character.AnimController.IgnorePlatforms = ignorePlatforms; //clients get the facing direction from the server @@ -396,7 +385,7 @@ namespace Barotrauma { bool isBeingChased = IsBeingChased; float reactDistance = !isBeingChased && selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); - if (squaredDistance <= Math.Pow(reactDistance + escapeMargin, 2)) + if (squaredDistance <= Math.Pow(reactDistance + movementMargin, 2)) { float halfReactDistance = reactDistance / 2; float attackDistance = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDistance; @@ -408,26 +397,56 @@ namespace Barotrauma else { run = isBeingChased ? true : squaredDistance < Math.Pow(halfReactDistance, 2); - if (escapeMargin <= 0) + if (movementMargin <= 0) { - escapeMargin = halfReactDistance; + movementMargin = halfReactDistance; } - escapeMargin = MathHelper.Clamp(escapeMargin += deltaTime, halfReactDistance, reactDistance); + movementMargin = MathHelper.Clamp(movementMargin += deltaTime, halfReactDistance, reactDistance); UpdateEscape(deltaTime); } } else { - escapeMargin = 0; + movementMargin = 0; UpdateIdle(deltaTime); } } break; + case AIState.Protect: + if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) + { + State = AIState.Idle; + return; + } + if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker) + { + // Attack the character that attacked the target we are protecting + ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); + SelectTarget(attacker.AiTarget); + return; + } + float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); + float reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); + if (sqrDist > Math.Pow(reactDist + movementMargin, 2)) + { + movementMargin = reactDist; + run = true; + UpdateFollow(deltaTime); + } + else + { + movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, reactDist); + UpdateIdle(deltaTime); + } + break; default: throw new NotImplementedException(); } - LatchOntoAI?.Update(this, deltaTime); + if (!Character.AnimController.SimplePhysicsEnabled) + { + LatchOntoAI?.Update(this, deltaTime); + } IsSteeringThroughGap = false; if (SwarmBehavior != null) { @@ -435,6 +454,8 @@ namespace Barotrauma SwarmBehavior.Refresh(); SwarmBehavior.UpdateSteering(deltaTime); } + // Ensure that the creature keeps inside the level + SteerInsideLevel(deltaTime); float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun); steeringManager.Update(speed); Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, State == AIState.Idle && Character.AnimController.InWater ? Steering.Length() : speed); @@ -459,13 +480,12 @@ namespace Barotrauma SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); return; } - SteerInsideLevel(deltaTime); } var target = SelectedAiTarget ?? _lastAiTarget; if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null) { // Keep heading to the last known position of the target - var memory = GetTargetMemory(target); + var memory = GetTargetMemory(target, false); if (memory != null) { var location = memory.Location; @@ -579,10 +599,10 @@ namespace Barotrauma } else if (pathSteering != null) { - if (canAttackSub && hasValidPath) + if (canAttackDoors && hasValidPath) { var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; - if (door != null && !door.IsOpen) + if (door != null && !door.IsOpen && !door.IsBroken) { if (SelectedAiTarget != door.Item.AiTarget) { @@ -618,7 +638,6 @@ namespace Barotrauma { SteeringManager.SteeringWander(); SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); - SteerInsideLevel(deltaTime); } } } @@ -644,17 +663,18 @@ namespace Barotrauma if (SelectedAiTarget.Entity is Item item) { // If the item is held by a character, attack the character instead. - var pickable = item.GetComponent(); - if (pickable != null) + Character owner = GetOwner(item); + if (owner != null) { - Entity owner = pickable.Picker ?? item.ParentInventory?.Owner; - if (owner != null) + if (IsFriendly(Character, owner)) { - var target = owner.AiTarget; - if (target?.Entity != null && !target.Entity.Removed) - { - SelectedAiTarget = target; - } + ResetAITarget(); + State = AIState.Idle; + return; + } + else + { + SelectedAiTarget = owner.AiTarget; } } } @@ -666,7 +686,8 @@ namespace Barotrauma { attackWorldPos += wallTarget.Structure.Submarine.Position; } - attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos); + attackSimPos = Character.Submarine == wallTarget.Structure.Submarine ? wallTarget.Position : attackWorldPos; + attackSimPos = ConvertUnits.ToSimUnits(attackSimPos); } else { @@ -704,7 +725,7 @@ namespace Barotrauma var door = i.GetComponent(); // Steer through the door manually if it's open or broken // Don't try to enter dry hulls if cannot walk or if the gap is too narrow - if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && door.IsOpen) + if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.IsBroken)) { if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25) { @@ -722,7 +743,6 @@ namespace Barotrauma } else if (SelectedAiTarget.Entity is Structure w && wallTarget == null) { - // Targeting only the outer walls bool isBroken = true; for (int i = 0; i < w.Sections.Length; i++) { @@ -893,35 +913,23 @@ namespace Barotrauma } canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0; } - if (!canAttack && SelectedAiTarget.Entity.Submarine != null && !canAttackSub) + if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls)) { - float dist = Vector2.Distance(Character.AnimController.MainLimb.WorldPosition, attackWorldPos); - if (wallTarget != null) + if (Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000) { - // Steer towards the target, but turn away if a wall is blocking the way - if (dist < ConvertUnits.ToDisplayUnits(colliderLength) * 3) - { - State = AIState.Idle; - IgnoreTarget(SelectedAiTarget); - // Resetting the ai target prevents the character from chasing it - ResetAITarget(); - return; - } - } - else if (dist < 1000) - { - // Check that we are not bumping into a door + // Check that we are not bumping into a door or a wall Vector2 rayStart = SimPosition; if (Character.Submarine == null) { rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; } - Vector2 toTarget = SelectedAiTarget.WorldPosition - WorldPosition; - Vector2 rayEnd = rayStart + toTarget.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2); + Vector2 dir = SelectedAiTarget.WorldPosition - WorldPosition; + Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2); Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true); - if (Submarine.LastPickedFraction != 1.0f && closestBody != null && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent() != null) + if (Submarine.LastPickedFraction != 1.0f && closestBody != null && + (!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent() != null)) { - // Target is unreachable, there's a door ahead + // Target is unreachable, there's a door or wall ahead State = AIState.Idle; IgnoreTarget(SelectedAiTarget); ResetAITarget(); @@ -934,23 +942,28 @@ namespace Barotrauma Character targetCharacter = SelectedAiTarget.Entity as Character; if (canAttack) { - // Target a specific limb instead of the target center position - if (wallTarget == null && targetCharacter != null) + if (!Character.AnimController.SimplePhysicsEnabled) { - var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType; - attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType); - if (attackTargetLimb == null) + // Target a specific limb instead of the target center position + if (wallTarget == null && targetCharacter != null) { - State = AIState.Idle; - IgnoreTarget(SelectedAiTarget); - ResetAITarget(); - return; + var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType; + attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType); + if (attackTargetLimb == null) + { + State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); + ResetAITarget(); + return; + } + attackWorldPos = attackTargetLimb.WorldPosition; + attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb); } - attackWorldPos = attackTargetLimb.WorldPosition; - attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb); } - // Check that we can reach the target - Vector2 toTarget = attackWorldPos - AttackingLimb.WorldPosition; + + Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition; + Vector2 toTarget = attackWorldPos - attackLimbPos; + // Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform) if (wallTarget != null) { if (wallTarget.Structure.Submarine != null) @@ -961,7 +974,6 @@ namespace Barotrauma } else if (targetCharacter != null) { - // Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform) Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity); toTarget += margin; } @@ -981,6 +993,7 @@ namespace Barotrauma return ConvertUnits.ToDisplayUnits(targetVelocity) * AttackingLimb.attack.Duration * dot; } + // Check that we can reach the target distance = toTarget.Length(); canAttack = distance < AttackingLimb.attack.Range; if (!canAttack && !IsCoolDownRunning) @@ -1035,21 +1048,25 @@ namespace Barotrauma } else { - Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; - // Offset so that we don't overshoot the movement - Vector2 steerPos = attackSimPos + offset; + Vector2 steerPos = attackSimPos; + if (!Character.AnimController.SimplePhysicsEnabled) + { + // Offset so that we don't overshoot the movement + Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; + steerPos += offset; + } if (SteeringManager is IndoorsSteeringManager pathSteering) { if (pathSteering.CurrentPath != null) { // Attack doors - if (canAttackSub) + if (canAttackDoors) { // If the target is in the same hull, there shouldn't be any doors blocking the path if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull) { var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; - if (door != null && !door.IsOpen) + if (door != null && !door.IsOpen && !door.IsBroken) { if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget) { @@ -1063,13 +1080,14 @@ namespace Barotrauma if ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && (targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity))) { - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition)); + Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition; + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos)); } else { SteeringManager.SteeringSeek(steerPos, 2); // Switch to Idle when cannot reach the target and if cannot damage the walls - if ((!canAttackSub || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) + if ((!canAttackWalls || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) { State = AIState.Idle; return; @@ -1145,7 +1163,7 @@ namespace Barotrauma if (attack == null) { continue; } if (attack.CoolDownTimer > 0) { continue; } if (!attack.IsValidContext(currentContexts)) { continue; } - if (!attack.IsValidTarget(target)) { continue; } + if (!attack.IsValidTarget(target as IDamageable)) { continue; } if (target is ISerializableEntity se && target is Character) { if (attack.Conditionals.Any(c => !c.Matches(se))) { continue; } @@ -1176,6 +1194,7 @@ namespace Barotrauma float CalculatePriority(Limb limb, Vector2 attackPos) { + if (Character.AnimController.SimplePhysicsEnabled) { return 1 + limb.attack.Priority; } float dist = Vector2.Distance(limb.WorldPosition, attackPos); // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. // We also need a max value that is more than the actual range. @@ -1242,7 +1261,10 @@ namespace Barotrauma LatchOntoAI?.SetAttachTarget(wall.Submarine.PhysicsBody.FarseerBody, wall.Submarine, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal); if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall)) { - wallTarget = new WallTarget(sectionPos, wall, sectionIndex); + if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner")) + { + wallTarget = new WallTarget(sectionPos, wall, sectionIndex); + } } } if (!Character.AnimController.CanEnterSubmarine && wallTarget == null) @@ -1271,7 +1293,7 @@ namespace Barotrauma } return isDisabled; } - + public override void OnAttacked(Character attacker, AttackResult attackResult) { float reactionTime = Rand.Range(0.1f, 0.3f); @@ -1281,7 +1303,7 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); LatchOntoAI?.DeattachFromBody(); if (attacker == null || attacker.AiTarget == null) { return; } - bool isFriendly = attacker.SpeciesName == Character.SpeciesName || attacker.Params.Group == Character.Params.Group; + bool isFriendly = IsFriendly(Character, attacker); if (wasLatched) { avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f); @@ -1302,7 +1324,7 @@ namespace Barotrauma } if (!isFriendly && attackResult.Damage > 0.0f) { - bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackSub; + bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls; if (Character.Params.AI.AttackWhenProvoked && canAttack) { if (attacker.IsHusk) @@ -1356,7 +1378,7 @@ namespace Barotrauma } } - AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget); + AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, true); targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AggressionHurt; // Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown @@ -1399,7 +1421,7 @@ namespace Barotrauma var aiTarget = wallTarget.Structure.AiTarget; if (aiTarget != null && SelectedAiTarget != aiTarget) { - SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget).Priority); + SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, true).Priority); } } IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; @@ -1464,7 +1486,7 @@ namespace Barotrauma State = AIState.Idle; return; } - Vector2 mouthPos = Character.AnimController.GetMouthPosition().Value; + Vector2 mouthPos = Character.AnimController.SimplePhysicsEnabled ? SimPosition : Character.AnimController.GetMouthPosition().Value; Vector2 attackSimPosition = Character.GetRelativeSimPosition(target); Vector2 limbDiff = attackSimPosition - mouthPos; float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2); @@ -1493,6 +1515,25 @@ namespace Barotrauma #endregion + private void UpdateFollow(float deltaTime) + { + if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) + { + State = AIState.Idle; + return; + } + Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition); + if (!MathUtils.IsValid(dir)) + { + return; + } + steeringManager.SteeringManual(deltaTime, dir); + if (Character.AnimController.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); + } + } + #region Targeting private bool IsLatchedOnSub => LatchOntoAI != null && LatchOntoAI.IsAttachedToSub; @@ -1619,13 +1660,16 @@ namespace Barotrauma Door door = null; if (aiTarget.Entity is Item item) { - //item inside and we're outside -> attack the hull - if (item.CurrentHull != null && character.CurrentHull == null) - { - targetingTag = "room"; - } - door = item.GetComponent(); + bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null; + if (targetingFromOutsideToInside) + { + if (door != null && !canAttackDoors || !canAttackWalls) + { + // Can't reach + continue; + } + } foreach (var prio in AIParams.Targets) { if (item.HasTag(prio.Tag)) @@ -1634,7 +1678,25 @@ namespace Barotrauma break; } } - + if (door == null && targetingTag == null) + { + if (item.GetComponent() != null) + { + targetingTag = "sonar"; + } + else if (targetingFromOutsideToInside) + { + targetingTag = "room"; + } + } + else if (targetingTag == "nasonov") + { + if ((item.Submarine == null || !item.Submarine.Info.IsPlayer) && item.ParentInventory == null) + { + // Only target nasonovartifacts when they are held be a player or inside the playersub + continue; + } + } // Ignore the target if it's a decoy and the character is already inside a sub if (character.CurrentHull != null && targetingTag == "decoy") { @@ -1649,15 +1711,13 @@ namespace Barotrauma // Ignore structures that doesn't have a body (not walls) continue; } - if (s.IsPlatform) + if (s.IsPlatform) { continue; } + if (s.Submarine == null) { continue; } + bool isCharacterInside = character.CurrentHull != null; + bool isInnerWall = s.prefab.Tags.Contains("inner"); + if (isInnerWall && !isCharacterInside) { - continue; - } - bool isCharacterOutside = s.Submarine == null || character.CurrentHull == null; - bool targetInnerWalls = AIParams.TargetInnerWalls; - if (!isCharacterOutside && !targetInnerWalls) - { - // Ignore walls when inside (walltargets still work) + // Ignore inner walls when outside (walltargets still work) continue; } valueModifier = 1; @@ -1670,48 +1730,71 @@ namespace Barotrauma var section = s.Sections[i]; if (section.gap == null) { continue; } bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; + isInnerWall = isInnerWall || !leadsInside; if (Character.AnimController.CanEnterSubmarine) { - if (isCharacterOutside) + if (!isCharacterInside) { if (CanPassThroughHole(s, i)) { - valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : (targetInnerWalls ? 1 : 0); + valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0; } - else + else if (AggressiveBoarding && leadsInside && canAttackWalls && AIParams.TargetOuterWalls) { - // Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in - if (!canAttackSub) + // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside + valueModifier *= 1 + section.gap.Open; + } + } + else + { + // Inside + if (AggressiveBoarding) + { + if (!isInnerWall) + { + // Only interested in getting inside (aggressive boarder) -> don't target outer walls when already inside + valueModifier = 0; + break; + } + else if (CanPassThroughHole(s, i)) + { + valueModifier *= isInnerWall ? 1 : 0; + } + else if (!canAttackWalls) { valueModifier = 0; break; } - if (AggressiveBoarding && leadsInside) + } + else + { + if (!canAttackWalls) { - // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside - valueModifier *= 1 + section.gap.Open; + valueModifier = 0; + break; } + // We are actually interested in breaking things -> reduce the priority when the wall is already broken + // (Terminalcells) + valueModifier *= 1 - section.gap.Open * 0.25f; } } - else if (!canAttackSub || CanPassThroughHole(s, i)) + } + else + { + // Cannot enter + if (isInnerWall || !canAttackWalls) { - // Already inside -> ignore holes in the walls and ignore walls if cannot attack the sub. + // Ignore inner walls and all walls if cannot do damage on walls. valueModifier = 0; break; } - else if (canAttackSub && !AggressiveBoarding) + else if (AggressiveBoarding) { - // We are actually interested in breaking things -> reduce the priority when the wall is already broken - valueModifier *= 1 - section.gap.Open * 0.25f; + // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside + // (Bonethreshers) + valueModifier *= 1 + section.gap.Open; } } - else if (!leadsInside || !canAttackSub) - { - // Can't get in, ignore inner walls - // Also ignore all walls if cannot attack the sub - valueModifier = 0; - break; - } } } else @@ -1727,31 +1810,34 @@ namespace Barotrauma } if (door.Item.Submarine == null) { continue;} bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom; - bool isOpen = door.IsOpen; - if (!isOpen && (!canAttackSub)) + bool isOpen = door.IsOpen || door.IsBroken; + if (!isOpen && !canAttackDoors || (isOutdoor && !AIParams.TargetOuterWalls)) { - // Ignore doors that are not open if cannot attack items/structures. Open doors should be targeted, so that we can get in if we are aggressive boarders - valueModifier = 0; + // Ignore doors that are not open if cannot attack doors or shouldn't target outer doors. + continue; } - if (character.CurrentHull == null) + if (isOpen && (!Character.AnimController.CanEnterSubmarine || !AggressiveBoarding)) { - valueModifier = isOutdoor ? 1 : 0; + // Ignore broken and open doors + // Aggressive boarders don't ignore open doors, because they use them for get in. + continue; } - else if (AggressiveBoarding) + if (AggressiveBoarding) { - // Increase priority if the character is outside and an aggressive boarder, and the door is from outside to inside - if (character.CurrentHull == null) + // Increase the priority if the character is outside and the door is from outside to inside + if (character.CurrentHull == null && isOutdoor) { valueModifier *= isOpen ? 5 : 1; } else { - valueModifier *= isOpen ? 0 : 1; + // Inside + valueModifier *= isOpen || isOutdoor ? 0 : 1; } } - else if (!Character.AnimController.CanEnterSubmarine && isOpen) //ignore broken and open doors + else if (character.CurrentHull == null) { - continue; + valueModifier = isOutdoor ? 1 : 0; } } else if (aiTarget.Entity is IDamageable targetDamageable && targetDamageable.Health <= 0.0f) @@ -1783,7 +1869,7 @@ namespace Barotrauma // -> just ignore the distance and attack whatever has the highest priority dist = Math.Max(dist, 100.0f); - AITargetMemory targetMemory = GetTargetMemory(aiTarget); + AITargetMemory targetMemory = GetTargetMemory(aiTarget, true); if (Character.CurrentHull != null && Math.Abs(toTarget.Y) > Character.CurrentHull.Size.Y) { // Inside the sub, treat objects that are up or down, as they were farther away. @@ -1793,9 +1879,18 @@ namespace Barotrauma if (valueModifier > targetValue) { - // Don't target items that we own. - // This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive) - if (aiTarget.Entity is Item i && i.IsOwnedBy(character)) { continue; } + if (aiTarget.Entity is Item i) + { + Character owner = GetOwner(i); + // Don't target items that we own. + // This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive) + if (owner == character) { continue; } + if (owner != null && IsFriendly(Character, owner)) + { + // If the item is held by a friendly character, ignore it. + continue; + } + } if (targetCharacter != null) { if (targetCharacter.Submarine != Character.Submarine) @@ -1820,7 +1915,7 @@ namespace Barotrauma foreach (var gap in Character.CurrentHull.ConnectedGaps) { var door = gap.ConnectedDoor; - if (door == null || !door.IsOpen) + if (door == null || !door.IsOpen && !door.IsBroken) { var wall = gap.ConnectedWall; if (wall != null) @@ -1855,12 +1950,15 @@ namespace Barotrauma return SelectedAiTarget; } - private AITargetMemory GetTargetMemory(AITarget target) + private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound) { if (!targetMemories.TryGetValue(target, out AITargetMemory memory)) { - memory = new AITargetMemory(target, 10); - targetMemories.Add(target, memory); + if (addIfNotFound) + { + memory = new AITargetMemory(target, 10); + targetMemories.Add(target, memory); + } } return memory; } @@ -1875,8 +1973,11 @@ namespace Barotrauma } else if (CanPerceive(_selectedAiTarget, distSquared: Vector2.DistanceSquared(Character.WorldPosition, _selectedAiTarget.WorldPosition))) { - var memory = GetTargetMemory(_selectedAiTarget); - memory.Location = _selectedAiTarget.WorldPosition; + var memory = GetTargetMemory(_selectedAiTarget, false); + if (memory != null) + { + memory.Location = _selectedAiTarget.WorldPosition; + } } } } @@ -2014,11 +2115,17 @@ namespace Barotrauma { // If the target is shooting from the submarine, we might not perceive it because it doesn't move. // --> Target the submarine too. - if (target.Submarine != null && canAttackSub) + if (target.Submarine != null && (canAttackDoors || canAttackWalls)) { ChangeParams("room", state, priority); - ChangeParams("wall", state, priority); - ChangeParams("door", state, priority); + if (canAttackWalls) + { + ChangeParams("wall", state, priority); + } + if (canAttackDoors) + { + ChangeParams("door", state, priority); + } } ChangeParams("provocative", state, priority, onlyExisting: true); ChangeParams("light", state, priority, onlyExisting: true); @@ -2039,7 +2146,7 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); escapeTarget = null; AttackingLimb = null; - escapeMargin = 0; + movementMargin = 0; allGapsSearched = false; unreachableGaps.Clear(); if (isStateChanged && to == AIState.Idle && from != to) @@ -2064,28 +2171,66 @@ namespace Barotrauma } } + public void ReevaluateAttacks() + { + canAttackWalls = LatchOntoAI != null && LatchOntoAI.AttachToSub; + canAttackDoors = false; + canAttackCharacters = false; + foreach (var limb in Character.AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.attack == null) { continue; } + if (!canAttackWalls) + { + canAttackWalls = limb.attack.IsValidTarget(AttackTarget.Structure) && limb.attack.StructureDamage > 0; + } + if (!canAttackDoors) + { + canAttackDoors = limb.attack.IsValidTarget(AttackTarget.Structure) && limb.attack.ItemDamage > 0; + } + if (!canAttackCharacters) + { + canAttackCharacters = limb.attack.IsValidTarget(AttackTarget.Character); + } + } + if (PathSteering != null) + { + PathSteering.CanBreakDoors = canAttackDoors; + } + } + + private Vector2 returnDir; + private float returnTimer; private void SteerInsideLevel(float deltaTime) { - if (Level.Loaded == null) { return; } - - Vector2 levelSimSize = new Vector2( - ConvertUnits.ToSimUnits(Level.Loaded.Size.X), - ConvertUnits.ToSimUnits(Level.Loaded.Size.Y)); - - float margin = 10.0f; - - if (SimPosition.Y < 0.0) + if (SteeringManager is IndoorsSteeringManager || !StayInsideLevel) { return; } + if (Level.Loaded == null) { return; } + Vector2 levelSimSize = ConvertUnits.ToSimUnits(Level.Loaded.Size.X, Level.Loaded.Size.Y); + float returnTime = 3; + if (SimPosition.Y < 0) { - steeringManager.SteeringManual(deltaTime, Vector2.UnitY * MathUtils.InverseLerp(0.0f, -margin, SimPosition.Y)); + // Too far down + returnTimer = returnTime * Rand.Range(0.75f, 1.25f); + returnDir = Vector2.UnitY; } - if (SimPosition.X < 0.0f) + if (SimPosition.X < 0) { - steeringManager.SteeringManual(deltaTime, Vector2.UnitX * MathUtils.InverseLerp(0.0f, -margin, SimPosition.X)); + // Too far left + returnTimer = returnTime * Rand.Range(0.75f, 1.25f); + returnDir = Vector2.UnitX; } if (SimPosition.X > levelSimSize.X) { - steeringManager.SteeringManual(deltaTime, Vector2.UnitX * MathUtils.InverseLerp(levelSimSize.X, levelSimSize.X + margin, SimPosition.X)); - } + // Too far right + returnTimer = returnTime * Rand.Range(0.75f, 1.25f); + returnDir = -Vector2.UnitX; + } + if (returnTimer > 0) + { + returnTimer -= deltaTime; + SteeringManager.Reset(); + SteeringManager.SteeringManual(deltaTime, returnDir); + } } private bool CanPassThroughHole(Structure wall, int sectionIndex) @@ -2116,7 +2261,6 @@ namespace Barotrauma targetLimbs.Clear(); foreach (var limb in target.AnimController.Limbs) { - if (limb.IsSevered) { continue; } if (limb.type == targetLimbType || targetLimbType == LimbType.None) { targetLimbs.Add(limb); @@ -2131,6 +2275,7 @@ namespace Barotrauma Limb targetLimb = null; foreach (Limb limb in targetLimbs) { + if (limb.IsSevered) { continue; } float dist = Vector2.DistanceSquared(limb.WorldPosition, attackLimb.WorldPosition) / Math.Max(limb.AttackPriority, 0.1f); if (dist < closestDist) { @@ -2140,6 +2285,27 @@ namespace Barotrauma } return targetLimb; } + + private Character GetOwner(Item item) + { + // If the item is held by a character, attack the character instead. + var pickable = item.GetComponent(); + if (pickable != null) + { + Character owner = pickable.Picker ?? item.FindParentInventory(i => i.Owner is Character)?.Owner as Character; + if (owner != null) + { + var target = owner.AiTarget; + if (target?.Entity != null && !target.Entity.Removed) + { + return owner; + } + } + } + return null; + } + + public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); } //the "memory" of the Character diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 1cb0911e6..b033c6eb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -30,8 +30,9 @@ namespace Barotrauma public static float HULL_SAFETY_THRESHOLD = 50; - public HashSet UnreachableHulls { get; private set; } = new HashSet(); - public HashSet UnsafeHulls { get; private set; } = new HashSet(); + public readonly HashSet UnreachableHulls = new HashSet(); + public readonly HashSet UnsafeHulls = new HashSet(); + public readonly List IgnoredItems = new List(); private SteeringManager outsideSteering, insideSteering; @@ -55,13 +56,13 @@ namespace Barotrauma private set; } - public float CurrentHullSafety { get; private set; } + public float CurrentHullSafety { get; private set; } = 100; public HumanAIController(Character c) : base(c) { if (!c.IsHuman) { - throw new System.Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!"); + throw new Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!"); } insideSteering = new IndoorsSteeringManager(this, true, false); outsideSteering = new SteeringManager(this); @@ -85,7 +86,7 @@ namespace Barotrauma { unreachableClearTimer = clearUnreachableInterval; UnreachableHulls.Clear(); - ignoredContainers.Clear(); + IgnoredItems.Clear(); } // Use the pathfinding also outside of the sub, but not farther than the extents of the sub + 500 units. @@ -175,9 +176,7 @@ namespace Barotrauma } steeringManager.Update(Character.AnimController.GetCurrentSpeed(run && Character.CanRun)); - bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && - (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); - + bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); if (steeringManager == insideSteering) { var currPath = PathSteering.CurrentPath; @@ -185,51 +184,24 @@ namespace Barotrauma { if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y) { - // Don't allow to jump from too high. The formula might require tweaking. + // Don't allow to jump from too high. float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2; float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y); ignorePlatforms = height < allowedJumpHeight; } } - if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent) { Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y)); } } - Character.AnimController.IgnorePlatforms = ignorePlatforms; Vector2 targetMovement = AnimController.TargetMovement; - if (!Character.AnimController.InWater) { targetMovement = new Vector2(Character.AnimController.TargetMovement.X, MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f)); } - - if (Character.AnimController.InWater && targetMovement.LengthSquared() < 0.000001f) - { - bool isAiming = false; - var holdable = Character.SelectedConstruction?.GetComponent(); - if (holdable != null) - { - isAiming = holdable.ControlPose; - } - bool swimInPlace = !isAiming; - if (swimInPlace && ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo goToObjective) - { - if (goToObjective.Target != Character) - { - swimInPlace = false; - } - } - if (swimInPlace) - { - // Swim in place so that we don't fall motionless and look dead. - targetMovement = new Vector2(targetMovement.X, Rand.Range(-0.001f, 0.001f)); - } - } - Character.AnimController.TargetMovement = Character.ApplyMovementLimits(targetMovement, AnimController.GetCurrentSpeed(run)); flipTimer -= deltaTime; @@ -280,14 +252,14 @@ namespace Barotrauma else { findItemState = FindItemState.Extinguisher; - if (FindSuitableContainer(Character, extinguisher, out Item targetContainer)) + if (FindSuitableContainer(extinguisher, out Item targetContainer)) { findItemState = FindItemState.None; itemIndex = 0; if (targetContainer != null) { var decontainObjective = new AIObjectiveDecontainItem(Character, extinguisher, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer); + decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } @@ -310,42 +282,47 @@ namespace Barotrauma || ObjectiveManager.IsCurrentObjective() || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); bool removeDivingSuit = !Character.AnimController.HeadInWater && oxygenLow; - AIObjectiveGoTo gotoObjective = ObjectiveManager.GetActiveObjective(); + bool takeMaskOff = !Character.AnimController.HeadInWater && oxygenLow; if (!removeDivingSuit) { - bool targetHasNoSuit = gotoObjective != null && gotoObjective.mimic && !HasDivingSuit(gotoObjective.Target as Character); - removeDivingSuit = !shouldKeepTheGearOn && (gotoObjective == null || targetHasNoSuit); - } - bool takeMaskOff = !Character.AnimController.HeadInWater && oxygenLow; - if (!takeMaskOff && Character.CurrentHull.WaterPercentage < 40) - { - bool targetHasNoMask = gotoObjective != null && gotoObjective.mimic && !HasDivingMask(gotoObjective.Target as Character); - takeMaskOff = !shouldKeepTheGearOn && (gotoObjective == null || targetHasNoMask); - } - if (gotoObjective != null) - { - if (gotoObjective.Target is Hull h) + if (shouldKeepTheGearOn) { - if (NeedsDivingGear(Character, h, out _)) - { - removeDivingSuit = false; - takeMaskOff = false; - } + removeDivingSuit = false; } - else if (gotoObjective.Target is Character c) + } + if (!takeMaskOff) + { + if (shouldKeepTheGearOn) { - if (NeedsDivingGear(Character, c.CurrentHull, out _)) - { - removeDivingSuit = false; - takeMaskOff = false; - } + takeMaskOff = false; } - else if (gotoObjective.Target is Item i) + } + if (!shouldKeepTheGearOn && (!takeMaskOff || !removeDivingSuit)) + { + foreach (var objective in ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(includingSelf: true)) { - if (NeedsDivingGear(Character, i.CurrentHull, out _)) + if (objective is AIObjectiveGoTo gotoObjective) { - removeDivingSuit = false; - takeMaskOff = false; + bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; + Hull targetHull = gotoObjective.GetTargetHull(); + bool targetIsOutside = (gotoObjective.Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes); + if (targetIsOutside || NeedsDivingGear(Character, targetHull, out _)) + { + removeDivingSuit = false; + takeMaskOff = false; + break; + } + else if (gotoObjective.mimic) + { + if (!removeDivingSuit) + { + removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character); + } + if (!takeMaskOff) + { + takeMaskOff = !HasDivingMask(gotoObjective.Target as Character); + } + } } } } @@ -363,7 +340,7 @@ namespace Barotrauma else { findItemState = FindItemState.DivingSuit; - if (FindSuitableContainer(Character, divingSuit, out Item targetContainer)) + if (FindSuitableContainer(divingSuit, out Item targetContainer)) { findItemState = FindItemState.None; itemIndex = 0; @@ -375,7 +352,7 @@ namespace Barotrauma }; decontainObjective.Abandoned += () => { - ignoredContainers.Add(targetContainer); + IgnoredItems.Add(targetContainer); }; ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; @@ -405,14 +382,14 @@ namespace Barotrauma else { findItemState = FindItemState.DivingMask; - if (FindSuitableContainer(Character, mask, out Item targetContainer)) + if (FindSuitableContainer(mask, out Item targetContainer)) { findItemState = FindItemState.None; itemIndex = 0; if (targetContainer != null) { var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer); + decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } @@ -442,14 +419,14 @@ namespace Barotrauma { if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any })) { - if (FindSuitableContainer(Character, item, out Item targetContainer)) + if (FindSuitableContainer(item, out Item targetContainer)) { findItemState = FindItemState.None; itemIndex = 0; if (targetContainer != null) { var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer); + decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } @@ -478,11 +455,10 @@ namespace Barotrauma } private FindItemState findItemState; private int itemIndex; - private List ignoredContainers = new List(); - public bool FindSuitableContainer(Character character, Item containableItem, out Item suitableContainer) + public bool FindSuitableContainer(Item containableItem, out Item suitableContainer) { suitableContainer = null; - if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: i => + if (Character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: IgnoredItems, customPriorityFunction: i => { var container = i.GetComponent(); if (container == null) { return 0; } @@ -583,7 +559,7 @@ namespace Barotrauma if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, Character)) { - if (item.Repairables.All(r => item.ConditionPercentage > r.AIRepairThreshold)) { continue; } + if (item.Repairables.All(r => item.ConditionPercentage > r.RepairThreshold)) { continue; } if (AddTargets(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective()) { var orderPrefab = Order.GetPrefab("reportbrokendevices"); @@ -633,9 +609,13 @@ namespace Barotrauma if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; } if (attacker == null || attacker.IsDead || attacker.Removed) { + // Don't react on the damage if there's no attacker. + // We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying. + // But fires and enemies should already be handled by the FindSafetyObjective. + return; // Ignore damage from falling etc that we shouldn't react to. - if (Character.LastDamageSource == null) { return; } - AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced)); + //if (Character.LastDamageSource == null) { return; } + //AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced)); } else if (IsFriendly(attacker)) { @@ -784,7 +764,6 @@ namespace Barotrauma return false; } - public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage); /// @@ -852,7 +831,7 @@ namespace Barotrauma if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, character)) { - if (item.Repairables.All(r => item.ConditionPercentage >= r.AIRepairThreshold)) { continue; } + if (item.Repairables.All(r => item.ConditionPercentage >= r.RepairThreshold)) { continue; } AddTargets(character, item); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 6628d279f..2f2b44d6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -12,7 +12,8 @@ namespace Barotrauma private PathFinder pathFinder; private SteeringPath currentPath; - private bool canOpenDoors, canBreakDoors; + private bool canOpenDoors; + public bool CanBreakDoors { get; set; } private Character character; @@ -50,8 +51,8 @@ namespace Barotrauma /// public bool InLadders => currentPath != null && - currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null || - (currentPath.NextNode != null && currentPath.NextNode.Ladders != null)); + currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null && !currentPath.CurrentNode.Ladders.Item.NonInteractable || + (currentPath.NextNode != null && currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable)); /// /// Returns true if any node in the path is in stairs @@ -69,6 +70,7 @@ namespace Barotrauma if (currentPath.NextNode == null) { return false; } var currentLadder = currentPath.CurrentNode.Ladders; if (currentLadder == null) { return false; } + if (currentLadder.Item.NonInteractable) { return false; } var nextLadder = GetNextLadder(); return nextLadder != null && nextLadder == currentLadder; } @@ -80,7 +82,7 @@ namespace Barotrauma pathFinder.GetNodePenalty = GetNodePenalty; this.canOpenDoors = canOpenDoors; - this.canBreakDoors = canBreakDoors; + this.CanBreakDoors = canBreakDoors; character = (host as AIController).Character; @@ -103,6 +105,12 @@ namespace Barotrauma IsPathDirty = false; } + public void ResetPath() + { + currentPath = null; + IsPathDirty = true; + } + public void SteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null) { steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter); @@ -115,7 +123,7 @@ namespace Barotrauma { if (currentPath == null) { return null; } if (currentPath.NextNode == null) { return null; } - if (currentPath.NextNode.Ladders != null) + if (currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable) { return currentPath.NextNode.Ladders; } @@ -126,7 +134,10 @@ namespace Barotrauma { var node = currentPath.Nodes[index]; if (node == null) { return null; } - return node.Ladders; + if (node.Ladders != null && !node.Ladders.Item.NonInteractable) + { + return node.Ladders; + } } return null; } @@ -134,7 +145,19 @@ namespace Barotrauma private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null) { - bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || Vector2.DistanceSquared(target, currentTarget) > 1); + Vector2 targetDiff = target - currentTarget; + if (currentPath != null && currentPath.Nodes.Any()) + { + //current path calculated relative to a different sub than where the character is now + //take that into account when calculating if the target has moved + Submarine currentPathSub = currentPath?.Nodes.First().Submarine; + if (currentPathSub != character.Submarine && character.Submarine != null) + { + Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition; + targetDiff += subDiff; + } + } + bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || targetDiff.LengthSquared() > 1); //find a new path if one hasn't been found yet or the target is different from the current target if (needsNewPath || findPathTimer < -1.0f) { @@ -172,12 +195,13 @@ namespace Barotrauma Vector2 diff = DiffToCurrentNode(); var collider = character.AnimController.Collider; + // Only humanoids can climb ladders + bool canClimb = character.AnimController is HumanoidAnimController; //if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically - if (!character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius) + if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius) { diff.Y = 0.0f; } - //if (diff.LengthSquared() < 0.001f) { return -host.Steering; } if (diff == Vector2.Zero) { return Vector2.Zero; } return Vector2.Normalize(diff) * weight; } @@ -186,8 +210,10 @@ namespace Barotrauma private Vector2 DiffToCurrentNode() { - if (currentPath == null || currentPath.Unreachable) return Vector2.Zero; - + if (currentPath == null || currentPath.Unreachable) + { + return Vector2.Zero; + } if (currentPath.Finished) { Vector2 pos2 = host.SimPosition; @@ -197,15 +223,12 @@ namespace Barotrauma pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition; } return currentTarget - pos2; - } - + } if (canOpenDoors && !character.LockHands && buttonPressCooldown <= 0.0f) { CheckDoorsInPath(); - } - + } Vector2 pos = host.SimPosition; - if (character != null && currentPath.CurrentNode != null) { if (CurrentPath.CurrentNode.Submarine != null) @@ -220,19 +243,17 @@ namespace Barotrauma } } } - bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; - - //only humanoids can climb ladders - if (!isDiving && character.AnimController is HumanoidAnimController && IsNextLadderSameAsCurrent) + // Only humanoids can climb ladders + bool canClimb = character.AnimController is HumanoidAnimController; + if (canClimb && !isDiving && IsNextLadderSameAsCurrent) { - if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item && - currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition)) + var ladders = currentPath.CurrentNode.Ladders; + if (character.SelectedConstruction != ladders.Item && ladders.Item.IsInsideTrigger(character.WorldPosition)) { currentPath.CurrentNode.Ladders.Item.TryInteract(character, false, true); } - } - + } var collider = character.AnimController.Collider; if (character.IsClimbing && !isDiving) { @@ -252,13 +273,16 @@ namespace Barotrauma { diff.Y = Math.Max(diff.Y, 1.0f); } + // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. + float margin = 0.1f; + bool isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f; // If the next waypoint is horizontally far, we don't want to keep holding the ladders - if (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50) + if (isAboveFloor && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)) { character.AnimController.Anim = AnimController.Animation.None; character.SelectedConstruction = null; } - else if (!nextLadderSameAsCurrent) + else if (nextLadder != null && !nextLadderSameAsCurrent) { // Try to change the ladder (hatches between two submarines) if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition)) @@ -266,9 +290,6 @@ namespace Barotrauma nextLadder.Item.TryInteract(character, false, true); } } - // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. - float margin = 0.1f; - bool isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f; if (nextLadder != null || isAboveFloor) { currentPath.SkipToNextNode(); @@ -286,7 +307,7 @@ namespace Barotrauma } return diff; } - else if (character.AnimController.InWater) + else if (!canClimb || character.AnimController.InWater) { // If the character is underwater, we don't need the ladders anymore if (character.IsClimbing && isDiving) @@ -294,49 +315,59 @@ namespace Barotrauma character.AnimController.Anim = AnimController.Animation.None; character.SelectedConstruction = null; } - float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); - float targetDistance = collider.GetSize().X * multiplier; - float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X); - float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y); - if (character.CurrentHull != currentPath.CurrentNode.CurrentHull) + var door = currentPath.CurrentNode.ConnectedDoor; + bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken; + if (!blockedByDoor) { - verticalDistance *= 2; - } - float distance = horizontalDistance + verticalDistance; - if (ConvertUnits.ToSimUnits(distance) < targetDistance) - { - currentPath.SkipToNextNode(); + float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); + float targetDistance = collider.GetSize().X * multiplier; + float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X); + float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y); + if (character.CurrentHull != currentPath.CurrentNode.CurrentHull) + { + verticalDistance *= 2; + } + float distance = horizontalDistance + verticalDistance; + if (ConvertUnits.ToSimUnits(distance) < targetDistance) + { + currentPath.SkipToNextNode(); + } } } else if (!IsNextLadderSameAsCurrent) { + // Walking horizontally Vector2 colliderBottom = character.AnimController.GetColliderBottom(); Vector2 colliderSize = collider.GetSize(); Vector2 velocity = collider.LinearVelocity; - // If the character is smaller than this, it fails to use the waypoint nodes, because they are always too high. + // If the character is smaller than this, it would fail to use the waypoint nodes because they are always too high. float minHeight = 1; // Cannot use the head position, because not all characters have head or it can be below the total height of the character float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight); float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X); bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y; bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight; + var door = currentPath.CurrentNode.ConnectedDoor; + bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken; float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1)); float targetDistance = collider.radius * margin; - if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh) + if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && !blockedByDoor) { currentPath.SkipToNextNode(); } } - - if (currentPath.CurrentNode == null) return Vector2.Zero; - + if (currentPath.CurrentNode == null) + { + return Vector2.Zero; + } return currentPath.CurrentNode.SimPosition - pos; } private bool CanAccessDoor(Door door, Func buttonFilter = null) { if (door.IsOpen) { return true; } - if (canBreakDoors) { return true; } + if (door.Item.NonInteractable) { return false; } + if (CanBreakDoors) { return true; } if (door.IsStuck) { return false; } if (!canOpenDoors || character.LockHands) { return false; } if (door.HasIntegratedButtons) @@ -345,7 +376,7 @@ namespace Barotrauma } else { - return door.Item.GetConnectedComponents(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))); + return door.Item.GetConnectedComponents(true).Any(b => !b.Item.NonInteractable && b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))); } } @@ -381,7 +412,11 @@ namespace Barotrauma { //the node we're heading towards is the last one in the path, and at a door //the door needs to be open for the character to reach the node - shouldBeOpen = true; + if (currentWaypoint.ConnectedDoor.LinkedGap != null && currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom) + { + shouldBeOpen = true; + door = currentWaypoint.ConnectedDoor; + } } else { @@ -519,9 +554,9 @@ namespace Barotrauma //non-humanoids can't climb up ladders if (!(character.AnimController is HumanoidAnimController)) { - if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && - nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up - nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y) //upper node not underwater + if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && nextNode.Waypoint.Ladders.Item.NonInteractable || + (nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up + nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y)) //upper node not underwater { return null; } @@ -539,7 +574,10 @@ namespace Barotrauma } if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f) { - penalty += 500.0f; + if (!HumanAIController.HasDivingSuit(character)) + { + penalty += 500.0f; + } } if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 68746f340..11fb2d14d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -1,11 +1,11 @@ using FarseerPhysics; -using FarseerPhysics.Common; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Xml.Linq; +using System.Linq; namespace Barotrauma { @@ -19,8 +19,8 @@ namespace Barotrauma private Vector2 attachSurfaceNormal; private Submarine attachTargetSubmarine; - private bool attachToSub; - private bool attachToWalls; + public bool AttachToSub { get; private set; } + public bool AttachToWalls { get; private set; } private float minDeattachSpeed = 3.0f, maxDeattachSpeed = 10.0f; private float damageOnDetach = 0.0f, detachStun = 0.0f; @@ -58,8 +58,8 @@ namespace Barotrauma public LatchOntoAI(XElement element, EnemyAIController enemyAI) { - attachToWalls = element.GetAttributeBool("attachtowalls", false); - attachToSub = element.GetAttributeBool("attachtosub", false); + AttachToWalls = element.GetAttributeBool("attachtowalls", false); + AttachToSub = element.GetAttributeBool("attachtosub", false); minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 3.0f); maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 10.0f)); damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f); @@ -67,11 +67,19 @@ namespace Barotrauma localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero)); attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f)); - if (Enum.TryParse(element.GetAttributeString("attachlimb", "Head"), out LimbType attachLimbType)) + string limbString = element.GetAttributeString("attachlimb", null); + attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase)); + if (attachLimb == null) { - attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType); + if (Enum.TryParse(limbString, out LimbType attachLimbType)) + { + attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType); + } + } + if (attachLimb == null) + { + attachLimb = enemyAI.Character.AnimController.MainLimb; } - if (attachLimb == null) attachLimb = enemyAI.Character.AnimController.MainLimb; enemyAI.Character.OnDeath += OnCharacterDeath; } @@ -108,7 +116,9 @@ namespace Barotrauma //something went wrong, limb body is very far from the joint anchor -> deattach if (Vector2.DistanceSquared(attachJoints[i].WorldAnchorB, attachJoints[i].BodyA.Position) > 10.0f * 10.0f) { +#if DEBUG DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach"); +#endif DeattachFromBody(); return; } @@ -131,7 +141,7 @@ namespace Barotrauma switch (enemyAI.State) { case AIState.Idle: - if (attachToWalls && character.Submarine == null && Level.Loaded != null) + if (AttachToWalls && character.Submarine == null && Level.Loaded != null) { if (!IsAttached) { @@ -180,8 +190,9 @@ namespace Barotrauma } else { - float dist = Vector2.Distance(character.SimPosition, wallAttachPos); - if (dist < Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f) + float squaredDistance = Vector2.DistanceSquared(character.SimPosition, wallAttachPos); + float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f; + if (squaredDistance < targetDistance * targetDistance) { //close enough to a wall -> attach AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, wallAttachPos); @@ -197,12 +208,13 @@ namespace Barotrauma } break; case AIState.Attack: + case AIState.Aggressive: if (enemyAI.AttackingLimb != null) { - if (attachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null) + if (AttachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null) { // is not attached or is attached to something else - if (!IsAttached || IsAttached && attachJoints[0].BodyB == attachTargetBody) + if (!IsAttached || IsAttached && attachJoints[0].BodyB != attachTargetBody) { if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) { @@ -247,16 +259,17 @@ namespace Barotrauma if (attachJoints.Count > 0) { //already attached to the target body, no need to do anything - if (attachJoints[0].BodyB == targetBody) return; + if (attachJoints[0].BodyB == targetBody) { return; } DeattachFromBody(); } jointDir = attachLimb.Dir; Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale; - if (jointDir < 0.0f) transformedLocalAttachPos.X = -transformedLocalAttachPos.X; - - //transformedLocalAttachPos = Vector2.Transform(transformedLocalAttachPos, Matrix.CreateRotationZ(attachLimb.Rotation)); + if (jointDir < 0.0f) + { + transformedLocalAttachPos.X = -transformedLocalAttachPos.X; + } float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir; attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle); @@ -274,7 +287,10 @@ namespace Barotrauma // Limb scale is already taken into account when creating the collider. Vector2 colliderFront = collider.GetLocalFront(); - if (jointDir < 0.0f) colliderFront.X = -colliderFront.X; + if (jointDir < 0.0f) + { + colliderFront.X = -colliderFront.X; + } collider.SetTransform(attachPos + attachSurfaceNormal * colliderFront.Length(), MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2); var colliderJoint = new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index c5ab0ea33..b1fcd4848 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -375,9 +375,7 @@ namespace Barotrauma } } - StreamWriter file = new StreamWriter(@"NPCConversations.csv"); - file.WriteLine(sb.ToString()); - file.Close(); + File.WriteAllText("NPCConversations.csv", sb.ToString()); } private static void WriteConversation(System.Text.StringBuilder sb, NPCConversation conv, int depthIndex) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index bf6fa27d1..42112ccfd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -118,7 +118,6 @@ namespace Barotrauma public void TryComplete(float deltaTime) { if (isCompleted) { return; } - //if (Abandon && !IsLoop && subObjectives.None()) { return; } if (CheckState()) { return; } // Not ready -> act (can't do foreach because it's possible that the collection is modified in event callbacks. for (int i = 0; i < subObjectives.Count; i++) @@ -201,7 +200,7 @@ namespace Barotrauma } else { - Priority = CumulatedDevotion * PriorityModifier; + Priority = CumulatedDevotion; } return Priority; } @@ -211,7 +210,7 @@ namespace Barotrauma var currentObjective = objectiveManager.CurrentObjective; if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this))) { - CumulatedDevotion += Devotion * PriorityModifier * deltaTime; + CumulatedDevotion += Devotion * deltaTime; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index e45d30e08..74e1ffd5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -20,6 +20,7 @@ namespace Barotrauma { if (battery == null) { return false; } var item = battery.Item; + if (item.NonInteractable) { return false; } if (item.Submarine == null) { return false; } if (item.CurrentHull == null) { return false; } if (item.Submarine.TeamID != character.TeamID) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index f454f4704..c68b18bdc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -121,6 +121,12 @@ namespace Barotrauma protected override bool Check() { + if (initialMode == CombatMode.Offensive && Mode != CombatMode.Offensive) + { + Abandon = true; + SteeringManager.Reset(); + return false; + } bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0); if (completed) { @@ -465,7 +471,6 @@ namespace Barotrauma SteeringManager.Reset(); return; } - retreatTarget = null; RemoveSubObjective(ref retreatObjective); RemoveSubObjective(ref seekAmmunition); @@ -482,9 +487,8 @@ namespace Barotrauma }, onAbandon: () => { - Mode = CombatMode.Defensive; + Abandon = true; SteeringManager.Reset(); - RemoveSubObjective(ref followTargetObjective); }); if (followTargetObjective != null) { @@ -593,10 +597,7 @@ namespace Barotrauma private void Attack(float deltaTime) { - float squaredDistance = Vector2.DistanceSquared(character.Position, Enemy.Position); character.CursorPosition = Enemy.Position; - float engageDistance = 500; - if (character.CurrentHull != Enemy.CurrentHull && squaredDistance > engageDistance * engageDistance) { return; } if (!character.CanSeeCharacter(Enemy)) { return; } if (Weapon.RequireAimToUse) { @@ -604,7 +605,7 @@ namespace Barotrauma if (SteeringManager == PathSteering) { var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor; - if (door != null && !door.IsOpen) + if (door != null && !door.IsOpen && !door.IsBroken) { isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents(true).Any(); } @@ -626,7 +627,7 @@ namespace Barotrauma } if (WeaponComponent is MeleeWeapon meleeWeapon) { - if (squaredDistance <= meleeWeapon.Range * meleeWeapon.Range) + if (Vector2.DistanceSquared(character.Position, Enemy.Position) <= meleeWeapon.Range * meleeWeapon.Range) { character.SetInput(InputType.Shoot, false, true); Weapon.Use(deltaTime, character); @@ -636,7 +637,7 @@ namespace Barotrauma { if (WeaponComponent is RepairTool repairTool) { - if (squaredDistance > repairTool.Range * repairTool.Range) { return; } + if (Vector2.DistanceSquared(character.Position, Enemy.Position) > repairTool.Range * repairTool.Range) { return; } } if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index a77455b67..42f48b0da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -158,7 +158,7 @@ namespace Barotrauma Abandon = true; }, onCompleted: () => { - if (getItemObjective.TargetItem != null) + if (getItemObjective?.TargetItem != null) { containedItems.Add(getItemObjective.TargetItem); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 9b59a27a4..b6fb5ebce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -106,7 +106,7 @@ namespace Barotrauma if (SteeringManager == PathSteering) { var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor; - if (door != null && !door.IsOpen) + if (door != null && !door.IsOpen && !door.IsBroken) { isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents(true).Any(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 38a791318..9d9f74c13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -1,9 +1,4 @@ -using Barotrauma.Items.Components; -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; -using Barotrauma.Extensions; +using System.Collections.Generic; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index f3c677359..0cc79ce1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -43,7 +43,7 @@ namespace Barotrauma } if (character.CurrentHull == null) { - Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100; + Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo && HumanAIController.HasDivingSuit(character) ? 0 : 100; } else { @@ -83,8 +83,9 @@ namespace Barotrauma else { float dangerFactor = (100 - currenthullSafety) / 100; - Priority = Math.Min(Priority + dangerFactor * priorityIncrease * deltaTime, 100); + Priority += dangerFactor * priorityIncrease * deltaTime; } + Priority = MathHelper.Clamp(Priority, 0, 100); } } @@ -93,34 +94,39 @@ namespace Barotrauma protected override void Act(float deltaTime) { var currentHull = character.CurrentHull; - bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit); - bool needsEquipment = false; - if (needsDivingSuit) + bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0; + if (!dangerousPressure) { - needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold); - } - else if (needsDivingGear) - { - needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold); - } - if (needsEquipment && divingGearObjective == null && !character.LockHands) - { - RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref divingGearObjective, - constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), - onAbandon: () => - { - searchHullTimer = Math.Min(1, searchHullTimer); + // Don't try to seek diving gear if the pressure is dangerous. Just get out. + bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit); + bool needsEquipment = false; + if (needsDivingSuit) + { + needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold); + } + else if (needsDivingGear) + { + needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold); + } + if (needsEquipment && divingGearObjective == null && !character.LockHands) + { + RemoveSubObjective(ref goToObjective); + TryAddSubObjective(ref divingGearObjective, + constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), + onAbandon: () => + { + searchHullTimer = Math.Min(1, searchHullTimer); // Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again. }, - onCompleted: () => - { - resetPriority = true; - searchHullTimer = Math.Min(1, searchHullTimer); - RemoveSubObjective(ref divingGearObjective); - }); + onCompleted: () => + { + resetPriority = true; + searchHullTimer = Math.Min(1, searchHullTimer); + RemoveSubObjective(ref divingGearObjective); + }); + } } - else if (divingGearObjective == null || !divingGearObjective.CanBeCompleted) + if (divingGearObjective == null || !divingGearObjective.CanBeCompleted) { if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index e58862a03..995d71611 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -37,7 +37,7 @@ namespace Barotrauma protected override float TargetEvaluation() { - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective(), onlyBots: true); + int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); int totalLeaks = Targets.Count(); if (totalLeaks == 0) { return 0; } int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 14f746626..06af9fd06 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -20,7 +21,9 @@ namespace Barotrauma //can be either tags or identifiers private string[] itemIdentifiers; public IEnumerable Identifiers => itemIdentifiers; - private Item targetItem, moveToTarget, rootContainer; + + private Item targetItem; + private ISpatialEntity moveToTarget; private bool isDoneSeeking; public Item TargetItem => targetItem; private int currSearchIndex; @@ -29,6 +32,8 @@ namespace Barotrauma private float currItemPriority; private bool checkInventory; + public static float DefaultReach = 100; + public bool AllowToFindDivingGear { get; set; } = true; public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) @@ -37,6 +42,7 @@ namespace Barotrauma currSearchIndex = -1; this.equip = equip; this.targetItem = targetItem; + moveToTarget = targetItem?.GetRootInventoryOwner(); } public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1) @@ -62,8 +68,7 @@ namespace Barotrauma if (item != null) { targetItem = item; - rootContainer = item.GetRootContainer(); - moveToTarget = rootContainer ?? item; + moveToTarget = item.GetRootInventoryOwner(); } return item != null; } @@ -86,6 +91,15 @@ namespace Barotrauma } if (!isDoneSeeking) { + bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0; + if (dangerousPressure) + { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Seeking item aborted, because the pressure is dangerous.", Color.Yellow); +#endif + Abandon = true; + return; + } FindTargetItem(); objectiveManager.GetObjective().Wander(deltaTime); return; @@ -108,7 +122,26 @@ namespace Barotrauma Reset(); return; } - if (character.CanInteractWith(targetItem, out _, checkLinked: false)) + bool canInteract = false; + if (moveToTarget is Character c) + { + if (character == c) + { + canInteract = true; + moveToTarget = null; + } + else + { + character.SelectCharacter(c); + canInteract = character.CanInteractWith(c, maxDist: DefaultReach); + character.DeselectCharacter(); + } + } + else if (moveToTarget is Item parentItem) + { + canInteract = character.CanInteractWith(parentItem, out _, checkLinked: false); + } + if (canInteract) { var pickable = targetItem.GetComponent(); if (pickable == null) @@ -173,17 +206,17 @@ namespace Barotrauma } } } - else + else if (moveToTarget != null) { TryAddSubObjective(ref goToObjective, constructor: () => { - return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear) + return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach) { // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) - abortCondition = () => targetItem == null || targetItem.GetRootContainer() != rootContainer, + abortCondition = () => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget, DialogueIdentifier = "dialogcannotreachtarget", - TargetName = moveToTarget.Name + TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString() }; }, onAbandon: () => @@ -212,9 +245,9 @@ namespace Barotrauma { currSearchIndex++; var item = Item.ItemList[currSearchIndex]; - if (item.Submarine == null) { continue; } - if (item.CurrentHull == null) { continue; } - if (item.Submarine.TeamID != character.TeamID) { continue; } + Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine; + if (itemSub == null) { continue; } + if (itemSub.TeamID != character.TeamID) { continue; } if (!CheckItem(item)) { continue; } if (ignoredContainerIdentifiers != null && item.Container != null) { @@ -222,8 +255,8 @@ namespace Barotrauma } if (character.Submarine != null) { - if (item.Submarine.Info.Type != character.Submarine.Info.Type) { continue; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } + if (itemSub.Info.Type != character.Submarine.Info.Type) { continue; } + if (character.Submarine.GetConnectedSubs().None(s => s == itemSub && itemSub.TeamID == character.TeamID && itemSub.Info.Type == character.Submarine.Info.Type)) { continue; } } if (character.IsItemTakenBySomeoneElse(item)) { continue; } float itemPriority = 1; @@ -231,8 +264,8 @@ namespace Barotrauma { itemPriority = GetItemPriority(item); } - Item rootContainer = item.GetRootContainer(); - Vector2 itemPos = (rootContainer ?? item).WorldPosition; + Entity rootInventoryOwner = item.GetRootInventoryOwner(); + Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y); yDist = yDist > 100 ? yDist * 5 : 0; float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist; @@ -243,8 +276,7 @@ namespace Barotrauma if (itemPriority < currItemPriority) { continue; } currItemPriority = itemPriority; targetItem = item; - moveToTarget = rootContainer ?? item; - this.rootContainer = rootContainer; + moveToTarget = rootInventoryOwner ?? item; } if (currSearchIndex >= Item.ItemList.Count - 1) { @@ -293,7 +325,6 @@ namespace Barotrauma RemoveSubObjective(ref goToObjective); targetItem = null; moveToTarget = null; - rootContainer = null; isDoneSeeking = false; currSearchIndex = 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index e2f0577a2..43319c633 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -80,21 +80,28 @@ namespace Barotrauma this.repeat = repeat; waitUntilPathUnreachable = 3.0f; this.getDivingGearIfNeeded = getDivingGearIfNeeded; - CloseEnough = closeEnough; if (Target is Item i) { CloseEnough = Math.Max(CloseEnough, i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2); } + else if (Target is Character) + { + CloseEnough = Math.Max(closeEnough, AIObjectiveGetItem.DefaultReach); + } + else + { + CloseEnough = closeEnough; + } } private void SpeakCannotReach() { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow); #endif if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null) { - string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, true); + string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character)); if (msg != null) { character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f); @@ -213,10 +220,19 @@ namespace Barotrauma return; } } - if (repeat && IsCloseEnough) + if (repeat) { - OnCompleted(); - return; + if (IsCloseEnough) + { + if (requiredCondition == null || requiredCondition()) + { + if (character.CanSeeTarget(Target)) + { + OnCompleted(); + return; + } + } + } } if (SteeringManager == PathSteering) { @@ -244,7 +260,7 @@ namespace Barotrauma } } - private Hull GetTargetHull() + public Hull GetTargetHull() { if (Target is Hull h) { @@ -284,13 +300,7 @@ namespace Barotrauma //otherwise characters can let go of the ladders too soon once they're close enough to the target if (PathSteering.CurrentPath.NextNode != null) { return false; } } - - bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough; - if (closeEnough) - { - closeEnough = !(Target is Character) || Target is Character c && c.CurrentHull == character.CurrentHull; - } - return closeEnough; + return Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough; } } @@ -326,7 +336,9 @@ namespace Barotrauma } else if (Target is Character targetCharacter) { - if (character.CanInteractWith(targetCharacter, CloseEnough)) { IsCompleted = true; } + character.SelectCharacter(targetCharacter); + if (character.CanInteractWith(targetCharacter, skipDistanceCheck: true)) { IsCompleted = true; } + character.DeselectCharacter(); } else { @@ -338,6 +350,16 @@ namespace Barotrauma return IsCompleted; } + protected override void OnAbandon() + { + StopMovement(); + if (SteeringManager == PathSteering) + { + PathSteering.ResetPath(); + } + base.OnAbandon(); + } + private void StopMovement() { character.AIController.SteeringManager.Reset(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 2a2a9be3b..ba35ef7a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -35,41 +34,43 @@ namespace Barotrauma { standStillTimer = Rand.Range(-10.0f, 10.0f); walkDuration = Rand.Range(0.0f, 10.0f); + CalculatePriority(); } protected override bool Check() => false; public override bool CanBeCompleted => true; - public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } + public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); } private float randomTimer; private float randomUpdateInterval = 5; public float Random { get; private set; } - public void CalculatePriority() + public void CalculatePriority(float max = 0) { - Random = Rand.Range(0.5f, 1.5f); - randomTimer = randomUpdateInterval; - float max = Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100); - float initiative = character.GetSkillLevel("initiative"); - Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random)); + //Random = Rand.Range(0.5f, 1.5f); + //randomTimer = randomUpdateInterval; + //max = max > 0 ? max : Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100); + //float initiative = character.GetSkillLevel("initiative"); + //Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random)); + Priority = 1; } public override float GetPriority() => Priority; public override void Update(float deltaTime) { - if (objectiveManager.CurrentObjective == this) - { - if (randomTimer > 0) - { - randomTimer -= deltaTime; - } - else - { - CalculatePriority(); - } - } + //if (objectiveManager.CurrentObjective == this) + //{ + // if (randomTimer > 0) + // { + // randomTimer -= deltaTime; + // } + // else + // { + // CalculatePriority(); + // } + //} } protected override void Act(float deltaTime) @@ -129,7 +130,7 @@ namespace Barotrauma //choose a random available hull currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); bool isCurrentHullAllowed = !IsForbidden(character.CurrentHull); - var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, nodeFilter: node => + var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node => { if (node.Waypoint.CurrentHull == null) { return false; } // Check that there is no unsafe or forbidden hulls on the way to the target diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index d2c3f3a6a..7d51adb9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -47,7 +47,7 @@ namespace Barotrauma public override bool AllowSubObjectiveSorting => true; public virtual bool InverseTargetEvaluation => false; - public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } + public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } public override void Update(float deltaTime) { @@ -204,7 +204,7 @@ namespace Barotrauma { Objectives.Remove(target); ignoreList.Add(target); - targetUpdateTimer = 0; + targetUpdateTimer = Math.Min(0.1f, targetUpdateTimer); }; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 8da13ea1e..89e2d3dda 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -14,8 +14,11 @@ namespace Barotrauma public const float OrderPriority = 70; public const float RunPriority = 50; // Constantly increases the priority of the selected objective, unless overridden - public const float baseDevotion = 3; + public const float baseDevotion = 5; + /// + /// Excluding the current order. + /// public List Objectives { get; private set; } = new List(); private readonly Character character; @@ -88,8 +91,25 @@ namespace Barotrauma public Dictionary DelayedObjectives { get; private set; } = new Dictionary(); + private void ClearIgnored() + { + if (character.AIController is HumanAIController humanAi) + { + humanAi.UnreachableHulls.Clear(); + humanAi.IgnoredItems.Clear(); + } + } + public void CreateAutonomousObjectives() { + if (character.IsDead) + { +#if DEBUG + DebugConsole.ThrowError("Attempted to create autonomous orders for a dead character"); +#else + return; +#endif + } foreach (var delayedObjective in DelayedObjectives) { CoroutineManager.StopCoroutines(delayedObjective.Value); @@ -99,15 +119,15 @@ namespace Barotrauma AddObjective(new AIObjectiveFindSafety(character, this)); AddObjective(new AIObjectiveIdle(character, this)); int objectiveCount = Objectives.Count; - foreach (var automaticOrder in character.Info.Job.Prefab.AutomaticOrders) + foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjective) { - var orderPrefab = Order.GetPrefab(automaticOrder.identifier); - if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{automaticOrder.identifier}'"); } + var orderPrefab = Order.GetPrefab(autonomousObjective.identifier); + if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); } var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, false)?.GetRandom() : null; var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character); if (order == null) { continue; } - var objective = CreateObjective(order, automaticOrder.option, character, automaticOrder.priorityModifier); + var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier); if (objective != null && objective.CanBeCompleted) { AddObjective(objective, delay: Rand.Value() / 2); @@ -160,7 +180,7 @@ namespace Barotrauma { previousObjective?.OnDeselected(); CurrentObjective?.OnSelected(); - GetObjective().CalculatePriority(); + GetObjective().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0)); } return CurrentObjective; } @@ -172,7 +192,21 @@ namespace Barotrauma public void UpdateObjectives(float deltaTime) { - CurrentOrder?.Update(deltaTime); + if (CurrentOrder != null) + { +#if DEBUG + // Note: don't automatically remove orders here. Removing orders needs to be done via dismissing. + if (CurrentOrder.IsCompleted) + { + DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red); + } + else if (!CurrentOrder.CanBeCompleted) + { + DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag}, CANNOT BE COMPLETED.", Color.Red); + } +#endif + CurrentOrder.Update(deltaTime); + } if (WaitTimer > 0) { WaitTimer -= deltaTime; @@ -195,7 +229,7 @@ namespace Barotrauma #endif Objectives.Remove(objective); } - else if (objective != CurrentOrder) + else { objective.Update(deltaTime); } @@ -233,7 +267,16 @@ namespace Barotrauma public void SetOrder(Order order, string option, Character orderGiver) { - CurrentOrder = CreateObjective(order, option, orderGiver); + if (character.IsDead) + { +#if DEBUG + DebugConsole.ThrowError("Attempted to set an order for a dead character"); +#else + return; +#endif + } + ClearIgnored(); + CurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false); if (CurrentOrder == null) { // Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding) @@ -245,7 +288,7 @@ namespace Barotrauma } } - public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1) + public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1) { if (order == null) { return null; } AIObjective newObjective; @@ -284,14 +327,21 @@ namespace Barotrauma newObjective = new AIObjectiveRepairItems(character, this, priorityModifier: priorityModifier, prioritizedItem: order.TargetEntity as Item) { RelevantSkill = order.AppropriateSkill, - RequireAdequateSkills = option == "jobspecific" + RequireAdequateSkills = isAutonomous }; break; case "pumpwater": if (order.TargetItemComponent is Pump targetPump) { - newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier); - // newObjective.Completed += DismissSelf; + if (order.TargetItemComponent.Item.NonInteractable) { return null; } + newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier) + { + IsLoop = true, + Override = orderGiver != null && orderGiver.IsPlayer + }; + // ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs, + // if we want that the bot just switches the pump on/off and continues doing something else. + // If we want that the bot does the objective and then forgets about it, I think we could do the same plus dismiss when the bot is done. } else { @@ -306,9 +356,11 @@ namespace Barotrauma break; case "steer": var steering = (order?.TargetEntity as Item)?.GetComponent(); - if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; + if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; } if (order.TargetItemComponent == null) { return null; } - newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) + if (order.TargetItemComponent.Item.NonInteractable) { return null; } + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, + requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { IsLoop = true, // Don't override unless it's an order by a player @@ -317,12 +369,15 @@ namespace Barotrauma break; default: if (order.TargetItemComponent == null) { return null; } - newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) + if (order.TargetItemComponent.Item.NonInteractable) { return null; } + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, + requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { IsLoop = true, // Don't override unless it's an order by a player Override = orderGiver != null && orderGiver.IsPlayer }; + if (newObjective.Abandon) { return null; } break; } return newObjective; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 73d597ec5..471d776e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -29,6 +28,7 @@ namespace Barotrauma public ItemComponent GetTarget() => useController ? controller : component; public Func completionCondition; + private bool isDoneOperating; public override float GetPriority() { @@ -47,17 +47,41 @@ namespace Barotrauma { Priority = AIObjectiveManager.OrderPriority; } - Item targetItem = GetTarget()?.Item; + ItemComponent target = GetTarget(); + Item targetItem = target?.Item; if (targetItem == null) { #if DEBUG - DebugConsole.ThrowError("Item or component of AI Objective Operate item wass null. This shouldn't happen."); + DebugConsole.ThrowError("Item or component of AI Objective Operate item was null. This shouldn't happen."); #endif Abandon = true; Priority = 0; - return 0.0f; + return Priority; } - if (targetItem.CurrentHull == null || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(GetTarget(), out _)) + var reactor = component?.Item.GetComponent(); + if (reactor != null) + { + switch (Option) + { + case "shutdown": + if (!reactor.PowerOn) + { + Priority = 0; + return Priority; + } + break; + case "powerup": + // Check that we don't already have another order that is targeting the same item. + // Without this the autonomous objective will tell the bot to turn the reactor on again. + if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target) + { + Priority = 0; + return Priority; + } + break; + } + } + if (targetItem.CurrentHull == null || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(target, out _)) { Priority = 0; } @@ -68,26 +92,33 @@ namespace Barotrauma else { float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier); - float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90); + float max = objectiveManager.CurrentOrder == this ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1; Priority = MathHelper.Clamp(value, 0, max); } } return Priority; } - public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1) + public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, + Entity operateTarget = null, bool useController = false, ItemComponent controller = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier, option) { - this.component = item ?? throw new System.ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target."); + component = item ?? throw new ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target."); this.requireEquip = requireEquip; this.operateTarget = operateTarget; this.useController = useController; - if (useController) + if (useController) { this.controller = controller ?? component?.Item?.FindController(); } + var target = GetTarget(); + if (target == null) { - //try finding the controller with the simpler non-recursive method first - controller = - component.Item.GetConnectedComponents().FirstOrDefault() ?? - component.Item.GetConnectedComponents(recursive: true).FirstOrDefault(); +#if DEBUG + throw new Exception("target null"); +#endif + Abandon = true; + } + else if (target.Item.NonInteractable) + { + Abandon = true; } } @@ -122,7 +153,7 @@ namespace Barotrauma } if (component.AIOperate(deltaTime, character, this)) { - IsCompleted = completionCondition == null || completionCondition(); + isDoneOperating = completionCondition == null || completionCondition(); } } else @@ -189,12 +220,12 @@ namespace Barotrauma } if (component.AIOperate(deltaTime, character, this)) { - IsCompleted = completionCondition == null || completionCondition(); + isDoneOperating = completionCondition == null || completionCondition(); } } } } - protected override bool Check() => IsCompleted && !IsLoop; + protected override bool Check() => isDoneOperating && !IsLoop; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 7e50b0738..a7fe83d4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; -using Barotrauma.Extensions; namespace Barotrauma { @@ -27,6 +26,7 @@ namespace Barotrauma protected override bool Filter(Pump pump) { if (pump == null) { return false; } + if (pump.Item.NonInteractable) { return false; } if (pump.Item.HasTag("ballast")) { return false; } if (pump.Item.Submarine == null) { return false; } if (pump.Item.CurrentHull == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index b01e35deb..fb9745108 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Linq; using Barotrauma.Extensions; -using FarseerPhysics; namespace Barotrauma { @@ -52,12 +51,11 @@ namespace Barotrauma float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist; distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist)); } - float damagePriority = isPriority ? 1 : MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition); - float successFactor = isPriority ? 1 : MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character))); + float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character); float isSelected = IsRepairing ? 50 : 0; float devotion = (CumulatedDevotion + isSelected) / 100; float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90); - Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (damagePriority * distanceFactor * successFactor * PriorityModifier), 0, 1)); + Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); } return Priority; } @@ -150,7 +148,8 @@ namespace Barotrauma { if (character.SelectedConstruction != Item) { - if (!Item.TryInteract(character, true, true)) + if (!Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) && + !Item.TryInteract(character, ignoreRequiredItems: true, forceActionKey: true)) { Abandon = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 1a7867cd1..8271b4af8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Barotrauma.Items.Components; using Barotrauma.Extensions; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -56,7 +57,7 @@ namespace Barotrauma { Objectives.Remove(item); ignoreList.Add(item); - targetUpdateTimer = 0; + targetUpdateTimer = Math.Min(0.1f, targetUpdateTimer); }; } break; @@ -75,13 +76,9 @@ namespace Barotrauma if (item != character.SelectedConstruction) { float condition = item.ConditionPercentage; - if (item.Repairables.All(r => condition >= r.AIRepairThreshold)) { return false; } + if (item.Repairables.All(r => condition >= r.RepairThreshold)) { return false; } } } - if (RequireAdequateSkills) - { - if (item.Repairables.Any(r => !r.HasRequiredSkills(character))) { return false; } - } if (!string.IsNullOrWhiteSpace(RelevantSkill)) { if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; } @@ -96,7 +93,7 @@ namespace Barotrauma // Don't stop fixing until done return 100; } - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective(), onlyBots: true); + int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); int items = Targets.Count; bool anyFixers = otherFixers > 0; float ratio = anyFixers ? items / (float)otherFixers : 1; @@ -111,10 +108,24 @@ namespace Barotrauma // Enough fixers return 0; } - return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio; + if (RequireAdequateSkills) + { + return Targets.Sum(t => GetTargetPriority(t, character)) * ratio; + } + else + { + return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio; + } } } + public static float GetTargetPriority(Item item, Character character) + { + float damagePriority = MathHelper.Lerp(1, 0, item.Condition / item.MaxCondition); + float successFactor = MathHelper.Lerp(0, 1, item.Repairables.Average(r => r.DegreeOfSuccess(character))); + return MathHelper.Lerp(0, 100, MathHelper.Clamp(damagePriority * successFactor, 0, 1)); + } + protected override IEnumerable GetList() => Item.ItemList; protected override AIObjective ObjectiveConstructor(Item item) @@ -126,6 +137,7 @@ namespace Barotrauma public static bool IsValidTarget(Item item, Character character) { if (item == null) { return false; } + if (item.NonInteractable) { return false; } if (item.IsFullCondition) { return false; } if (item.CurrentHull == null) { return false; } if (item.Submarine == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index c7e58a41d..d85f2b39a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -38,7 +38,19 @@ namespace Barotrauma } this.targetCharacter = targetCharacter; } - + + protected override void OnAbandon() + { + character.SelectedCharacter = null; + base.OnAbandon(); + } + + protected override void OnCompleted() + { + character.SelectedCharacter = null; + base.OnCompleted(); + } + protected override void Act(float deltaTime) { if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) @@ -46,16 +58,13 @@ namespace Barotrauma Abandon = true; return; } - if (targetCharacter.SelectedBy != null && targetCharacter.SelectedBy != character) + var otherRescuer = targetCharacter.SelectedBy; + if (otherRescuer != null && otherRescuer != character) { - var otherCharacter = character.SelectedBy; - if (otherCharacter != null) - { - // Someone else is rescuing/holding the target. - Abandon = otherCharacter.IsPlayer || character.GetSkillLevel("medical") < otherCharacter.GetSkillLevel("medical"); - } + // Someone else is rescuing/holding the target. + Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical"); + return; } - if (targetCharacter != character) { // Incapacitated target is not in a safe place -> Move to a safe place first @@ -161,13 +170,23 @@ namespace Barotrauma private readonly List suitableItemIdentifiers = new List(); private readonly List itemNameList = new List(); - private Dictionary currentTreatmentSuitabilities = new Dictionary(); + private readonly Dictionary currentTreatmentSuitabilities = new Dictionary(); private void GiveTreatment(float deltaTime) { + if (targetCharacter == null) + { + string errorMsg = $"{character.Name}: Attempted to update a Rescue objective with no target!"; + DebugConsole.ThrowError(errorMsg); + Abandon = true; + return; + } + + SteeringManager?.Reset(); + if (!targetCharacter.IsPlayer) { // If the target is a bot, don't let it move - targetCharacter.AIController?.SteeringManager.Reset(); + targetCharacter.AIController?.SteeringManager?.Reset(); } if (treatmentTimer > 0.0f) { @@ -182,6 +201,8 @@ namespace Barotrauma //check if we already have a suitable treatment for any of the afflictions foreach (Affliction affliction in GetSortedAfflictions(targetCharacter)) { + if (affliction == null) { throw new Exception("Affliction was null"); } + if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); } foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) { if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f) @@ -258,7 +279,7 @@ namespace Barotrauma ic.PlaySound(ActionType.OnUse, character); #endif ic.WasUsed = true; - ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb); + ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: character); if (ic.DeleteOnUse) { remove = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 4ea96940d..e01280b2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -33,46 +33,31 @@ namespace Barotrauma protected override float TargetEvaluation() { - int otherRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective(), onlyBots: true); - int targetCount = Targets.Count; - bool anyRescuers = otherRescuers > 0; - float ratio = anyRescuers ? targetCount / (float)otherRescuers : 1; - if (objectiveManager.CurrentOrder == this) + if (objectiveManager.CurrentOrder != this) { - return Targets.Min(t => GetVitalityFactor(t)) / ratio; - } - else - { - float multiplier = 1; - if (anyRescuers) + if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious)) { - float mySkill = character.GetSkillLevel("medical"); - int betterRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.Character.Info.Job.GetSkillLevel("medical") >= mySkill, onlyBots: true); - if (targetCount / (float)betterRescuers <= 1) - { - // Enough rescuers - return 100; - } - else - { - bool foundOtherMedics = HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.Info.Job.Prefab.Identifier == "medicaldoctor"); - if (foundOtherMedics) - { - if (character.Info.Job.Prefab.Identifier != "medicaldoctor") - { - // Double the vitality factor -> less likely to take action - multiplier = 2; - } - } - } + // Don't do anything if there's a medic on board and we are not a medic + return 100; } - return Targets.Min(t => GetVitalityFactor(t)) / ratio * multiplier; } + float worstCondition = Targets.Min(t => GetVitalityFactor(t)); + if (Targets.Contains(character)) + { + if (character.Bleeding > 10) + { + // Enforce the highest priority when bleeding out. + worstCondition = 0; + } + // Boost the priority when wounded. + worstCondition /= 2; + } + return worstCondition; } public static float GetVitalityFactor(Character character) { - float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0); + float vitality = character.HealthPercentage - (character.Bleeding * 2) - character.Bloodloss + Math.Min(character.Oxygen, 0); vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis"); return Math.Clamp(vitality, 0, 100); } @@ -92,6 +77,11 @@ namespace Barotrauma if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; } if (!humanAI.ObjectiveManager.IsCurrentOrder()) { + if (!character.IsMedic && target != character) + { + // Don't allow to treat others autonomously + return false; + } // Ignore unsafe hulls, unless ordered if (humanAI.UnsafeHulls.Contains(target.CurrentHull)) { @@ -111,10 +101,10 @@ namespace Barotrauma if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } } - if (!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) + if (target != character &&!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) { // Ignore all concious targets that are currently fighting, fleeing or treating characters - if (targetAI.ObjectiveManager.HasActiveObjective() || + if (targetAI.ObjectiveManager.HasActiveObjective() || targetAI.ObjectiveManager.HasActiveObjective() || targetAI.ObjectiveManager.HasActiveObjective()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index d5ad028e1..d278433e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -85,32 +85,22 @@ namespace Barotrauma //legacy support public readonly string[] AppropriateJobs; public readonly string[] Options; - public readonly string[] OptionNames; + private readonly Dictionary OptionNames; public readonly Dictionary OptionSprites; + private readonly Dictionary minimapIcons; + public Dictionary MinimapIcons => IsPrefab ? minimapIcons : Prefab.minimapIcons; + public readonly float Weight; public readonly bool MustSetTarget; public readonly string AppropriateSkill; - public bool HasOptions - { - get - { - if (IsPrefab) - { - return MustSetTarget || Options.Length > 1; - } - else - { - return Prefab.MustSetTarget || Prefab.Options.Length > 1; - } - } - } + public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1; public bool IsPrefab { get; private set; } public readonly bool MustManuallyAssign; - static Order() + public static void Init() { Prefabs = new Dictionary(); OrderCategoryIcons = new Dictionary>(); @@ -219,25 +209,18 @@ namespace Barotrauma MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false); AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null); - string translatedOptionNames = TextManager.Get("OrderOptions." + Identifier, true); - if (translatedOptionNames == null) + var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', ',') ?? + orderElement.GetAttributeStringArray("optionnames", new string[0]); + OptionNames = new Dictionary(); + for (int i = 0; i < Options.Length && i < optionNames.Length; i++) { - OptionNames = orderElement.GetAttributeStringArray("optionnames", new string[0]); + OptionNames.Add(Options[i], optionNames[i].Trim()); } - else - { - string[] splitOptionNames = translatedOptionNames.Split(',', ','); - OptionNames = new string[Options.Length]; - for (int i = 0; i < Options.Length && i < splitOptionNames.Length; i++) - { - OptionNames[i] = splitOptionNames[i].Trim(); - } - } - - if (OptionNames.Length != Options.Length) + if (OptionNames.Count != Options.Length) { DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options."); - OptionNames = Options; + OptionNames.Clear(); + Options.ForEach(o => OptionNames.Add(o, o)); } var spriteElement = orderElement.GetChildElement("sprite"); @@ -261,6 +244,15 @@ namespace Barotrauma } } + minimapIcons = new Dictionary(); + var minimapIconElements = orderElement.GetChildElements("minimapicon"); + foreach (XElement minimapIconElement in minimapIconElements) + { + var id = minimapIconElement.GetAttributeString("id", null); + if (string.IsNullOrWhiteSpace(id)) { continue; } + minimapIcons.Add(id, new Sprite(minimapIconElement.GetChildElement("sprite"), lazyLoad: true)); + } + IsPrefab = true; MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); } @@ -268,7 +260,7 @@ namespace Barotrauma /// /// Constructor for order instances /// - public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null) + public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) { Prefab = prefab; @@ -293,27 +285,23 @@ namespace Barotrauma TargetEntity = targetEntity; if (targetItem != null) { - if (UseController) { ConnectedController = FindController(targetItem); } + if (UseController) + { + ConnectedController = targetItem.Item?.FindController(); + if (ConnectedController == null) + { +#if DEBUG + throw new Exception("Tried to use controller, but couldn't find one"); +#endif + UseController = false; + } + } TargetEntity = targetItem.Item; TargetItemComponent = targetItem; } IsPrefab = false; } - - private Controller FindController(ItemComponent targetComponent) - { - if (targetComponent?.Item == null) { return null; } - //try finding the controller with the simpler non-recursive method first - return targetComponent.Item.GetConnectedComponents().FirstOrDefault() ?? - targetComponent.Item.GetConnectedComponents(recursive: true).FirstOrDefault(); - } - - private bool TryFindController(ItemComponent targetComponent, out Controller controller) - { - controller = FindController(targetComponent); - return controller != null; - } public bool HasAppropriateJob(Character character) { @@ -368,7 +356,7 @@ namespace Barotrauma matchingItems.RemoveAll(it => it.NonInteractable); if (UseController) { - matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType && TryFindController(c, out _))); + matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType) && !i.TryFindController(out _)); } } return matchingItems; @@ -381,5 +369,16 @@ namespace Barotrauma Submarine.MainSub; return GetMatchingItems(submarine, mustBelongToPlayerSub); } + + public string GetOptionName(string id) + { + return Prefab == null ? OptionNames[id] : Prefab.OptionNames[id]; + } + + public string GetOptionName(int index) + { + if (index < 0 || index >= Options.Length) { return null; } + return GetOptionName(Options[index]); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 405a12b9c..8ae8ce48a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -46,7 +46,7 @@ namespace Barotrauma var nodes = new Dictionary(); foreach (WayPoint wayPoint in wayPoints) { - if (wayPoint == null) continue; + if (wayPoint == null) { continue; } if (nodes.ContainsKey(wayPoint.ID)) { #if DEBUG @@ -63,7 +63,7 @@ namespace Barotrauma { PathNode connectedNode = null; nodes.TryGetValue(linked.ID, out connectedNode); - if (connectedNode == null) continue; + if (connectedNode == null) { continue; } node.Value.connections.Add(connectedNode); } @@ -107,17 +107,17 @@ namespace Barotrauma void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { - if (Submarine.Unloading) return; + if (Submarine.Unloading) { return; } var waypoints = sender as IEnumerable; foreach (MapEntity me in waypoints) { WayPoint wp = me as WayPoint; - if (me == null) continue; + if (me == null) { continue; } var node = nodes.Find(n => n.Waypoint == wp); - if (node == null) return; + if (node == null) { return; } if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) { @@ -136,10 +136,10 @@ namespace Barotrauma for (int i = 0; i < wp.linkedTo.Count; i++) { WayPoint connected = wp.linkedTo[i] as WayPoint; - if (connected == null) continue; + if (connected == null) { continue; } //already connected, continue - if (node.connections.Any(n => n.Waypoint == connected)) continue; + if (node.connections.Any(n => n.Waypoint == connected)) { continue; } var matchingNode = nodes.Find(n => n.Waypoint == connected); if (matchingNode == null) @@ -201,8 +201,8 @@ namespace Barotrauma if (body != null) { //if (body.UserData is Submarine) continue; - if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue; - if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue; + if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; } + if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } } } @@ -236,9 +236,9 @@ namespace Barotrauma if (InsideSubmarine) { //much higher cost to waypoints that are outside - if (node.Waypoint.CurrentHull == null) dist *= 10.0f; + if (node.Waypoint.CurrentHull == null) { dist *= 10.0f; } //avoid stopping at a doorway - if (node.Waypoint.ConnectedDoor != null) dist *= 10.0f; + if (node.Waypoint.ConnectedDoor != null) { dist *= 10.0f; } } if (dist < closestDist || endNode == null) { @@ -251,8 +251,8 @@ namespace Barotrauma if (body != null) { //if (body.UserData is Submarine) continue; - if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue; - if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue; + if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; } + if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs index 86fbdfc8d..79c8dbefa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs @@ -9,12 +9,31 @@ namespace Barotrauma int currentIndex; + private float? totalLength; + public bool Unreachable { get; set; } + public float TotalLength + { + get + { + if (Unreachable) { return float.PositiveInfinity; } + if (!totalLength.HasValue) + { + totalLength = 0.0f; + for (int i = 0; i < nodes.Count - 1; i++) + { + totalLength += Vector2.Distance(nodes[i].WorldPosition, nodes[i + 1].WorldPosition); + } + } + return totalLength.Value; + } + } + public SteeringPath(bool unreachable = false) { nodes = new List(); @@ -23,10 +42,10 @@ namespace Barotrauma public void AddNode(WayPoint node) { - if (node == null) return; + if (node == null) { return; } nodes.Add(node); - if (node.CurrentHull == null) HasOutdoorsNodes = true; + if (node.CurrentHull == null) { HasOutdoorsNodes = true; } } public bool HasOutdoorsNodes @@ -48,10 +67,10 @@ namespace Barotrauma public WayPoint PrevNode { - get + get { - if (currentIndex-1 < 0 || currentIndex-1 > nodes.Count - 1) return null; - return nodes[currentIndex-1]; + if (currentIndex - 1 < 0 || currentIndex - 1 > nodes.Count - 1) { return null; } + return nodes[currentIndex - 1]; } } @@ -59,7 +78,7 @@ namespace Barotrauma { get { - if (currentIndex < 0 || currentIndex > nodes.Count - 1) return null; + if (currentIndex < 0 || currentIndex > nodes.Count - 1) { return null; } return nodes[currentIndex]; } } @@ -73,7 +92,7 @@ namespace Barotrauma { get { - if (currentIndex+1 < 0 || currentIndex+1 > nodes.Count - 1) return null; + if (currentIndex + 1 < 0 || currentIndex + 1 > nodes.Count - 1) { return null; } return nodes[currentIndex+1]; } } @@ -90,8 +109,8 @@ namespace Barotrauma public WayPoint CheckProgress(Vector2 simPosition, float minSimDistance = 0.1f) { - if (nodes.Count == 0 || currentIndex>nodes.Count-1) return null; - if (Vector2.Distance(simPosition, nodes[currentIndex].SimPosition) < minSimDistance) currentIndex++; + if (nodes.Count == 0 || currentIndex > nodes.Count - 1) { return null; } + if (Vector2.Distance(simPosition, nodes[currentIndex].SimPosition) < minSimDistance) { currentIndex++; } return CurrentNode; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index fe06aa3a4..c2c82c00d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -268,7 +268,26 @@ namespace Barotrauma { if (Config.KillAgentsWhenEntityDies) { - protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null, isNetworkMessage: true)); + protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null)); + if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent)) + { + 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.Equals(Config.OffensiveAgent, StringComparison.OrdinalIgnoreCase)) + { + // 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, Wreck.WorldPosition) < maxDistance * maxDistance) + { + character.Kill(CauseOfDeathType.Unknown, null); + } + } + } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs index 87515b535..9830a8d4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs @@ -18,6 +18,9 @@ namespace Barotrauma [Serialize("", false)] public string DefensiveAgent { get; private set; } + [Serialize("", false)] + public string OffensiveAgent { get; private set; } + [Serialize("", false)] public string Brain { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index e8a4e4ff4..cdd764cc6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -47,12 +47,16 @@ namespace Barotrauma base.Update(deltaTime, cam); if (!Enabled) { return; } - if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated) { return; } + if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated) + { + //don't enable simple physics on dead/incapacitated characters + //the ragdoll controls the movement of incapacitated characters instead of the collider, + //but in simple physics mode the ragdoll would get disabled, causing the character to not move at all + AnimController.SimplePhysicsEnabled = false; + return; + } - //don't enable simple physics on dead/incapacitated characters - //the ragdoll controls the movement of incapacitated characters instead of the collider, - //but in simple physics mode the ragdoll would get disabled, causing the character to not move at all - if (!IsRemotePlayer) + if (!IsRemotePlayer && !(AIController is HumanAIController)) { float characterDist = float.MaxValue; #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 79fea4873..859b76516 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -76,7 +76,8 @@ namespace Barotrauma { if (InWater || !CanWalk) { - return TargetMovement.Length() > (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f; + float avg = (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f; + return TargetMovement.LengthSquared() > avg * avg; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index bcb29ad85..12f9c5bf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -213,7 +213,12 @@ namespace Barotrauma UpdateWalkAnim(deltaTime); } - //don't flip or drag when simply physics is enabled + if (character.SelectedCharacter != null) + { + DragCharacter(character.SelectedCharacter, deltaTime); + } + + //don't flip when simply physics is enabled if (SimplePhysicsEnabled) { return; } if (!character.IsRemotePlayer && (character.AIController == null || character.AIController.CanFlip)) @@ -248,29 +253,33 @@ namespace Barotrauma } } - if (character.SelectedCharacter != null) - { - DragCharacter(character.SelectedCharacter, deltaTime); - } - if (!CurrentFishAnimation.Flip) { return; } if (IsStuck) { return; } if (character.AIController != null && !character.AIController.CanFlip) { return; } flipCooldown -= deltaTime; - - if (TargetDir != Direction.None && TargetDir != dir) + if (TargetDir != Direction.None && TargetDir != dir) { flipTimer += deltaTime; - if ((flipTimer > 0.5f && flipCooldown <= 0.0f) || character.IsRemotePlayer) + // Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example). + float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2; + if (CurrentHull != null) + { + // Enemy movement speeds are halved inside submarines + requiredSpeed /= 2; + } + bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed; + bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y); + if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards)) + || character.IsRemotePlayer) { Flip(); if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) { - Mirror(); + Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true); } flipTimer = 0.0f; - flipCooldown = 1.0f; + flipCooldown = CurrentFishAnimation.FlipCooldown; } } else @@ -295,7 +304,7 @@ namespace Barotrauma if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { //stop dragging if there's something between the pull limb and the target - Vector2 sourceSimPos = mouthLimb.SimPosition; + Vector2 sourceSimPos = SimplePhysicsEnabled ? character.SimPosition : mouthLimb.SimPosition; Vector2 targetSimPos = target.SimPosition; if (character.Submarine != null && character.SelectedCharacter.Submarine == null) { @@ -317,7 +326,7 @@ namespace Barotrauma float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10); eatTimer += deltaTime * eatSpeed; - Vector2 mouthPos = GetMouthPosition().Value; + Vector2 mouthPos = SimplePhysicsEnabled ? character.SimPosition : GetMouthPosition().Value; Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition; Vector2 limbDiff = attackSimPosition - mouthPos; @@ -525,6 +534,7 @@ namespace Barotrauma foreach (var limb in Limbs) { + if (limb.IsSevered) { continue; } if (Math.Abs(limb.Params.ConstantTorque) > 0) { limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true); @@ -550,10 +560,12 @@ namespace Barotrauma for (int i = 0; i < Limbs.Length; i++) { - if (Limbs[i].SteerForce <= 0.0f) { continue; } + var limb = Limbs[i]; + if (limb.IsSevered) { continue; } + if (limb.SteerForce <= 0.0f) { continue; } if (!Collider.PhysEnabled) { continue; } - Vector2 pullPos = Limbs[i].PullJointWorldAnchorA; - Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass * Math.Max(character.SpeedMultiplier, 1), pullPos); + Vector2 pullPos = limb.PullJointWorldAnchorA; + limb.body.ApplyForce(movement * limb.SteerForce * limb.Mass * Math.Max(character.SpeedMultiplier, 1), pullPos); } Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition; @@ -604,6 +616,14 @@ namespace Barotrauma float stepLift = TargetMovement.X == 0.0f ? 0 : (float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100); + float limpAmount = character.GetLegPenalty(); + if (limpAmount > 0) + { + float walkPosX = (float)Math.Cos(WalkPos); + //make the footpos oscillate when limping + limpAmount = Math.Max(Math.Abs(walkPosX) * limpAmount, 0.0f) * Math.Min(Math.Abs(TargetMovement.X), 0.3f) * Dir; + } + Limb torso = GetLimb(LimbType.Torso); if (torso != null) { @@ -613,7 +633,7 @@ namespace Barotrauma } if (TorsoPosition.HasValue) { - Vector2 pos = colliderBottom + new Vector2(0, TorsoPosition.Value + stepLift); + Vector2 pos = colliderBottom + new Vector2(limpAmount, TorsoPosition.Value + stepLift); if (torso != mainLimb) { @@ -635,7 +655,7 @@ namespace Barotrauma } if (HeadPosition.HasValue) { - Vector2 pos = colliderBottom + new Vector2(0, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier); + Vector2 pos = colliderBottom + new Vector2(limpAmount, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier); if (head != mainLimb) { @@ -670,6 +690,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } if (Math.Abs(limb.Params.ConstantTorque) > 0) { limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true); @@ -766,6 +787,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } #if CLIENT if (limb.LightSource != null) { @@ -821,6 +843,7 @@ namespace Barotrauma base.Flip(); foreach (Limb l in Limbs) { + if (l.IsSevered) { continue; } if (!l.DoesFlip) { continue; } if (RagdollParams.IsSpritesheetOrientationHorizontal) { @@ -838,10 +861,13 @@ namespace Barotrauma foreach (Limb l in Limbs) { + if (l.IsSevered) { continue; } + TrySetLimbPosition(l, centerOfMass, new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y), lerp); + l.body.PositionSmoothingFactor = 0.8f; if (!l.DoesFlip) { continue; } @@ -862,7 +888,7 @@ namespace Barotrauma if (diff < 100.0f) { character.SelectedCharacter.AnimController.SetPosition( - new Vector2(centerOfMass.X - diff, character.SelectedCharacter.SimPosition.Y), lerp: true); + new Vector2(centerOfMass.X - diff, character.SelectedCharacter.SimPosition.Y), lerp); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index f1e43f569..29dc9b60d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -323,7 +323,14 @@ namespace Barotrauma levitatingCollider = true; ColliderIndex = Crouching ? 1 : 0; - if (!Crouching && ColliderIndex == 1) Crouching = true; + if (character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false) + { + Crouching = false; + } + else if (!Crouching && ColliderIndex == 1) + { + Crouching = true; + } //stun (= disable the animations) if the ragdoll receives a large enough impact if (strongestImpact > 0.0f) @@ -542,12 +549,6 @@ namespace Barotrauma Limb leftLeg = GetLimb(LimbType.LeftLeg); Limb rightLeg = GetLimb(LimbType.RightLeg); - float limpAmount = - character.CharacterHealth.GetAfflictionStrength("damage", leftFoot, true) + - character.CharacterHealth.GetAfflictionStrength("damage", rightFoot, true) + - character.CharacterHealth.GetAfflictionStrength("spaceherpes"); - limpAmount = MathHelper.Clamp(limpAmount / 100.0f, 0.0f, 1.0f); - float walkCycleMultiplier = 1.0f; if (Stairs != null) { @@ -582,6 +583,11 @@ namespace Barotrauma stepSize.Y *= walkPosY; float footMid = colliderPos.X; + + var herpes = character.CharacterHealth.GetAffliction("spaceherpes", false); + float herpesAmount = herpes == null ? 0 : herpes.Strength / herpes.Prefab.MaxStrength; + float legDamage = character.GetLegPenalty(startSum: -0.1f) * 1.1f; + float limpAmount = MathHelper.Lerp(0, 1, legDamage + herpesAmount); if (limpAmount > 0.0f) { //make the footpos oscillate when limping @@ -652,6 +658,7 @@ namespace Barotrauma (float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100); float y = colliderPos.Y + stepLift; + if (TorsoPosition.HasValue) { y += TorsoPosition.Value; @@ -690,6 +697,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } MoveLimb(limb, limb.SimPosition + move, 15.0f, true); } @@ -947,8 +955,6 @@ namespace Barotrauma torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f); - if (TargetMovement == Vector2.Zero) { return; } - movement = MathUtils.SmoothStep(movement, TargetMovement, 0.3f); if (TorsoAngle.HasValue) @@ -1001,29 +1007,31 @@ namespace Barotrauma } WalkPos += movement.Length(); - legCyclePos += Vector2.Normalize(movement).Length(); + legCyclePos += Math.Min(movement.LengthSquared() + Collider.AngularVelocity, 1.0f); handCyclePos += MathHelper.ToRadians(CurrentSwimParams.HandCycleSpeed) * Math.Sign(movement.X); var waist = GetLimb(LimbType.Waist); footPos = waist == null ? Vector2.Zero : waist.SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength); - Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength / character.SpeedMultiplier) * CurrentSwimParams.LegMoveAmount, 0.0f); + Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount, 0.0f); transformedFootPos = Vector2.Transform(transformedFootPos, Matrix.CreateRotationZ(Collider.Rotation)); + float torque = CurrentSwimParams.FootRotateStrength * character.SpeedMultiplier * (1.2f - character.GetLegPenalty()); if (rightFoot != null && !rightFoot.Disabled) { - FootIK(rightFoot, footPos - transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians); + FootIK(rightFoot, footPos - transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians); } if (leftFoot != null && !leftFoot.Disabled) { - FootIK(leftFoot, footPos + transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians); + FootIK(leftFoot, footPos + transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians); } handPos = (torso.SimPosition + head.SimPosition) / 2.0f; - //at the surface, not moving sideways -> hands just float around - if (!headInWater && TargetMovement.X == 0.0f && TargetMovement.Y > 0) + //at the surface, not moving sideways OR not moving at all + // -> hands just float around + if ((!headInWater && TargetMovement.X == 0.0f && TargetMovement.Y > 0) || TargetMovement.LengthSquared() < 0.001f) { - handPos.X = handPos.X + Dir * 0.6f; + handPos += MathUtils.RotatePoint(Vector2.UnitX * Dir * 0.6f, torso.Rotation); float wobbleAmount = 0.1f; @@ -1060,7 +1068,7 @@ namespace Barotrauma rightHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, rightHandPos.X) : Math.Min(-0.3f, rightHandPos.X); rightHandPos = Vector2.Transform(rightHandPos, rotationMatrix); - HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier); + HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetRightHandPenalty())); } if (leftHand != null && !leftHand.Disabled) @@ -1069,7 +1077,7 @@ namespace Barotrauma leftHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, leftHandPos.X) : Math.Min(-0.3f, leftHandPos.X); leftHandPos = Vector2.Transform(leftHandPos, rotationMatrix); - HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier); + HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty())); } } @@ -1251,31 +1259,39 @@ namespace Barotrauma Limb head = GetLimb(LimbType.Head); Limb torso = GetLimb(LimbType.Torso); - - //if the head is moving, try to protect it with the hands - if (head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered) + + if (head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered) { + //if the head is moving, try to protect it with the hands Limb leftHand = GetLimb(LimbType.LeftHand); Limb rightHand = GetLimb(LimbType.RightHand); //move hands in front of the head in the direction of the movement Vector2 protectPos = head.SimPosition + Vector2.Normalize(head.LinearVelocity); - if (!rightHand.IsSevered) HandIK(rightHand, protectPos, strength * 0.1f); - if (!leftHand.IsSevered) HandIK(leftHand, protectPos, strength * 0.1f); + if (rightHand != null && !rightHand.IsSevered) + { + HandIK(rightHand, protectPos, strength * 0.1f); + } + if (leftHand != null && !leftHand.IsSevered) + { + HandIK(leftHand, protectPos, strength * 0.1f); + } } + if (torso == null) { return; } //attempt to make legs stay in a straight line with the torso to prevent the character from doing a split for (int i = 0; i < 2; i++) { var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh); - if (thigh.IsSevered) continue; + if (thigh == null) { continue; } + if (thigh.IsSevered) { continue; } float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation)); float thighTorque = thighDiff * thigh.Mass * Math.Sign(torso.Rotation - thigh.Rotation) * 5.0f; thigh.body.ApplyTorque(thighTorque * strength); var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg); - if (leg.IsSevered) continue; + if (leg == null || leg.IsSevered) { continue; } float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation)); float legTorque = legDiff * leg.Mass * Math.Sign(torso.Rotation - leg.Rotation) * 5.0f; leg.body.ApplyTorque(legTorque * strength); @@ -1697,7 +1713,7 @@ namespace Barotrauma if (holdable.ControlPose) { - head.body.SmoothRotate(itemAngle); + head?.body.SmoothRotate(itemAngle); if (TargetMovement == Vector2.Zero && inWater) { @@ -1719,13 +1735,13 @@ namespace Barotrauma { if (character.SelectedItems[0] == item) { - if (rightHand.IsSevered) return; + if (rightHand == null || rightHand.IsSevered) { return; } transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0]; itemAngle = (rightHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir); } else if (character.SelectedItems[1] == item) { - if (leftHand.IsSevered) return; + if (leftHand == null || leftHand.IsSevered) { return; } transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; itemAngle = (leftHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir); } @@ -1734,12 +1750,12 @@ namespace Barotrauma { if (character.SelectedItems[0] == item) { - if (rightHand.IsSevered) return; + if (rightHand == null || rightHand.IsSevered) { return; } rightHand.Disabled = true; } if (character.SelectedItems[1] == item) { - if (leftHand.IsSevered) return; + if (leftHand == null || leftHand.IsSevered) { return; } leftHand.Disabled = true; } @@ -1797,17 +1813,14 @@ namespace Barotrauma } } - item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false); + item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false); - if (!isClimbing) + if (!isClimbing && !character.IsIncapacitated) { for (int i = 0; i < 2; i++) { - if (character.SelectedItems[i] != item) continue; - if (itemPos == Vector2.Zero) continue; - + if (character.SelectedItems[i] != item || itemPos == Vector2.Zero) { continue; } Limb hand = (i == 0) ? rightHand : leftHand; - HandIK(hand, transformedHoldPos + transformedHandlePos[i]); } } @@ -1987,6 +2000,8 @@ namespace Barotrauma foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } + bool mirror = false; bool flipAngle = false; bool wrapAngle = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 154402779..8ac4c5480 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -47,7 +47,9 @@ namespace Barotrauma private readonly Queue impactQueue = new Queue(); protected Hull currentHull; - + + private bool accessRemovedCharacterErrorShown; + private Limb[] limbs; public Limb[] Limbs { @@ -55,16 +57,17 @@ namespace Barotrauma { if (limbs == null) { - string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this); -#if DEBUG || UNSTABLE - errorMsg += '\n' + Environment.StackTrace; -#endif - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce( - "Ragdoll.Limbs:AccessRemoved", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace); - + if (!accessRemovedCharacterErrorShown) + { + string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this); + errorMsg += '\n' + Environment.StackTrace; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + "Ragdoll.Limbs:AccessRemoved", + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace); + accessRemovedCharacterErrorShown = true; + } return new Limb[0]; } return limbs; @@ -97,12 +100,17 @@ namespace Barotrauma private bool simplePhysicsEnabled; + public Character Character => character; protected Character character; protected float strongestImpact; private float splashSoundTimer; + //the ragdoll builds a "tolerance" to the flow force when being pushed by water. + //Allows sudden forces (breach, letting water through a door) to heavily push the character around while ensuring flowing water won't make the characters permanently stuck. + private float flowForceTolerance, flowStunTolerance; + //the movement speed of the ragdoll public Vector2 movement; //the target speed towards which movement is interpolated @@ -156,8 +164,8 @@ namespace Barotrauma } set { - if (value == colliderIndex || collider == null) return; - if (value >= collider.Count || value < 0) return; + if (value == colliderIndex || collider == null) { return; } + if (value >= collider.Count || value < 0) { return; } if (collider[colliderIndex].height < collider[value].height) { @@ -165,10 +173,9 @@ namespace Barotrauma pos1.Y -= collider[colliderIndex].height * ColliderHeightFromFloor; Vector2 pos2 = pos1; pos2.Y += collider[value].height * 1.1f; - if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall))) return; + if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall))) { return; } } - Vector2 pos = collider[colliderIndex].SimPosition; pos.Y -= collider[colliderIndex].height * 0.5f; pos.Y += collider[value].height * 0.5f; @@ -216,7 +223,7 @@ namespace Barotrauma mainLimb = torso ?? head; if (mainLimb == null) { - mainLimb = Limbs.FirstOrDefault(); + mainLimb = Limbs.FirstOrDefault(l => !l.IsSevered && !l.ignoreCollisions); } } return mainLimb; @@ -238,13 +245,13 @@ namespace Barotrauma get { return simplePhysicsEnabled; } set { - if (value == simplePhysicsEnabled) return; + if (value == simplePhysicsEnabled) { return; } simplePhysicsEnabled = value; foreach (Limb limb in Limbs) { - if (limb.IsSevered) continue; + if (limb.IsSevered) { continue; } if (limb.body == null) { DebugConsole.ThrowError("Limb has no body! (" + (character != null ? character.Name : "Unknown character") + ", " + limb.type.ToString()); @@ -296,8 +303,6 @@ namespace Barotrauma public float ImpactTolerance => RagdollParams.ImpactTolerance; public bool Draggable => RagdollParams.Draggable; public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine; - public bool CanAttackSubmarine => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Structure)); - public bool CanAttackCharacters => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Character)); public float Dir => dir == Direction.Left ? -1.0f : 1.0f; @@ -324,6 +329,7 @@ namespace Barotrauma Submarine currSubmarine = currentHull?.Submarine; foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } limb.body.Submarine = currSubmarine; } Collider.Submarine = currSubmarine; @@ -377,7 +383,7 @@ namespace Barotrauma foreach (var kvp in items) { int id = kvp.Key.ID; - // This can be the case if we manipulate the ragdoll in runtime (husk appendage, limb severance) + // This can be the case if we manipulate the ragdoll at runtime (husk appendage, limb removal in the character editor) if (id > limbs.Length - 1) { continue; } var limb = limbs[id]; var itemList = kvp.Value; @@ -438,7 +444,7 @@ namespace Barotrauma { foreach (LimbJoint joint in LimbJoints) { - if (GameMain.World.JointList.Contains(joint)) { GameMain.World.Remove(joint); } + if (GameMain.World.JointList.Contains(joint.Joint)) { GameMain.World.Remove(joint.Joint); } } } DebugConsole.Log($"Creating joints from {RagdollParams.Name}."); @@ -523,7 +529,7 @@ namespace Barotrauma public void AddJoint(JointParams jointParams) { LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this); - GameMain.World.Add(joint); + GameMain.World.Add(joint.Joint); for (int i = 0; i < LimbJoints.Length; i++) { if (LimbJoints[i] != null) continue; @@ -606,7 +612,7 @@ namespace Barotrauma limb.Remove(); foreach (LimbJoint limbJoint in attachedJoints) { - GameMain.World.Remove(limbJoint); + GameMain.World.Remove(limbJoint.Joint); } } @@ -706,7 +712,7 @@ namespace Barotrauma float impactDamage = Math.Min((impact - ImpactTolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage); character.LastDamageSource = null; - character.AddDamage(impactPos, new List() { AfflictionPrefab.InternalDamage.Instantiate(impactDamage) }, 0.0f, true); + character.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), 0.0f, true); strongestImpact = Math.Max(strongestImpact, impact - ImpactTolerance); character.ApplyStatusEffects(ActionType.OnImpact, 1.0f); //briefly disable impact damage @@ -720,12 +726,14 @@ namespace Barotrauma ImpactProjSpecific(impact, f1.Body); } - - public void SeverLimbJoint(LimbJoint limbJoint, bool playSound = true) + + private readonly List connectedLimbs = new List(); + private readonly List checkedJoints = new List(); + public bool SeverLimbJoint(LimbJoint limbJoint) { if (!limbJoint.CanBeSevered || limbJoint.IsSevered) { - return; + return false; } limbJoint.IsSevered = true; @@ -738,22 +746,29 @@ namespace Barotrauma limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); - List connectedLimbs = new List(); - List checkedJoints = new List(); - + connectedLimbs.Clear(); + checkedJoints.Clear(); GetConnectedLimbs(connectedLimbs, checkedJoints, MainLimb); foreach (Limb limb in Limbs) { if (connectedLimbs.Contains(limb)) { continue; } limb.IsSevered = true; + if (limb.type == LimbType.RightHand) + { + character.SelectedItems[0]?.Drop(character); + } + else if (limb.type == LimbType.LeftHand) + { + character.SelectedItems[1]?.Drop(character); + } } SeverLimbJointProjSpecific(limbJoint, playSound: true); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.Status }); } + return true; } partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound); @@ -764,7 +779,7 @@ namespace Barotrauma foreach (LimbJoint joint in LimbJoints) { - if (joint.IsSevered || checkedJoints.Contains(joint)) continue; + if (joint.IsSevered || checkedJoints.Contains(joint)) { continue; } if (joint.LimbA == limb) { if (!connectedLimbs.Contains(joint.LimbB)) @@ -861,7 +876,7 @@ namespace Barotrauma { for (int i = 0; i < Limbs.Length; i++) { - if (Limbs[i] == null) continue; + if (Limbs[i] == null) { continue; } Limbs[i].PullJointEnabled = false; } } @@ -982,8 +997,8 @@ namespace Barotrauma { foreach (Limb limb in Limbs) { - if (limb.IsSevered) continue; - if (limb.body.FarseerBody.ContactList == null) continue; + if (limb.IsSevered) { continue; } + if (limb.body.FarseerBody.ContactList == null) { continue; } ContactEdge ce = limb.body.FarseerBody.ContactList; while (ce != null && ce.Contact != null) @@ -995,7 +1010,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb.IsSevered) continue; + if (limb.IsSevered) { continue; } limb.body.LinearVelocity += velocityChange; } @@ -1020,15 +1035,15 @@ namespace Barotrauma Category collisionCategory = (IgnorePlatforms) ? wall | Physics.CollisionProjectile | Physics.CollisionStairs : wall | Physics.CollisionProjectile | Physics.CollisionPlatform | Physics.CollisionStairs; - - if (collisionCategory == prevCollisionCategory) return; + + if (collisionCategory == prevCollisionCategory) { return; } prevCollisionCategory = collisionCategory; Collider.CollidesWith = collisionCategory | Physics.CollisionItemBlocking; foreach (Limb limb in Limbs) { - if (limb.ignoreCollisions || limb.IsSevered) continue; + if (limb.ignoreCollisions || limb.IsSevered) { continue; } try { @@ -1081,8 +1096,6 @@ namespace Barotrauma CheckDistFromCollider(); UpdateCollisionCategories(); - Vector2 flowForce = Vector2.Zero; - FindHull(); PreventOutsideCollision(); @@ -1104,10 +1117,7 @@ namespace Barotrauma } else { - flowForce = GetFlowForce(); - headInWater = false; - inWater = false; if (currentHull.WaterVolume > currentHull.Volume * 0.95f) { @@ -1129,7 +1139,7 @@ namespace Barotrauma if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); } } - float standHeight = + float standHeight = HeadPosition.HasValue ? HeadPosition.Value : TorsoPosition.HasValue ? TorsoPosition.Value : Collider.GetMaxExtent() * 0.5f; @@ -1140,10 +1150,7 @@ namespace Barotrauma } } - if (flowForce.LengthSquared() > 0.001f) - { - Collider.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - } + UpdateHullFlowForces(deltaTime); if (currentHull == null || currentHull.WaterVolume > currentHull.Volume * 0.95f || @@ -1152,7 +1159,6 @@ namespace Barotrauma Collider.ApplyWaterForces(); } - foreach (Limb limb in Limbs) { //find the room which the limb is in @@ -1177,14 +1183,7 @@ namespace Barotrauma if (limb.Position.Y < limbHull.Surface) { limb.inWater = true; - - if (flowForce.LengthSquared() > 0.001f) - { - limb.body.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - } - surfaceY = limbHull.Surface; - if (limb.type == LimbType.Head) { headInWater = true; @@ -1256,6 +1255,12 @@ namespace Barotrauma private int validityResets; private bool CheckValidity() { + if (limbs == null) + { + DebugConsole.ThrowError("Attempted to check the validity of a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this)); + Invalid = true; + return false; + } bool isColliderValid = CheckValidity(Collider); bool limbsValid = true; foreach (Limb limb in limbs) @@ -1278,8 +1283,8 @@ namespace Barotrauma Collider.SetTransform(Vector2.Zero, 0.0f); foreach (Limb limb in Limbs) { - limb.body.SetTransform(Collider.SimPosition, 0.0f); - limb.body.ResetDynamics(); + limb.body?.SetTransform(Collider.SimPosition, 0.0f); + limb.body?.ResetDynamics(); } Frozen = true; } @@ -1353,6 +1358,74 @@ namespace Barotrauma partial void Splash(Limb limb, Hull limbHull); + private void UpdateHullFlowForces(float deltaTime) + { + if (currentHull == null) { return; } + + const float StunForceThreshold = 5.0f; + const float StunDuration = 0.5f; + const float ToleranceIncreaseSpeed = 5.0f; + const float ToleranceDecreaseSpeed = 1.0f; + + //how much distance to a gap affects the force it exerts on the character + const float DistanceFactor = 0.5f; + const float ForceMultiplier = 0.035f; + + Vector2 flowForce = Vector2.Zero; + foreach (Gap gap in Gap.GapList) + { + if (gap.Open <= 0.0f || !gap.linkedTo.Contains(currentHull) || gap.LerpedFlowForce.LengthSquared() < 0.01f) { continue; } + float dist = Vector2.Distance(MainLimb.WorldPosition, gap.WorldPosition) * DistanceFactor; + flowForce += Vector2.Normalize(gap.LerpedFlowForce) * (Math.Max(gap.LerpedFlowForce.Length() - dist, 0.0f) * ForceMultiplier); + } + + //throwing conscious/moving characters around takes more force -> double the flow force + if (character.CanMove) { flowForce *= 2.0f; } + + float flowForceMagnitude = flowForce.Length(); + float limbMultipier = limbs.Count(l => l.inWater) / (float)limbs.Length; + //if the force strong enough, stun the character to let it get thrown around by the water + if ((flowForceMagnitude * limbMultipier) - flowStunTolerance > StunForceThreshold) + { + character.Stun = Math.Max(character.Stun, StunDuration); + flowStunTolerance = Math.Max(flowStunTolerance, flowForceMagnitude); + } + + if (character == Character.Controlled && inWater && Screen.Selected?.Cam != null) + { + float shakeStrength = Math.Min(flowForceMagnitude / 10.0f, 5.0f) * limbMultipier; + Screen.Selected.Cam.Shake = Math.Max(Screen.Selected.Cam.Shake, shakeStrength); + } + + if (flowForceMagnitude > 0.0001f) + { + flowForce = Vector2.Normalize(flowForce) * Math.Max(flowForceMagnitude - flowForceTolerance, 0.0f); + } + + if (flowForceTolerance <= flowForceMagnitude * 1.5f && inWater) + { + //build up "tolerance" to the flow force + //ensures the character won't get permanently stuck by forces, while allowing sudden changes in flow to push the character hard + flowForceTolerance += deltaTime * ToleranceIncreaseSpeed; + flowStunTolerance = Math.Max(flowStunTolerance, flowForceTolerance); + } + else + { + flowForceTolerance = Math.Max(flowForceTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f); + flowStunTolerance = Math.Max(flowStunTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f); + } + + if (flowForce.LengthSquared() > 0.001f) + { + Collider.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + foreach (Limb limb in limbs) + { + if (!limb.inWater) { continue; } + limb.body.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + } + } + } + private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false) { PhysicsBody refBody = refLimb == null ? Collider : refLimb.body; @@ -1490,7 +1563,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb.IsSevered) continue; + if (limb.IsSevered) { continue; } //check visibility from the new position of the collider to the new position of this limb Vector2 movePos = limb.SimPosition + limbMoveAmount; @@ -1554,7 +1627,7 @@ namespace Barotrauma Vector2 forceDir = diff / (float)Math.Sqrt(distSqrd); foreach (Limb limb in Limbs) { - if (limb.IsSevered) continue; + if (limb.IsSevered) { continue; } limb.body.CollidesWith = Physics.CollisionNone; limb.body.ApplyForce(forceDir * limb.Mass * 10.0f, maxVelocity: 10.0f); } @@ -1600,28 +1673,37 @@ namespace Barotrauma UpdateNetPlayerPositionProjSpecific(deltaTime, lowestSubPos); } - private Vector2 GetFlowForce() - { - Vector2 limbPos = Limbs[0].Position; - - Vector2 force = Vector2.Zero; - foreach (Gap gap in Gap.GapList) - { - if (gap.Open <= 0.0f || gap.FlowTargetHull != currentHull || gap.LerpedFlowForce.LengthSquared() < 0.01f) continue; - - Vector2 gapPos = gap.SimPosition; - float dist = Vector2.Distance(limbPos, gapPos); - force += Vector2.Normalize(gap.LerpedFlowForce) * (Math.Max(gap.LerpedFlowForce.Length() - dist, 0.0f) / 500.0f); - } - return force; - } - /// /// Note that if there are multiple limbs of the same type, only the first of them is found in the dictionary. /// - public Limb GetLimb(LimbType limbType) + public Limb GetLimb(LimbType limbType, bool excludeSevered = true) { - limbDictionary.TryGetValue(limbType, out Limb limb); + Limb limb = null; + if (HasMultipleLimbsOfSameType) + { + for (int i = 0; i < 10; i++) + { + limbDictionary.TryGetValue(limbType, out limb); + if (limb == null) + { + // No limbs found + break; + } + if (!excludeSevered || !limb.IsSevered) + { + // Found a valid limb + break; + } + } + } + else + { + limbDictionary.TryGetValue(limbType, out limb); + } + if (excludeSevered && limb != null && limb.IsSevered) + { + limb = null; + } return limb; } @@ -1664,10 +1746,15 @@ namespace Barotrauma Limb lowestLimb = null; foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } if (lowestLimb == null) + { lowestLimb = limb; + } else if (limb.SimPosition.Y < lowestLimb.SimPosition.Y) + { lowestLimb = limb; + } } return lowestLimb; @@ -1700,11 +1787,12 @@ namespace Barotrauma if (LimbJoints != null) { - foreach (RevoluteJoint joint in LimbJoints) + foreach (var joint in LimbJoints) { - if (GameMain.World.JointList.Contains(joint)) + var j = joint.Joint; + if (GameMain.World.JointList.Contains(j)) { - GameMain.World.Remove(joint); + GameMain.World.Remove(j); } } LimbJoints = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 6cc8bb8ff..4729ea027 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -316,11 +316,6 @@ namespace Barotrauma continue; } } - - //float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); - //var affliction = afflictionPrefab.Instantiate(afflictionStrength); - //Afflictions.Add(affliction, subElement); - break; case "conditional": foreach (XAttribute attribute in subElement.Attributes()) @@ -347,14 +342,18 @@ namespace Barotrauma afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab != null) { - float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); - affliction = afflictionPrefab.Instantiate(afflictionStrength); + affliction = afflictionPrefab.Instantiate(0.0f); } else { affliction = new Affliction(null, 0); } affliction.Deserialize(subElement); + //backwards compatibility + if (subElement.Attribute("amount") != null && subElement.Attribute("strength") == null) + { + affliction.Strength = subElement.GetAttributeFloat("amount", 0.0f); + } // add the affliction anyway, so that it can be shown in the editor. Afflictions.Add(affliction, subElement); } @@ -572,18 +571,14 @@ namespace Barotrauma public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType; - public bool IsValidTarget(Entity target) + public bool IsValidTarget(IDamageable target) { - switch (TargetType) + return TargetType switch { - case AttackTarget.Character: - return target is Character; - case AttackTarget.Structure: - return !(target is Character); - case AttackTarget.Any: - default: - return true; - } + AttackTarget.Character => target is Character, + AttackTarget.Structure => !(target is Character), + _ => true, + }; } public Vector2 CalculateAttackPhase(TransitionMode easing = TransitionMode.Linear) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 859bec436..ed81d9829 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -3,7 +3,7 @@ using FarseerPhysics; using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; -using System.IO; +using Barotrauma.IO; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -43,6 +43,7 @@ namespace Barotrauma foreach (Limb limb in AnimController.Limbs) { + if (limb.IsSevered) { continue; } if (limb.body != null) { limb.body.Enabled = enabled; @@ -235,6 +236,7 @@ namespace Barotrauma { get { + if (info != null && !string.IsNullOrWhiteSpace(info.Name)) { return info.Name; } var displayName = Params.DisplayName; if (string.IsNullOrWhiteSpace(displayName)) { @@ -587,30 +589,32 @@ namespace Barotrauma set { canInventoryBeAccessed = value; } } + private bool accessRemovedCharacterErrorShown; public override Vector2 SimPosition { get { if (AnimController?.Collider == null) { - string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + "."; - if (AnimController == null) + if (!accessRemovedCharacterErrorShown) { - errorMsg += " AnimController == null"; + string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + "."; + if (AnimController == null) + { + errorMsg += " AnimController == null"; + } + else if (AnimController.Collider == null) + { + errorMsg += " AnimController.Collider == null"; + } + errorMsg += '\n' + Environment.StackTrace; + DebugConsole.NewMessage(errorMsg, Color.Red); + GameAnalyticsManager.AddErrorEventOnce( + "Character.SimPosition:AccessRemoved", + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + errorMsg + "\n" + Environment.StackTrace); + accessRemovedCharacterErrorShown = true; } - else if (AnimController.Collider == null) - { - errorMsg += " AnimController.Collider == null"; - } -#if DEBUG || UNSTABLE - errorMsg += '\n' + Environment.StackTrace; -#endif - DebugConsole.NewMessage(errorMsg, Color.Red); - GameAnalyticsManager.AddErrorEventOnce( - "Character.SimPosition:AccessRemoved", - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - errorMsg + "\n" + Environment.StackTrace); - return Vector2.Zero; } @@ -1140,22 +1144,86 @@ namespace Barotrauma greatestNegativeSpeedMultiplier = 1f; } + /// + /// Speed reduction from the current limb specific damage. Min 0, max 1. + /// + public float GetTemporarySpeedReduction() + { + float reduction = 0; + reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightFoot, excludeSevered: false), reduction); + reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftFoot, excludeSevered: false), reduction); + if (AnimController is HumanoidAnimController) + { + if (AnimController.InWater) + { + // Currently only humans use hands for swimming. + reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), reduction); + reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), reduction); + } + } + else + { + int totalTailLimbs = 0; + int destroyedTailLimbs = 0; + foreach (var limb in AnimController.Limbs) + { + if (limb.type == LimbType.Tail) + { + totalTailLimbs++; + if (limb.IsSevered) + { + destroyedTailLimbs++; + } + } + } + if (destroyedTailLimbs > 0) + { + reduction += MathHelper.Lerp(0, AnimController.InWater ? 1f : 0.5f, (float)destroyedTailLimbs / totalTailLimbs); + } + } + return Math.Clamp(reduction, 0, 1f); + } + + private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.4f) + { + if (limb != null) + { + sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: "damage")); + } + return Math.Clamp(sum, 0, 1f); + } + + public float GetRightHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), 0, max: 1); + public float GetLeftHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), 0, max: 1); + + public float GetLegPenalty(float startSum = 0) + { + float sum = startSum; + foreach (var limb in AnimController.Limbs) + { + switch (limb.type) + { + case LimbType.RightFoot: + case LimbType.LeftFoot: + sum += CalculateMovementPenalty(limb, sum, max: 0.5f); + break; + } + } + return Math.Clamp(sum, 0, 1f); + } + public float ApplyTemporarySpeedLimits(float speed) { - var leftFoot = AnimController.GetLimb(LimbType.LeftFoot); - if (leftFoot != null) + float max; + if (AnimController is HumanoidAnimController) { - float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", leftFoot, true); - speed *= MathHelper.Lerp(1.0f, 0.4f, MathHelper.Clamp(footAfflictionStrength / 80.0f, 0.0f, 1.0f)); + max = AnimController.InWater ? 0.5f : 0.7f; } - - var rightFoot = AnimController.GetLimb(LimbType.RightFoot); - if (rightFoot != null) + else { - float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", rightFoot, true); - speed *= MathHelper.Lerp(1.0f, 0.4f, MathHelper.Clamp(footAfflictionStrength / 80.0f, 0.0f, 1.0f)); + max = AnimController.InWater ? 0.9f : 0.5f; } - + speed *= 1f - MathHelper.Lerp(0, max, GetTemporarySpeedReduction()); return speed; } @@ -1256,58 +1324,72 @@ namespace Barotrauma } else if (IsKeyDown(InputType.Attack) && (IsRemotePlayer || Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient))) { + Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position); + List ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); + ignoredBodies.Add(AnimController.Collider.FarseerBody); + + var body = Submarine.PickBody( + SimPosition, + attackPos, + ignoredBodies, + Physics.CollisionCharacter | Physics.CollisionWall); + + IDamageable attackTarget = null; + if (body != null) + { + attackPos = Submarine.LastPickedPosition; + + if (body.UserData is Submarine sub) + { + body = Submarine.PickBody( + SimPosition - ((Submarine)body.UserData).SimPosition, + attackPos - ((Submarine)body.UserData).SimPosition, + ignoredBodies, + Physics.CollisionWall); + + if (body != null) + { + attackPos = Submarine.LastPickedPosition + sub.SimPosition; + attackTarget = body.UserData as IDamageable; + } + } + else + { + if (body.UserData is IDamageable) + { + attackTarget = (IDamageable)body.UserData; + } + else if (body.UserData is Limb) + { + attackTarget = ((Limb)body.UserData).character; + } + } + } var currentContexts = GetAttackContexts(); - var validLimbs = AnimController.Limbs.Where(l => !l.IsSevered && !l.IsStuck && l.attack != null && l.attack.IsValidContext(currentContexts)); + var validLimbs = AnimController.Limbs.Where(l => + { + if (l.IsSevered || l.IsStuck) { return false; } + var attack = l.attack; + if (attack == null) { return false; } + if (attack.CoolDownTimer > 0) { return false; } + if (!attack.IsValidContext(currentContexts)) { return false; } + if (attackTarget != null) + { + if (!attack.IsValidTarget(attackTarget)) { return false; } + if (attackTarget is ISerializableEntity se && attackTarget is Character) + { + if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; } + } + } + if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; } + return true; + }); var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition)); // Select closest var attackLimb = sortedLimbs.FirstOrDefault(); if (attackLimb != null) { - Vector2 attackPos = attackLimb.SimPosition + Vector2.Normalize(cursorPosition - attackLimb.Position) * ConvertUnits.ToSimUnits(attackLimb.attack.Range); - - List ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); - ignoredBodies.Add(AnimController.Collider.FarseerBody); - - var body = Submarine.PickBody( - attackLimb.SimPosition, - attackPos, - ignoredBodies, - Physics.CollisionCharacter | Physics.CollisionWall); - - IDamageable attackTarget = null; - if (body != null) - { - attackPos = Submarine.LastPickedPosition; - - if (body.UserData is Submarine sub) - { - body = Submarine.PickBody( - attackLimb.SimPosition - ((Submarine)body.UserData).SimPosition, - attackPos - ((Submarine)body.UserData).SimPosition, - ignoredBodies, - Physics.CollisionWall); - - if (body != null) - { - attackPos = Submarine.LastPickedPosition + sub.SimPosition; - attackTarget = body.UserData as IDamageable; - } - } - else - { - if (body.UserData is IDamageable) - { - attackTarget = (IDamageable)body.UserData; - } - else if (body.UserData is Limb) - { - attackTarget = ((Limb)body.UserData).character; - } - } - } - attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult); - if (!attackLimb.attack.IsRunning) { attackCoolDown = 1.0f; @@ -1394,22 +1476,23 @@ namespace Barotrauma { Limb selfLimb = AnimController.GetLimb(LimbType.Head); if (selfLimb == null) { selfLimb = AnimController.GetLimb(LimbType.Torso); } - if (selfLimb == null) { selfLimb = AnimController.Limbs.FirstOrDefault(); } + if (selfLimb == null) { selfLimb = AnimController.MainLimb; } return selfLimb; } public bool CanSeeTarget(ISpatialEntity target, Limb seeingLimb = null) { - seeingLimb = seeingLimb ?? GetSeeingLimb(); + seeingLimb ??= GetSeeingLimb(); if (seeingLimb == null) { return false; } + ISpatialEntity seeingEntity = AnimController.SimplePhysicsEnabled ? this : seeingLimb as ISpatialEntity; // TODO: Could we just use the method below? If not, let's refactor it so that we can. - Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingLimb.WorldPosition); + Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition); Body closestBody; //both inside the same sub (or both outside) //OR the we're inside, the other character outside if (target.Submarine == Submarine || target.Submarine == null) { - closestBody = Submarine.CheckVisibility(seeingLimb.SimPosition, seeingLimb.SimPosition + diff); + closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); } //we're outside, the other character inside else if (Submarine == null) @@ -1419,7 +1502,7 @@ namespace Barotrauma //both inside different subs else { - closestBody = Submarine.CheckVisibility(seeingLimb.SimPosition, seeingLimb.SimPosition + diff); + closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); if (!IsBlocking(closestBody)) { closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff); @@ -1436,10 +1519,11 @@ namespace Barotrauma } else if (body.UserData is Item item && item != target) { + // TODO: The door collider should be disabled, so this check is probably unnecessary. var door = item.GetComponent(); if (door != null) { - return !door.IsOpen; + return !door.IsOpen && !door.IsBroken; } } return false; @@ -1466,7 +1550,7 @@ namespace Barotrauma Structure wall = closestBody.UserData as Structure; Item item = closestBody.UserData as Item; Door door = item?.GetComponent(); - return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen); + return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen || door.IsBroken); } public bool HasItem(Item item, bool requireEquipped = false) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this); @@ -1601,8 +1685,8 @@ namespace Barotrauma if (IsItemTakenBySomeoneElse(item)) { continue; } float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1; if (itemPriority <= 0) { continue; } - Item rootContainer = item.GetRootContainer(); - Vector2 itemPos = (rootContainer ?? item).WorldPosition; + Entity rootInventoryOwner = item.GetRootInventoryOwner(); + Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; float yDist = Math.Abs(WorldPosition.Y - itemPos.Y); yDist = yDist > 100 ? yDist * 5 : 0; float dist = Math.Abs(WorldPosition.X - itemPos.X) + yDist; @@ -1620,13 +1704,16 @@ namespace Barotrauma public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null; - public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true) + public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false) { - if (c == this || Removed || !c.Enabled || !c.CanBeSelected) return false; - if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && c.onCustomInteract == null) return false; + if (c == this || Removed || !c.Enabled || !c.CanBeSelected) { return false; } + if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && c.onCustomInteract == null) { return false; } - maxDist = ConvertUnits.ToSimUnits(maxDist); - if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) return false; + if (!skipDistanceCheck) + { + maxDist = ConvertUnits.ToSimUnits(maxDist); + if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; } + } return checkVisibility ? CanSeeCharacter(c) : true; } @@ -2394,7 +2481,7 @@ namespace Barotrauma } } - private readonly float maxAIRange = 10000; + private readonly float maxAIRange = 20000; private readonly float aiTargetChangeSpeed = 5; private void UpdateSightRange(float deltaTime) @@ -2628,40 +2715,60 @@ namespace Barotrauma GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack); } #endif - - bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; - - TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability); + // Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head) + TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: AIController == null || AIController is HumanAIController); return attackResult; } - public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability) + public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading) { - bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; - - if (isNotClient && - IsDead && Rand.Range(0.0f, 1.0f) < severLimbsProbability) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } +#if DEBUG + if (targetLimb.character != this) { - foreach (LimbJoint joint in AnimController.LimbJoints) - { - if (joint.CanBeSevered && (joint.LimbA == targetLimb || joint.LimbB == targetLimb)) - { -#if CLIENT - CurrentHull?.AddDecal("blood", WorldPosition, Rand.Range(0.5f, 1.5f)); + DebugConsole.ThrowError($"{Name} is attempting to sever joints of {targetLimb.character.Name}!"); + return; + } #endif - AnimController.SeverLimbJoint(joint); - - if (joint.LimbA == targetLimb) - { - joint.LimbB.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f; - } - else - { - joint.LimbA.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f; - } - } + if (damage < targetLimb.Params.MinSeveranceDamage) { return; } + if (!IsDead) + { + if (!allowBeheading && targetLimb.type == LimbType.Head) { return; } + if (!targetLimb.CanBeSeveredAlive) { return; } + } + bool wasSevered = false; + float random = Rand.Value(); + foreach (LimbJoint joint in AnimController.LimbJoints) + { + if (!joint.CanBeSevered) { continue; } + if (joint.LimbA != targetLimb && joint.LimbB != targetLimb) { continue; } + float probability = severLimbsProbability; + if (!IsDead) + { + probability *= joint.Params.SeveranceProbabilityModifier; } + if (probability <= 0) { continue; } + if (random > probability) { continue; } + bool severed = AnimController.SeverLimbJoint(joint); + if (!wasSevered) + { + wasSevered = severed; + } + if (severed) + { + Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA; + otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass); + } + } + if (wasSevered) + { + if (targetLimb.character.AIController is EnemyAIController enemyAI) + { + enemyAI.ReevaluateAttacks(); + } + ApplyStatusEffects(ActionType.OnSevered, 1.0f); + targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); } } @@ -2742,6 +2849,10 @@ namespace Barotrauma AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound); CharacterHealth.ApplyDamage(hitLimb, attackResult); ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + if (attackResult.Damage > 0) + { + hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + } if (attacker != this) { OnAttacked?.Invoke(attacker, attackResult); @@ -2800,8 +2911,10 @@ namespace Barotrauma { SelectedConstruction = null; } + HealthUpdateInterval = 0.0f; } + private readonly List targets = new List(); public void ApplyStatusEffects(ActionType actionType, float deltaTime) { foreach (StatusEffect statusEffect in statusEffects) @@ -2810,30 +2923,61 @@ namespace Barotrauma if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - var targets = new List(); + targets.Clear(); statusEffect.GetNearbyTargets(WorldPosition, targets); statusEffect.Apply(ActionType.OnActive, deltaTime, this, targets); } else { statusEffect.Apply(actionType, deltaTime, this, this); + if (statusEffect.targetLimbs != null) + { + foreach (var limbType in statusEffect.targetLimbs) + { + if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + { + // Target all matching limbs + foreach (var limb in AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.type == limbType) + { + statusEffect.Apply(actionType, deltaTime, this, limb); + } + } + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + { + // Target just the first matching limb + Limb limb = AnimController.GetLimb(limbType); + statusEffect.Apply(actionType, deltaTime, this, limb); + } + } + } } } + if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered) + { + // OnDamaged is called only for the limb that is hit. + AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime)); + } } private void Implode(bool isNetworkMessage = false) { - if (CharacterHealth.Unkillable) { return; } + if (CharacterHealth.Unkillable || IsDead) { return; } if (!isNetworkMessage) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return; + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } } - Kill(CauseOfDeathType.Pressure, null, isNetworkMessage); - CharacterHealth.PressureAffliction.Strength = CharacterHealth.PressureAffliction.Prefab.MaxStrength; - CharacterHealth.SetAllDamage(200.0f, 0.0f, 0.0f); - BreakJoints(); + CharacterHealth.ApplyAffliction(null, new Affliction(AfflictionPrefab.Pressure, AfflictionPrefab.Pressure.MaxStrength)); + if (isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Vitality <= CharacterHealth.MinVitality) { Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true); } + if (IsDead) + { + BreakJoints(); + } } public void BreakJoints() @@ -2841,6 +2985,7 @@ namespace Barotrauma Vector2 centerOfMass = AnimController.GetCenterOfMass(); foreach (Limb limb in AnimController.Limbs) { + if (limb.IsSevered) { continue; } limb.AddDamage(limb.SimPosition, 500.0f, 0.0f, 0.0f, false); Vector2 diff = centerOfMass - limb.SimPosition; @@ -2861,7 +3006,10 @@ namespace Barotrauma foreach (var joint in AnimController.LimbJoints) { - joint.LimitEnabled = false; + if (joint.revoluteJoint != null) + { + joint.revoluteJoint.LimitEnabled = false; + } } } @@ -2928,9 +3076,12 @@ namespace Barotrauma AnimController.ResetPullJoints(); - foreach (RevoluteJoint joint in AnimController.LimbJoints) + foreach (var joint in AnimController.LimbJoints) { - joint.MotorEnabled = false; + if (joint.revoluteJoint != null) + { + joint.revoluteJoint.MotorEnabled = false; + } } if (GameMain.GameSession != null) @@ -2961,7 +3112,11 @@ namespace Barotrauma foreach (LimbJoint joint in AnimController.LimbJoints) { - joint.MotorEnabled = true; + var revoluteJoint = joint.revoluteJoint; + if (revoluteJoint != null) + { + revoluteJoint.MotorEnabled = true; + } joint.Enabled = true; joint.IsSevered = false; } @@ -3176,5 +3331,15 @@ namespace Barotrauma } return targetPos; } + + public bool IsCaptain => HasJob("captain"); + public bool IsEngineer => HasJob("engineer"); + public bool IsMechanic => HasJob("mechanic"); + public bool IsMedic => HasJob("medicaldoctor"); + public bool IsOfficer => HasJob("securityofficer"); + public bool IsAsssitant => HasJob("assistant"); + public bool IsWatchman => HasJob("watchman"); + + public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index f2e23373f..1876804b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -4,7 +4,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index 93728e6a7..afbee5aaa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index ed9c2360d..80cdaa08d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -97,11 +97,13 @@ namespace Barotrauma private void ApplyDamage(float deltaTime, bool applyForce) { + int limbCount = character.AnimController.Limbs.Count(l => !l.ignoreCollisions && !l.IsSevered); foreach (Limb limb in character.AnimController.Limbs) { + if (limb.IsSevered) { continue; } float random = Rand.Value(Rand.RandSync.Server); huskInfection.Clear(); - huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / character.AnimController.Limbs.Length)); + huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount)); character.LastDamageSource = null; float force = applyForce ? random * 0.5f * limb.Mass : 0; character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force); @@ -186,18 +188,20 @@ namespace Barotrauma } } - if (character.Inventory.Items.Length != husk.Inventory.Items.Length) + if (character.Inventory != null && husk.Inventory != null) { - string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - yield return CoroutineStatus.Success; - } - - for (int i = 0; i < character.Inventory.Items.Length && i < husk.Inventory.Items.Length; i++) - { - if (character.Inventory.Items[i] == null) continue; - husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null); + if (character.Inventory.Items.Length != husk.Inventory.Items.Length) + { + string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + yield return CoroutineStatus.Success; + } + for (int i = 0; i < character.Inventory.Items.Length && i < husk.Inventory.Items.Length; i++) + { + if (character.Inventory.Items[i] == null) continue; + husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null); + } } husk.SetStun(5); @@ -255,20 +259,19 @@ namespace Barotrauma Limb attachLimb = null; if (matchingAffliction.AttachLimbId > -1) { - attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == matchingAffliction.AttachLimbId); + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId); } else if (matchingAffliction.AttachLimbName != null) { - attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Name == matchingAffliction.AttachLimbName); + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName); } else if (matchingAffliction.AttachLimbType != LimbType.None) { - attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.type == matchingAffliction.AttachLimbType); + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType); } if (attachLimb == null) { - DebugConsole.Log("Attachment limb not defined in the affliction prefab or no matching limb could be found. Using the appendage definition as it is."); - attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == jointParams.Limb1); + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1); } if (attachLimb != null) { @@ -286,10 +289,6 @@ namespace Barotrauma ragdoll.AddJoint(jointParams); appendage.Add(huskAppendage); } - else - { - DebugConsole.ThrowError("Attachment limb not found!"); - } } } return appendage; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 7c90aa161..39cae1adb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -189,6 +189,7 @@ namespace Barotrauma } public static AfflictionPrefab InternalDamage; + public static AfflictionPrefab ImpactDamage; public static AfflictionPrefab Bleeding; public static AfflictionPrefab Burn; public static AfflictionPrefab OxygenLow; @@ -291,6 +292,7 @@ namespace Barotrauma { CPRSettings.Unload(); InternalDamage = null; + ImpactDamage = null; Bleeding = null; Burn = null; OxygenLow = null; @@ -437,6 +439,9 @@ namespace Barotrauma case "internaldamage": InternalDamage = prefab; break; + case "blunttrauma": + ImpactDamage = prefab; + break; case "bleeding": Bleeding = prefab; break; @@ -456,6 +461,8 @@ namespace Barotrauma Stun = prefab; break; } + if (ImpactDamage == null) { ImpactDamage = InternalDamage; } + if (prefab != null) { Prefabs.Add(prefab, isOverride); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs index 7b3b9066f..a65e14cc8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs @@ -1,6 +1,6 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using Barotrauma.Extensions; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs index 0b6fa2970..3b36e03a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using System; namespace Barotrauma { @@ -19,7 +20,7 @@ namespace Barotrauma { foreach (Affliction affliction in afflictions) { - if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) continue; + if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) { continue; } affliction.MultiplierSource = null; affliction.StrengthDiminishMultiplier = 1f; } @@ -28,9 +29,9 @@ namespace Barotrauma { foreach (Affliction affliction in afflictions) { - if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource == this) continue; + if (!affliction.Prefab.IsBuff || affliction == this) { continue; } float multiplier = GetDiminishMultiplier(); - if (affliction.StrengthDiminishMultiplier < multiplier) continue; + if (affliction.StrengthDiminishMultiplier < multiplier && affliction.MultiplierSource != this) { continue; } affliction.MultiplierSource = this; affliction.StrengthDiminishMultiplier = multiplier; @@ -40,14 +41,15 @@ namespace Barotrauma private float GetDiminishMultiplier() { - if (Strength < Prefab.ActivationThreshold) return 1.0f; + if (Strength < Prefab.ActivationThreshold) { return 1.0f; } AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); - if (currentEffect == null) return 1.0f; + if (currentEffect == null) { return 1.0f; } - return MathHelper.Lerp( + float multiplier = MathHelper.Lerp( currentEffect.MinBuffMultiplier, currentEffect.MaxBuffMultiplier, (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + return 1.0f / Math.Max(multiplier, 0.001f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 55bd1db75..af5f932e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -127,7 +127,7 @@ namespace Barotrauma public bool IsUnconscious { - get { return Vitality <= 0.0f; } + get { return Vitality <= 0.0f || Character.IsDead; } } public float PressureKillDelay { get; private set; } = 5.0f; @@ -252,8 +252,8 @@ namespace Barotrauma : afflictions.Where(limbHealthFilter).Union(limbHealths.SelectMany(lh => lh.Afflictions.Where(limbHealthFilter))); } - private LimbHealth GetMatchingLimbHealth(Limb limb) => limbHealths[limb.HealthIndex]; - private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb)); + private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex]; + private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false)); /// /// Returns the limb afflictions and non-limbspecific afflictions that are set to be displayed on this limb. @@ -349,13 +349,16 @@ namespace Barotrauma /// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb. public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific) { - if (requireLimbSpecific && limbHealths.Count == 1) return 0.0f; + if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; } float strength = 0.0f; foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions) { - if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue; - if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength; + if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } + if (affliction.Prefab.AfflictionType == afflictionType) + { + strength += affliction.Strength; + } } return strength; } @@ -365,17 +368,23 @@ namespace Barotrauma float strength = 0.0f; foreach (Affliction affliction in afflictions) { - if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue; - if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength; + if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } + if (affliction.Prefab.AfflictionType == afflictionType) + { + strength += affliction.Strength; + } } - if (!allowLimbAfflictions) return strength; + if (!allowLimbAfflictions) { return strength; } foreach (LimbHealth limbHealth in limbHealths) { foreach (Affliction affliction in limbHealth.Afflictions) { - if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue; - if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength; + if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } + if (affliction.Prefab.AfflictionType == afflictionType) + { + strength += affliction.Strength; + } } } @@ -506,6 +515,34 @@ namespace Barotrauma if (Vitality <= MinVitality) { Kill(); } } + public float GetLimbDamage(Limb limb, string afflictionType = null) + { + float damageStrength; + if (limb.IsSevered) + { + return 1; + } + else + { + // Instead of using the limbhealth count here, I think it's best to define the max vitality per limb roughly with a constant value. + // Therefore with e.g. 80 health, the max damage per limb would be 20. + // Having at least 20 damage on both legs would cause maximum limping. + float max = MaxVitality / 4; + if (string.IsNullOrEmpty(afflictionType)) + { + float damage = GetAfflictionStrength("damage", limb, true); + float bleeding = GetAfflictionStrength("bleeding", limb, true); + float burn = GetAfflictionStrength("burn", limb, true); + damageStrength = Math.Min(damage + bleeding + burn, max); + } + else + { + damageStrength = Math.Min(GetAfflictionStrength("damage", limb, true), max); + } + return damageStrength / max; + } + } + public void RemoveAllAfflictions() { foreach (LimbHealth limbHealth in limbHealths) @@ -523,7 +560,7 @@ namespace Barotrauma private void AddLimbAffliction(Limb limb, Affliction newAffliction) { - if (!newAffliction.Prefab.LimbSpecific || limb == null) return; + if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; } if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name + @@ -535,8 +572,8 @@ namespace Barotrauma private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction) { - if (!DoesBleed && newAffliction is AfflictionBleeding) return; - if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; + if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } + if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } foreach (Affliction affliction in limbHealth.Afflictions) { @@ -545,7 +582,10 @@ namespace Barotrauma affliction.Strength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier)))); affliction.Source = newAffliction.Source; CalculateVitality(); - if (Vitality <= MinVitality) Kill(); + if (Vitality <= MinVitality) + { + Kill(); + } return; } } @@ -560,7 +600,10 @@ namespace Barotrauma Character.HealthUpdateInterval = 0.0f; CalculateVitality(); - if (Vitality <= MinVitality) Kill(); + if (Vitality <= MinVitality) + { + Kill(); + } #if CLIENT selectedLimbIndex = -1; #endif @@ -894,10 +937,7 @@ namespace Barotrauma partial void RemoveProjSpecific(); - /// - /// Automatically filters out buffs. - /// - public static IEnumerable SortAfflictionsBySeverity(IEnumerable afflictions) => - afflictions.Where(a => !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength); + public static IEnumerable SortAfflictionsBySeverity(IEnumerable afflictions, bool excludeBuffs = true) => + afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 3b738e1b9..836d86c16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -64,7 +64,7 @@ namespace Barotrauma public readonly Dictionary> ItemIdentifiers = new Dictionary>(); public readonly Dictionary> ShowItemPreview = new Dictionary>(); public readonly List Skills = new List(); - public readonly List AutomaticOrders = new List(); + public readonly List AutonomousObjective = new List(); public readonly List AppropriateOrders = new List(); [Serialize("1,1,1,1", false)] @@ -163,6 +163,7 @@ namespace Barotrauma } public Sprite Icon; + public Sprite IconSmall; public string FilePath { get; private set; } public XElement Element { get; private set; } @@ -198,7 +199,7 @@ namespace Barotrauma } break; case "autonomousobjectives": - subElement.Elements().ForEach(order => AutomaticOrders.Add(new AutonomousObjective(order))); + subElement.Elements().ForEach(order => AutonomousObjective.Add(new AutonomousObjective(order))); break; case "appropriateobjectives": case "appropriateorders": @@ -207,6 +208,9 @@ namespace Barotrauma case "jobicon": Icon = new Sprite(subElement.FirstElement()); break; + case "jobiconsmall": + IconSmall = new Sprite(subElement.FirstElement()); + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index cfd1a8b81..e3ebd0ce4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -19,8 +19,8 @@ namespace Barotrauma None, LeftHand, RightHand, LeftArm, RightArm, LeftForearm, RightForearm, LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist, Jaw }; - - partial class LimbJoint : RevoluteJoint + + partial class LimbJoint { public bool IsSevered; public bool CanBeSevered => Params.CanBeSevered; @@ -30,27 +30,135 @@ namespace Barotrauma public float Scale => Params.Scale * ragdoll.RagdollParams.JointScale; - public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One) + public readonly RevoluteJoint revoluteJoint; + public readonly WeldJoint weldJoint; + public Joint Joint => revoluteJoint ?? weldJoint as Joint; + + public bool Enabled + { + get => Joint.Enabled; + set => Joint.Enabled = value; + } + + public Body BodyA => Joint.BodyA; + + public Body BodyB => Joint.BodyB; + + public Vector2 WorldAnchorA + { + get => Joint.WorldAnchorA; + set => Joint.WorldAnchorA = value; + } + + public Vector2 WorldAnchorB + { + get => Joint.WorldAnchorB; + set => Joint.WorldAnchorB = value; + } + + public Vector2 LocalAnchorA + { + get => revoluteJoint != null ? revoluteJoint.LocalAnchorA : weldJoint.LocalAnchorA; + set + { + if (weldJoint != null) + { + weldJoint.LocalAnchorA = value; + } + else + { + revoluteJoint.LocalAnchorA = value; + } + } + } + + public Vector2 LocalAnchorB + { + get => revoluteJoint != null ? revoluteJoint.LocalAnchorB : weldJoint.LocalAnchorB; + set + { + if (weldJoint != null) + { + weldJoint.LocalAnchorB = value; + } + else + { + revoluteJoint.LocalAnchorB = value; + } + } + } + + public bool LimitEnabled + { + get => revoluteJoint != null ? revoluteJoint.LimitEnabled : false; + set + { + if (revoluteJoint != null) + { + revoluteJoint.LimitEnabled = value; + } + } + } + + public float LowerLimit + { + get => revoluteJoint != null ? revoluteJoint.LowerLimit : 0; + set + { + if (revoluteJoint != null) + { + revoluteJoint.LowerLimit = value; + } + } + } + + public float UpperLimit + { + get => revoluteJoint != null ? revoluteJoint.UpperLimit : 0; + set + { + if (revoluteJoint != null) + { + revoluteJoint.UpperLimit = value; + } + } + } + + public float JointAngle => revoluteJoint != null ? revoluteJoint.JointAngle : weldJoint.ReferenceAngle; + + public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One, jointParams.WeldJoint) { Params = jointParams; this.ragdoll = ragdoll; LoadParams(); } - public LimbJoint(Limb limbA, Limb limbB, Vector2 anchor1, Vector2 anchor2) - : base(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2) + public LimbJoint(Limb limbA, Limb limbB, Vector2 anchor1, Vector2 anchor2, bool weld = false) { - CollideConnected = false; - MotorEnabled = true; - MaxMotorTorque = 0.25f; + if (weld) + { + weldJoint = new WeldJoint(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2); + } + else + { + revoluteJoint = new RevoluteJoint(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2) + { + MotorEnabled = true, + MaxMotorTorque = 0.25f + }; + } + Joint.CollideConnected = false; LimbA = limbA; LimbB = limbB; } public void LoadParams() { - MaxMotorTorque = Params.Stiffness; - LimitEnabled = Params.LimitEnabled; + if (revoluteJoint != null) + { + revoluteJoint.MaxMotorTorque = Params.Stiffness; + revoluteJoint.LimitEnabled = Params.LimitEnabled; + } if (float.IsNaN(Params.LowerLimit)) { Params.LowerLimit = 0; @@ -61,17 +169,33 @@ namespace Barotrauma } if (ragdoll.IsFlipped) { - LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale); - LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale); - UpperLimit = MathHelper.ToRadians(-Params.LowerLimit); - LowerLimit = MathHelper.ToRadians(-Params.UpperLimit); + if (weldJoint != null) + { + weldJoint.LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale); + weldJoint.LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale); + } + else + { + revoluteJoint.LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale); + revoluteJoint.LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale); + revoluteJoint.UpperLimit = MathHelper.ToRadians(-Params.LowerLimit); + revoluteJoint.LowerLimit = MathHelper.ToRadians(-Params.UpperLimit); + } } else { - LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale); - LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale); - UpperLimit = MathHelper.ToRadians(Params.UpperLimit); - LowerLimit = MathHelper.ToRadians(Params.LowerLimit); + if (weldJoint != null) + { + weldJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale); + weldJoint.LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale); + } + else + { + revoluteJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale); + revoluteJoint.LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale); + revoluteJoint.UpperLimit = MathHelper.ToRadians(Params.UpperLimit); + revoluteJoint.LowerLimit = MathHelper.ToRadians(Params.LowerLimit); + } } } } @@ -166,10 +290,20 @@ namespace Barotrauma if (isSevered) { ragdoll.SubtractMass(this); + if (type == LimbType.Head) + { + character.Kill(CauseOfDeathType.Unknown, null); + } + } + else + { + severedFadeOutTimer = 0.0f; } - if (!isSevered) severedFadeOutTimer = 0.0f; #if CLIENT - if (isSevered) damageOverlayStrength = 100.0f; + if (isSevered) + { + damageOverlayStrength = 100.0f; + } #endif } } @@ -366,14 +500,42 @@ namespace Barotrauma public string Name => Params.Name; + // Exposed for status effects public bool IsDead => character.IsDead; + public bool CanBeSeveredAlive + { + get + { + if (character.IsHumanoid) { return false; } + if (this == character.AnimController.MainLimb) { return false; } + if (character.AnimController.CanWalk) + { + switch (type) + { + case LimbType.LeftFoot: + case LimbType.RightFoot: + case LimbType.LeftLeg: + case LimbType.RightLeg: + case LimbType.LeftThigh: + case LimbType.RightThigh: + case LimbType.Legs: + case LimbType.Waist: + return false; + } + } + return true; + } + } + public Dictionary SerializableProperties { get; private set; } + private readonly List statusEffects = new List(); + public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams) { this.ragdoll = ragdoll; @@ -436,6 +598,9 @@ namespace Barotrauma case "damagemodifier": DamageModifiers.Add(new DamageModifier(subElement, character.Name)); break; + case "statuseffect": + statusEffects.Add(StatusEffect.Load(subElement, Name)); + break; } } @@ -521,11 +686,12 @@ namespace Barotrauma afflictionsCopy.Add(newAffliction); } } - AddDamageProjSpecific(afflictionsCopy, playSound, appliedDamageModifiers); - return new AttackResult(afflictionsCopy, this, appliedDamageModifiers); + var result = new AttackResult(afflictionsCopy, this, appliedDamageModifiers); + AddDamageProjSpecific(playSound, result); + return result; } - partial void AddDamageProjSpecific(IEnumerable afflictions, bool playSound, IEnumerable appliedDamageModifiers); + partial void AddDamageProjSpecific(bool playSound, AttackResult result); public bool SectorHit(Vector2 armorSector, Vector2 simPosition) { @@ -582,7 +748,8 @@ namespace Barotrauma public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null) { attackResult = default(AttackResult); - float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackSimPos)); + Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition; + float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos)); bool wasRunning = attack.IsRunning; attack.UpdateAttackTimer(deltaTime); @@ -595,7 +762,7 @@ namespace Barotrauma case HitDetection.Distance: if (dist < attack.DamageRange) { - structureBody = Submarine.PickBody(SimPosition, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true); + structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true); if (damageTarget is Item i && i.GetComponent() != null) { // If the attack is aimed to an item and hits an item, it's successful. @@ -689,6 +856,7 @@ namespace Barotrauma { if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; } Limb limb = character.AnimController.Limbs[limbIndex]; + if (limb.IsSevered) { continue; } diff = attackSimPos - limb.SimPosition; if (diff == Vector2.Zero) { continue; } limb.body.ApplyTorque(limb.Mass * character.AnimController.Dir * attack.Torque * limb.Params.AttackForceMultiplier); @@ -811,6 +979,30 @@ namespace Barotrauma } } + private readonly List targets = new List(); + public void ApplyStatusEffects(ActionType actionType, float deltaTime) + { + foreach (StatusEffect statusEffect in statusEffects) + { + if (statusEffect.type != actionType) { continue; } + if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || + statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) + { + targets.Clear(); + statusEffect.GetNearbyTargets(WorldPosition, targets); + statusEffect.Apply(ActionType.OnActive, deltaTime, character, targets); + } + else + { + if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + { + statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition); + } + statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition); + } + } + } + public void Remove() { body?.Remove(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 9f97ec0be..52d3ba651 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index 0d9f8a24e..275336876 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -72,6 +72,12 @@ namespace Barotrauma [Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")] public bool Flip { get; set; } + [Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] + public float FlipCooldown { get; set; } + + [Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] + public float FlipDelay { get; set; } + [Serialize(10.0f, true, description: "How much force is used to move the head to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float HeadMoveForce { get; set; } @@ -146,9 +152,18 @@ namespace Barotrauma [Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")] public bool Flip { get; set; } + [Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] + public float FlipCooldown { get; set; } + + [Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] + public float FlipDelay { get; set; } + [Editable, Serialize(true, true, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")] public bool Mirror { get; set; } + [Editable, Serialize(true, true, description: "Disabling this will make mirroring instantaneous.")] + public bool MirrorLerp { get; set; } + [Serialize(5f, true), Editable] public float WaveAmplitude { get; set; } @@ -205,7 +220,6 @@ namespace Barotrauma interface IFishAnimation { - bool Flip { get; set; } string FootAngles { get; set; } Dictionary FootAnglesInRadians { get; set; } float TailAngle { get; set; } @@ -214,5 +228,8 @@ namespace Barotrauma float TorsoTorque { get; set; } float TailTorque { get; set; } float FootTorque { get; set; } + bool Flip { get; set; } + float FlipCooldown { get; set; } + float FlipDelay { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 9127cd833..c664da6b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -25,10 +25,10 @@ namespace Barotrauma [Serialize("", true, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable] public string Group { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, true), Editable(ReadOnly = true)] public bool Humanoid { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, true), Editable(ReadOnly = true)] public bool HasInfo { get; private set; } [Serialize(false, true), Editable] @@ -43,13 +43,13 @@ namespace Barotrauma [Serialize(false, true, description: "Can the creature live without water or does it die on dry land?"), Editable] public bool NeedsWater { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, false), Editable] public bool CanSpeak { get; set; } - [Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 1000f)] + [Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)] public float Noise { get; set; } - [Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 1000f)] + [Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 100000f)] public float Visibility { get; set; } [Serialize("blood", true), Editable] @@ -70,6 +70,9 @@ namespace Barotrauma [Serialize(false, true), Editable] public bool HideInSonar { get; set; } + [Serialize(0f, true), Editable] + public float SonarDisruption { get; set; } + public readonly string File; public readonly List SubParams = new List(); @@ -474,8 +477,8 @@ namespace Barotrauma [Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()] public bool EnforceAggressiveBehaviorForMissions { get; private set; } - [Serialize(false, true, description: "Should the character target or ignore walls when it's inside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()] - public bool TargetInnerWalls { get; private set; } + [Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()] + public bool TargetOuterWalls { get; private set; } [Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable()] public bool RandomAttack { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs index 66bf5f3c3..1a7fa97f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs @@ -1,8 +1,12 @@ -using System.IO; -using System.Collections.Generic; -using System.Xml; +using System.Collections.Generic; using System.Xml.Linq; using Microsoft.Xna.Framework; +#if DEBUG +using System.IO; +using System.Xml; +#else +using Barotrauma.IO; +#endif namespace Barotrauma { @@ -75,7 +79,7 @@ namespace Barotrauma Folder = Path.GetDirectoryName(FullPath); } - public virtual bool Save(string fileNameWithoutExtension = null, XmlWriterSettings settings = null) + public virtual bool Save(string fileNameWithoutExtension = null, System.Xml.XmlWriterSettings settings = null) { if (!Directory.Exists(Folder)) { @@ -85,7 +89,7 @@ namespace Barotrauma Serialize(); if (settings == null) { - settings = new XmlWriterSettings + settings = new System.Xml.XmlWriterSettings { Indent = true, OmitXmlDeclaration = true, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index ad3f4ba54..ea95217df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; using System.Linq; -using System.IO; +using Barotrauma.IO; using System.Xml; using Barotrauma.Extensions; #if CLIENT @@ -470,6 +470,12 @@ namespace Barotrauma [Serialize(true, true), Editable] public bool CanBeSevered { get; set; } + [Serialize(0f, true, description:"Default 0 (Can't be severed when the creature is alive). Modifies the severance probability (defined per item/attack) when the character is alive. Currently only affects non-humanoid ragdolls. Also note that if CanBeSevered is false, this property doesn't have any effect."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] + public float SeveranceProbabilityModifier { get; set; } + + [Serialize("gore", true), Editable] + public string BreakSound { get; set; } + [Serialize(true, true), Editable] public bool LimitEnabled { get; set; } @@ -491,6 +497,9 @@ namespace Barotrauma [Serialize(1f, true, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable] public float Scale { get; set; } + [Serialize(false, false), Editable(ReadOnly = true)] + public bool WeldJoint { get; set; } + public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } } @@ -605,7 +614,11 @@ namespace Barotrauma [Serialize(1f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)] public float AttackForceMultiplier { get; set; } + [Serialize(1f, true, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)] + public float MinSeveranceDamage { get; set; } + // Non-editable -> + // TODO: make read-only [Serialize(0, true)] public int HealthIndex { get; set; } @@ -936,7 +949,7 @@ namespace Barotrauma { public override string Name => "Light Texture"; - [Serialize("", true), Editable] + [Serialize("Content/Lights/pointlight_bright.png", true), Editable] public string Texture { get; private set; } [Serialize("0.5, 0.5", true), Editable(DecimalCount = 2)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs index abe6aab58..433da2d10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs @@ -128,7 +128,7 @@ namespace Barotrauma if (Current == null) { - DebugConsole.NewMessage("Now skill settings found in the selected content packages. Using default values."); + DebugConsole.NewMessage("No skill settings found in the selected content packages. Using default values."); Current = new SkillSettings(null); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index 36768f311..dfb7c477b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Security.Cryptography; using System.Xml.Linq; @@ -53,7 +53,7 @@ namespace Barotrauma //these types of files are included in the MD5 hash calculation, //meaning that the players must have the exact same files to play together - private static HashSet multiplayerIncompatibleContent = new HashSet + public static HashSet MultiplayerIncompatibleContent { get; private set; } = new HashSet { ContentType.Jobs, ContentType.Item, @@ -161,7 +161,7 @@ namespace Barotrauma public bool HasMultiplayerIncompatibleContent { - get { return Files.Any(f => multiplayerIncompatibleContent.Contains(f.Type)); } + get { return Files.Any(f => MultiplayerIncompatibleContent.Contains(f.Type)); } } private ContentPackage() @@ -414,7 +414,42 @@ namespace Barotrauma doc.Root.Add(new XElement(file.Type.ToString(), new XAttribute("file", file.Path.CleanUpPathCrossPlatform()))); } - doc.Save(filePath); + doc.SaveSafe(filePath); + + var packagesToDeselect = List.Where(p => p.Path.CleanUpPath() == Path.CleanUpPath()).ToList(); + bool reselectPackage = false; + + if (packagesToDeselect.Any()) + { + foreach (var p in packagesToDeselect) + { + if (GameMain.Config.SelectedContentPackages.Contains(p)) + { + reselectPackage = true; + if (p.CorePackage) + { + GameMain.Config.SelectCorePackage(List.Find(cpp => cpp.CorePackage && !packagesToDeselect.Contains(cpp))); + } + else + { + GameMain.Config.DeselectContentPackage(p); + } + } + List.Remove(p); + } + List.Add(this); + if (reselectPackage) + { + if (CorePackage) + { + GameMain.Config.SelectCorePackage(this); + } + else + { + GameMain.Config.SelectContentPackage(this); + } + } + } } public void CalculateHash(bool logging = false) @@ -428,7 +463,7 @@ namespace Barotrauma foreach (ContentFile file in Files) { - if (!multiplayerIncompatibleContent.Contains(file.Type)) { continue; } + if (!MultiplayerIncompatibleContent.Contains(file.Type)) { continue; } try { @@ -539,7 +574,7 @@ namespace Barotrauma while (true) { - string temp = System.IO.Path.GetDirectoryName(path); + string temp = Barotrauma.IO.Path.GetDirectoryName(path); if (string.IsNullOrEmpty(temp)) { break; } path = temp; } @@ -580,7 +615,7 @@ namespace Barotrauma } } - string[] files = Directory.GetFiles(folder, "*.xml"); + IEnumerable files = Directory.GetFiles(folder, "*.xml"); List.Clear(); @@ -589,12 +624,12 @@ namespace Barotrauma List.Add(new ContentPackage(filePath)); } - string[] modDirectories = Directory.GetDirectories("Mods"); + IEnumerable modDirectories = Directory.GetDirectories("Mods"); foreach (string modDirectory in modDirectories) { - if (System.IO.Path.GetFileName(modDirectory.TrimEnd(System.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; } - string modFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName); - string copyingFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.CopyIndicatorFileName); + if (Barotrauma.IO.Path.GetFileName(modDirectory.TrimEnd(Barotrauma.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; } + string modFilePath = Barotrauma.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName); + string copyingFilePath = Barotrauma.IO.Path.Combine(modDirectory, Steam.SteamManager.CopyIndicatorFileName); if (File.Exists(copyingFilePath)) { //this mod didn't clean up its copying file; assume it's corrupted and delete it @@ -615,22 +650,30 @@ namespace Barotrauma public static void SortContentPackages() { - List = List - .OrderByDescending(p => p.CorePackage) - .ThenBy(p => List.IndexOf(p)) - .ToList(); - if (GameMain.Config != null) { + List = List + .OrderByDescending(p => p.CorePackage) + .ThenBy(p => GameMain.Config.SelectedContentPackages.IndexOf(p)) + .ThenBy(p => List.IndexOf(p)) + .ToList(); + var sortedSelected = GameMain.Config.SelectedContentPackages .OrderByDescending(p => p.CorePackage) - .ThenBy(p => List.IndexOf(p)) + .ThenBy(p => GameMain.Config.SelectedContentPackages.IndexOf(p)) .ToList(); GameMain.Config.SelectedContentPackages.Clear(); GameMain.Config.SelectedContentPackages.AddRange(sortedSelected); - var reportList = List.Where(p => GameMain.Config.SelectedContentPackages.Contains(p)); + var reportList = GameMain.Config.SelectedContentPackages; DebugConsole.NewMessage($"Content package load order: { string.Join(" | ", reportList.Select(cp => cp.Name)) }"); } + else + { + List = List + .OrderByDescending(p => p.CorePackage) + .ThenBy(p => List.IndexOf(p)) + .ToList(); + } } public void Delete() diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index b05f4bc9c..99852f5bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -8,7 +8,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; @@ -292,7 +292,7 @@ namespace Barotrauma commands.Add(new Command("startwhenclientsready", "startwhenclientsready [true/false]: Enable or disable automatically starting the round when clients are ready to start.", null)); - commands.Add(new Command("giveperm", "giveperm [id]: Grants administrative permissions to the player with the specified client ID.", null, + commands.Add(new Command("giveperm", "giveperm [id/steamid/endpoint/name]: Grants administrative permissions to the specified client.", null, () => { if (GameMain.NetworkMember == null) return null; @@ -304,7 +304,7 @@ namespace Barotrauma }; })); - commands.Add(new Command("revokeperm", "revokeperm [id]: Revokes administrative permissions to the player with the specified client ID.", null, + commands.Add(new Command("revokeperm", "revokeperm [id/steamid/endpoint/name]: Revokes administrative permissions from the specified client.", null, () => { if (GameMain.NetworkMember == null) return null; @@ -316,7 +316,7 @@ namespace Barotrauma }; })); - commands.Add(new Command("giverank", "giverank [id]: Assigns a specific rank (= a set of administrative permissions) to the player with the specified client ID.", null, + commands.Add(new Command("giverank", "giverank [id/steamid/endpoint/name]: Assigns a specific rank (= a set of administrative permissions) to the specified client.", null, () => { if (GameMain.NetworkMember == null) return null; @@ -328,12 +328,41 @@ namespace Barotrauma }; })); - commands.Add(new Command("givecommandperm", "givecommandperm [id]: Gives the player with the specified client ID the permission to use the specified console commands.", null)); + commands.Add(new Command("givecommandperm", "givecommandperm [id/steamid/endpoint/name]: Gives the specified client the permission to use the specified console commands.", null, + () => + { + if (GameMain.NetworkMember == null) return null; + + return new string[][] + { + GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), + commands.Select(c => c.names[0]).ToArray() + }; + })); + + commands.Add(new Command("revokecommandperm", "revokecommandperm [id/steamid/endpoint/name]: Revokes permission to use the specified console commands from the specified client.", null, + () => + { + if (GameMain.NetworkMember == null) return null; + + return new string[][] + { + GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), + new string[0] + }; + })); + + commands.Add(new Command("showperm", "showperm [id/steamid/endpoint/name]: Shows the current administrative permissions of the specified client.", null, + () => + { + if (GameMain.NetworkMember == null) return null; + + return new string[][] + { + GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray() + }; + })); - commands.Add(new Command("revokecommandperm", "revokecommandperm [id]: Revokes permission to use the specified console commands from the player with the specified client ID.", null)); - - commands.Add(new Command("showperm", "showperm [id]: Shows the current administrative permissions of the client with the specified client ID.", null)); - commands.Add(new Command("respawnnow", "respawnnow: Trigger a respawn immediately if there are any clients waiting to respawn.", null)); commands.Add(new Command("showkarma", "showkarma: Show the current karma values of the players.", null)); @@ -692,7 +721,7 @@ namespace Barotrauma } },null)); - commands.Add(new Command("teleportsub", "teleportsub [start/end]: Teleport the submarine to the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => + commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => { if (Submarine.MainSub == null || Level.Loaded == null) return; @@ -975,6 +1004,22 @@ namespace Barotrauma } })); + commands.Add(new Command("money", "", args => + { + if (args.Length == 0) { return; } + if (GameMain.GameSession.GameMode is CampaignMode campaign) + { + if (int.TryParse(args[0], out int money)) + { + campaign.Money += money; + } + else + { + ThrowError($"\"{args[0]}\" is not a valid numeric value."); + } + } + }, isCheat: true)); + commands.Add(new Command("difficulty|leveldifficulty", "difficulty [0-100]: Change the level difficulty setting in the server lobby.", null)); commands.Add(new Command("autoitemplacerdebug|outfitdebug", "autoitemplacerdebug: Toggle automatic item placer debug info on/off. The automatically placed items are listed in the debug console at the start of a round.", (string[] args) => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index cdcb77796..3d70417af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Barotrauma +namespace Barotrauma { public enum TransitionMode { @@ -13,4 +9,17 @@ namespace Barotrauma EaseOut, Exponential } + + public enum ActionType + { + Always, OnPicked, OnUse, OnSecondaryUse, + OnWearing, OnContaining, OnContained, OnNotContained, + OnActive, OnFailure, OnBroken, + OnFire, InWater, NotInWater, + OnImpact, + OnEating, + OnDeath = OnBroken, + OnDamaged, + OnSevered + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 55a6acc56..28b55c962 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using FarseerPhysics; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +10,8 @@ namespace Barotrauma { const float IntensityUpdateInterval = 5.0f; + const float CalculateDistanceTraveledInterval = 5.0f; + private Level level; private readonly List preloadedSprites = new List(); @@ -30,6 +33,11 @@ namespace Barotrauma private float intensityUpdateTimer; + private PathFinder pathFinder; + private float totalPathLength; + private float calculateDistanceTraveledTimer; + private float distanceTraveled; + private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger; private float roundDuration; @@ -72,6 +80,10 @@ namespace Barotrauma pendingEventSets.Clear(); selectedEvents.Clear(); + pathFinder = new PathFinder(WayPoint.WayPointList, indoorsSteering: false); + var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(Level.Loaded.StartPosition), ConvertUnits.ToSimUnits(Level.Loaded.EndPosition)); + totalPathLength = steeringPath.TotalLength; + this.level = level; SelectSettings(); @@ -137,7 +149,44 @@ namespace Barotrauma public void PreloadContent(IEnumerable contentFiles) { - foreach (ContentFile file in contentFiles) + var filesToPreload = new List(contentFiles); + foreach (Submarine sub in Submarine.Loaded) + { + if (sub.WreckAI == null) { continue; } + + if (!string.IsNullOrEmpty(sub.WreckAI.Config.DefensiveAgent)) + { + var prefab = CharacterPrefab.FindBySpeciesName(sub.WreckAI.Config.DefensiveAgent); + if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath)) + { + filesToPreload.Add(new ContentFile(prefab.FilePath, ContentType.Character)); + } + } + foreach (Item item in Item.ItemList) + { + if (item.Submarine != sub) { continue; } + foreach (Items.Components.ItemComponent component in item.Components) + { + if (component.statusEffectLists == null) { continue; } + foreach (var statusEffectList in component.statusEffectLists.Values) + { + foreach (StatusEffect statusEffect in statusEffectList) + { + foreach (var spawnInfo in statusEffect.SpawnCharacters) + { + var prefab = CharacterPrefab.FindBySpeciesName(spawnInfo.SpeciesName); + if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath)) + { + filesToPreload.Add(new ContentFile(prefab.FilePath, ContentType.Character)); + } + } + } + } + } + } + } + + foreach (ContentFile file in filesToPreload) { switch (file.Type) { @@ -299,12 +348,9 @@ namespace Barotrauma private bool CanStartEventSet(ScriptedEventSet eventSet) { - float distFromStart = Vector2.Distance(Submarine.MainSub.WorldPosition, level.StartPosition); - float distFromEnd = Vector2.Distance(Submarine.MainSub.WorldPosition, level.EndPosition); - - float distanceTraveled = MathHelper.Clamp( - (Submarine.MainSub.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), - 0.0f, 1.0f); + ISpatialEntity refEntity = GetRefEntity(); + float distFromStart = Vector2.Distance(refEntity.WorldPosition, level.StartPosition); + float distFromEnd = Vector2.Distance(refEntity.WorldPosition, level.EndPosition); //don't create new events if within 50 meters of the start/end of the level if (!eventSet.AllowAtStart) @@ -367,6 +413,13 @@ namespace Barotrauma } } + calculateDistanceTraveledTimer -= deltaTime; + if (calculateDistanceTraveledTimer <= 0.0f) + { + distanceTraveled = CalculateDistanceTraveled(); + calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval; + } + eventThreshold += settings.EventThresholdIncrease * deltaTime; if (eventCoolDown > 0.0f) { @@ -514,5 +567,62 @@ namespace Barotrauma currentIntensity = MathHelper.Max(0.0025f * IntensityUpdateInterval, targetIntensity); } } + + private float CalculateDistanceTraveled() + { + var refEntity = GetRefEntity(); + Vector2 target = ConvertUnits.ToSimUnits(Level.Loaded.EndPosition); + var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target); + if (steeringPath.Unreachable || float.IsPositiveInfinity(totalPathLength)) + { + //use horizontal position in the level as a fallback if a path can't be found + return MathHelper.Clamp((refEntity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f); + } + else + { + return MathHelper.Clamp(1.0f - steeringPath.TotalLength / totalPathLength, 0.0f, 1.0f); + } + } + + + /// + /// Get the entity that should be used in determining how far the player has progressed in the level. + /// = The submarine or player character that has progressed the furthest. + /// + private ISpatialEntity GetRefEntity() + { + ISpatialEntity refEntity = Submarine.MainSub; +#if CLIENT + if (Character.Controlled != null) + { + if (Character.Controlled.Submarine != null && + Character.Controlled.Submarine.Info.Type == SubmarineInfo.SubmarineType.Player) + { + refEntity = Character.Controlled.Submarine; + } + else + { + refEntity = Character.Controlled; + } + } +#else + foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients) + { + if (client.Character == null) { continue; } + //only take the players inside a player sub into account. + //Otherwise the system could be abused by for example making a respawned player wait + //close to the destination outpost + if (client.Character.Submarine != null && + client.Character.Submarine.Info.Type == SubmarineInfo.SubmarineType.Player) + { + if (client.Character.Submarine.WorldPosition.X > refEntity.WorldPosition.X) + { + refEntity = client.Character.Submarine; + } + } + } +#endif + return refEntity; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs index d6923a024..1f425f4ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs @@ -24,8 +24,9 @@ namespace Barotrauma public readonly float MinLevelDifficulty = 0.0f; public readonly float MaxLevelDifficulty = 100.0f; - static EventManagerSettings() + public static void Init() { + List.Clear(); foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.EventManagerSettings)) { Load(file); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index c5960b7e4..35d2e53c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -10,6 +11,8 @@ namespace Barotrauma private readonly XElement itemConfig; private readonly List items = new List(); + private readonly Dictionary itemIDs = new Dictionary(); + private readonly Dictionary parentInventoryIDs = new Dictionary(); private int requiredDeliveryAmount; @@ -22,8 +25,6 @@ namespace Barotrauma private void InitItems() { - items.Clear(); - if (itemConfig == null) { DebugConsole.ThrowError("Failed to initialize items for cargo mission (itemConfig == null)"); @@ -91,8 +92,13 @@ namespace Barotrauma var item = new Item(itemPrefab, position, cargoRoom.Submarine); item.FindHull(); items.Add(item); - - if (parent != null) parent.Combine(item, user: null); + itemIDs.Add(item, item.ID); + + if (parent != null) + { + parentInventoryIDs.Add(item, parent.ID); + parent.Combine(item, user: null); + } foreach (XElement subElement in element.Elements()) { @@ -106,6 +112,10 @@ namespace Barotrauma public override void Start(Level level) { + items.Clear(); + itemIDs.Clear(); + parentInventoryIDs.Clear(); + if (!IsClient) { InitItems(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 20c2dafd9..222cad5c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -108,23 +108,6 @@ namespace Barotrauma subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition)); subs[1].FlipX(); - //prevent wifi components from communicating between subs - List wifiComponents = new List(); - foreach (Item item in Item.ItemList) - { - wifiComponents.AddRange(item.GetComponents()); - } - foreach (WifiComponent wifiComponent in wifiComponents) - { - for (int i = 0; i < 2; i++) - { - if (wifiComponent.Item.Submarine == subs[i] || subs[i].ConnectedDockingPorts.ContainsKey(wifiComponent.Item.Submarine)) - { - wifiComponent.TeamID = subs[i].TeamID; - } - } - } - crews = new List[] { new List(), new List() }; foreach (Submarine submarine in Submarine.Loaded) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 88c43fabb..7bc3c98f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -70,7 +70,7 @@ namespace Barotrauma monsterFiles.Add(new Tuple(monster, new Point(min, max))); } description = description.Replace("[monster]", - TextManager.Get("character." + System.IO.Path.GetFileNameWithoutExtension(monsterFileName))); + TextManager.Get("character." + Barotrauma.IO.Path.GetFileNameWithoutExtension(monsterFileName))); } public override void Start(Level level) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 2c3333212..eddd5c5d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -103,6 +103,10 @@ namespace Barotrauma public override void Start(Level level) { +#if SERVER + originalItemID = Entity.NullEntityID; + originalInventoryID = Entity.NullEntityID; +#endif if (!IsClient) { //ruin/wreck items are allowed to spawn close to the sub @@ -147,6 +151,9 @@ namespace Barotrauma item.body.FarseerBody.BodyType = BodyType.Kinematic; item.FindHull(); } +#if SERVER + originalItemID = item.ID; +#endif for (int i = 0; i < statusEffects.Count; i++) { @@ -166,6 +173,7 @@ namespace Barotrauma foreach (Item it in Item.ItemList) { if (!it.HasTag(containerTag)) { continue; } + if (it.NonInteractable) { continue; } switch (spawnPositionType) { case Level.PositionType.Cave: @@ -181,7 +189,13 @@ namespace Barotrauma } var itemContainer = it.GetComponent(); if (itemContainer == null) { continue; } - if (itemContainer.Combine(item, user: null)) { break; } // Placement successful + if (itemContainer.Combine(item, user: null)) + { +#if SERVER + originalInventoryID = it.ID; +#endif + break; + } // Placement successful } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 02bdb78d8..2638d88a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -13,6 +13,9 @@ namespace Barotrauma private readonly int minAmount, maxAmount; private List monsters; + private readonly float scatter; + private readonly float offset; + private readonly bool spawnDeep; private Vector2? spawnPos; @@ -72,6 +75,8 @@ namespace Barotrauma } spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false); + offset = prefab.ConfigElement.GetAttributeFloat("offset", 0); + scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 1000), 0, 3000); if (GameMain.NetworkMember != null) { @@ -118,7 +123,7 @@ namespace Barotrauma private List GetAvailableSpawnPositions() { - var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType) && !Level.Loaded.UsedPositions.Contains(p)); + var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType)); var removals = new List(); foreach (var position in availablePositions) { @@ -169,10 +174,6 @@ namespace Barotrauma if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability) { removedPositions.Add(position); - if (prefab.AllowOnlyOnce) - { - Level.Loaded.UsedPositions.Add(position); - } } } removedPositions.ForEach(p => availablePositions.Remove(p)); @@ -245,11 +246,34 @@ namespace Barotrauma spawnPos = spawnPoint.WorldPosition; } } - spawnPending = true; - if (prefab.AllowOnlyOnce) + else if (chosenPosition.PositionType == Level.PositionType.MainPath && offset > 0) { - Level.Loaded.UsedPositions.Add(chosenPosition); + Vector2 dir; + var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null); + var nearestWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value)).FirstOrDefault(); + if (nearestWaypoint != null) + { + int currentIndex = waypoints.IndexOf(nearestWaypoint); + var nextWaypoint = waypoints[Math.Min(currentIndex + 20, waypoints.Count - 1)]; + dir = Vector2.Normalize(nextWaypoint.WorldPosition - nearestWaypoint.WorldPosition); + // Ensure that the spawn position is not offset to the left. + if (dir.X < 0) + { + dir.X = 0; + } + } + else + { + dir = new Vector2(1, Rand.Range(-1, 1)); + } + Vector2 targetPos = spawnPos.Value + dir * offset; + var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault(); + if (targetWaypoint != null) + { + spawnPos = targetWaypoint.WorldPosition; + } } + spawnPending = true; } } @@ -278,11 +302,14 @@ namespace Barotrauma if (spawnPending) { //wait until there are no submarines at the spawnpos - foreach (Submarine submarine in Submarine.Loaded) + if (spawnPosType == Level.PositionType.MainPath) { - if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } - float minDist = GetMinDistanceToSub(submarine); - if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; } + foreach (Submarine submarine in Submarine.Loaded) + { + if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } + float minDist = GetMinDistanceToSub(submarine); + if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; } + } } //if spawning in a ruin/cave, wait for someone to be close to it to spawning @@ -319,7 +346,7 @@ namespace Barotrauma //+1 because Range returns an integer less than the max value int amount = Rand.Range(minAmount, maxAmount + 1); monsters = new List(); - float offsetAmount = spawnPosType == Level.PositionType.MainPath ? 1000 : 100; + float offsetAmount = spawnPosType == Level.PositionType.MainPath ? scatter : 100; for (int i = 0; i < amount; i++) { CoroutineManager.InvokeAfter(() => @@ -329,7 +356,22 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events."); - monsters.Add(Character.Create(speciesName, spawnPos.Value + Rand.Vector(offsetAmount), Level.Loaded.Seed + i.ToString(), null, false, true, true)); + Vector2 pos = spawnPos.Value + Rand.Vector(offsetAmount); + if (spawnPosType == Level.PositionType.MainPath) + { + if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos))) + { + // Can't use the offset position, let's use the exact spawn position. + pos = spawnPos.Value; + } + else if (Level.Loaded.Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).ContainsWorld(pos))) + { + // Can't use the offset position, let's use the exact spawn position. + pos = spawnPos.Value; + } + } + + monsters.Add(Character.Create(speciesName, pos, Level.Loaded.Seed + i.ToString(), null, false, true, true)); if (monsters.Count == amount) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs index a6dc76b1e..877988d39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs @@ -11,7 +11,6 @@ namespace Barotrauma public readonly Type EventType; public readonly string MusicType; public readonly float SpawnProbability; - public readonly bool AllowOnlyOnce; public float Commonness; public ScriptedEventPrefab(XElement element) @@ -34,7 +33,6 @@ namespace Barotrauma } Commonness = element.GetAttributeFloat("commonness", 1.0f); SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1); - AllowOnlyOnce = element.GetAttributeBool("allowonlyonce", false); } public ScriptedEvent CreateInstance() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs index 33a357cc0..2fb9be6e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs @@ -1,13 +1,28 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices.ComTypes; using System.Xml.Linq; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma -{ +{ + class ScriptedEventSet { + internal class EventDebugStats + { + public readonly ScriptedEventSet RootSet; + public readonly Dictionary MonsterCounts = new Dictionary(); + + public EventDebugStats(ScriptedEventSet rootSet) + { + RootSet = rootSet; + } + } + public static List List { get; @@ -131,5 +146,115 @@ namespace Barotrauma } } } + + public static List GetDebugStatistics(int simulatedRoundCount = 100) + { + List debugLines = new List(); + + foreach (var eventSet in List) + { + List stats = new List(); + for (int i = 0; i < simulatedRoundCount; i++) + { + var newStats = new EventDebugStats(eventSet); + CheckEventSet(newStats, eventSet); + stats.Add(newStats); + } + debugLines.Add($"Event stats ({eventSet.DebugIdentifier}): "); + LogEventStats(stats, debugLines); + } + + for (int difficulty = 0; difficulty <= 100; difficulty += 10) + { + debugLines.Add($"Event stats on difficulty level {difficulty}: "); + List stats = new List(); + for (int i = 0; i < simulatedRoundCount; i++) + { + ScriptedEventSet selectedSet = List.Where(s => difficulty >= s.MinLevelDifficulty && difficulty <= s.MaxLevelDifficulty).GetRandom(); + if (selectedSet == null) { continue; } + var newStats = new EventDebugStats(selectedSet); + CheckEventSet(newStats, selectedSet); + stats.Add(newStats); + } + LogEventStats(stats, debugLines); + } + + return debugLines; + + static void CheckEventSet(EventDebugStats stats, ScriptedEventSet thisSet) + { + if (thisSet.ChooseRandom) + { + var eventPrefab = ToolBox.SelectWeightedRandom(thisSet.EventPrefabs, thisSet.EventPrefabs.Select(e => e.Commonness).ToList(), Rand.RandSync.Unsynced); + if (eventPrefab != null) + { + AddEvent(stats, eventPrefab); + } + } + else + { + foreach (var eventPrefab in thisSet.EventPrefabs) + { + AddEvent(stats, eventPrefab); + } + } + foreach (var childSet in thisSet.ChildSets) + { + CheckEventSet(stats, childSet); + } + } + + static void AddEvent(EventDebugStats stats, ScriptedEventPrefab eventPrefab) + { + if (eventPrefab.EventType == typeof(MonsterEvent)) + { + float spawnProbability = eventPrefab.ConfigElement.GetAttributeFloat("spawnprobability", 1.0f); + if (Rand.Value(Rand.RandSync.Server) > spawnProbability) + { + return; + } + + string character = eventPrefab.ConfigElement.GetAttributeString("characterfile", ""); + System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(character)); + int amount = eventPrefab.ConfigElement.GetAttributeInt("amount", 0); + int minAmount = eventPrefab.ConfigElement.GetAttributeInt("minamount", amount); + int maxAmount = eventPrefab.ConfigElement.GetAttributeInt("maxamount", amount); + + int count = Rand.Range(minAmount, maxAmount + 1); + if (count <= 0) { return; } + + if (!stats.MonsterCounts.ContainsKey(character)) { stats.MonsterCounts[character] = 0; } + stats.MonsterCounts[character] += count; + } + } + + static void LogEventStats(List stats, List debugLines) + { + if (stats.Count == 0 || stats.All(s => s.MonsterCounts.Values.Sum() == 0)) + { + debugLines.Add(" No monster spawns"); + debugLines.Add($" "); + } + else + { + stats.Sort((s1, s2) => { return s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum()); }); + + EventDebugStats minStats = stats.First(); + EventDebugStats maxStats = stats.First(); + debugLines.Add($" Minimum monster spawns: {stats.First().MonsterCounts.Values.Sum()}"); + debugLines.Add($" {LogMonsterCounts(stats.First())}"); + debugLines.Add($" Median monster spawns: {stats[stats.Count / 2].MonsterCounts.Values.Sum()}"); + debugLines.Add($" {LogMonsterCounts(stats[stats.Count / 2])}"); + debugLines.Add($" Maximum monster spawns: {stats.Last().MonsterCounts.Values.Sum()}"); + debugLines.Add($" {LogMonsterCounts(stats.Last())}"); + debugLines.Add($" "); + } + } + + static string LogMonsterCounts(EventDebugStats stats) + { + return string.Join(", ", stats.MonsterCounts.Select(mc => mc.Key + " x " + mc.Value)); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index b2dfb78c1..ecc1c2d72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -32,13 +32,22 @@ namespace Barotrauma.Extensions public static T GetRandom(this IEnumerable source, Func predicate, Rand.RandSync randSync = Rand.RandSync.Unsynced) { + if (predicate == null) { return GetRandom(source, randSync); } return source.Where(predicate).GetRandom(randSync); } public static T GetRandom(this IEnumerable source, Rand.RandSync randSync = Rand.RandSync.Unsynced) { - int count = source.Count(); - return count == 0 ? default(T) : source.ElementAt(Rand.Range(0, count, randSync)); + if (source is IList list) + { + int count = list.Count; + return count == 0 ? default : list[Rand.Range(0, count, randSync)]; + } + else + { + int count = source.Count(); + return count == 0 ? default : source.ElementAt(Rand.Range(0, count, randSync)); + } } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs index b634bf35a..3975c1cd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs @@ -2,7 +2,7 @@ using System; using System.Text; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 83825c377..2cf23ecdb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -174,6 +174,10 @@ namespace Barotrauma } var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine); + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = validContainer.Key.Item.Submarine.TeamID; + } spawnedItems.Add(item); #if SERVER Entity.Spawner.CreateNetworkEvent(item, remove: false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 32e2756bb..3561f8ce9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -4,7 +4,7 @@ using System; using System.Linq; using System.Xml.Linq; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index f4aedc0da..c90e1ed40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.IO; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -231,7 +232,7 @@ namespace Barotrauma if (port.Item.WorldPosition.Y < Submarine.WorldPosition.Y) { continue; } float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition); - if (myPort == null || dist < closestDistance || (port.MainDockingPort && !myPort.MainDockingPort)) + if ((myPort == null || dist < closestDistance || port.MainDockingPort) && !(myPort?.MainDockingPort ?? false)) { myPort = port; closestDistance = dist; @@ -463,7 +464,7 @@ namespace Barotrauma try { - doc.Save(filePath); + doc.SaveSafe(filePath); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index ef8f66979..58d0c9bc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -2,8 +2,7 @@ using System.Xml.Linq; using System.Collections.Generic; using Microsoft.Xna.Framework; -using System.Xml; -using System.IO; +using Barotrauma.IO; using Barotrauma.Extensions; #if CLIENT using Microsoft.Xna.Framework.Input; @@ -37,6 +36,8 @@ namespace Barotrauma public bool VSyncEnabled { get; set; } + public bool TextureCompressionEnabled { get; set; } + public bool EnableSplashScreen { get; set; } public int ParticleLimit { get; set; } @@ -66,6 +67,7 @@ namespace Barotrauma #if CLIENT private KeyOrMouse[] keyMapping; + private KeyOrMouse[] inventoryKeyMapping; #endif private WindowMode windowMode; @@ -207,7 +209,7 @@ namespace Barotrauma { musicVolume = MathHelper.Clamp(value, 0.0f, 1.0f); #if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("music", musicVolume, 0); + GameMain.SoundManager?.SetCategoryGainMultiplier("music", musicVolume * 0.7f, 0); #endif } } @@ -267,7 +269,7 @@ namespace Barotrauma public bool TextManagerDebugModeEnabled { get; set; } #endif - private FileSystemWatcher modsFolderWatcher; + private System.IO.FileSystemWatcher modsFolderWatcher; private int ContentFileLoadOrder(ContentFile a) { @@ -301,58 +303,11 @@ namespace Barotrauma !otherCorePackage.Files.Any(f2 => Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - bool shouldRefreshSubs = false; - bool shouldRefreshFabricationRecipes = false; - bool shouldRefreshSoundPlayer = false; - bool shouldRefreshRuinGenerationParams = false; - bool shouldRefreshScriptedEventSets = false; - bool shouldRefreshMissionPrefabs = false; - bool shouldRefreshLevelObjectPrefabs = false; - bool shouldRefreshLocationTypes = false; - bool shouldRefreshMapGenerationParams = false; - bool shouldRefreshLevelGenerationParams = false; - bool shouldRefreshAfflictions = false; + DisableContentPackageItems(filesToRemove.OrderBy(ContentFileLoadOrder)); - DisableContentPackageItems(filesToRemove.OrderBy(ContentFileLoadOrder), - ref shouldRefreshSubs, - ref shouldRefreshFabricationRecipes, - ref shouldRefreshSoundPlayer, - ref shouldRefreshRuinGenerationParams, - ref shouldRefreshScriptedEventSets, - ref shouldRefreshMissionPrefabs, - ref shouldRefreshLevelObjectPrefabs, - ref shouldRefreshLocationTypes, - ref shouldRefreshMapGenerationParams, - ref shouldRefreshLevelGenerationParams, - ref shouldRefreshAfflictions); + EnableContentPackageItems(filesToAdd.OrderBy(ContentFileLoadOrder)); - EnableContentPackageItems(filesToAdd.OrderBy(ContentFileLoadOrder), - ref shouldRefreshSubs, - ref shouldRefreshFabricationRecipes, - ref shouldRefreshSoundPlayer, - ref shouldRefreshRuinGenerationParams, - ref shouldRefreshScriptedEventSets, - ref shouldRefreshMissionPrefabs, - ref shouldRefreshLevelObjectPrefabs, - ref shouldRefreshLocationTypes, - ref shouldRefreshMapGenerationParams, - ref shouldRefreshLevelGenerationParams, - ref shouldRefreshAfflictions); - - if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } - if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } - if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } - if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } - if (shouldRefreshMissionPrefabs) { MissionPrefab.Init(); } - if (shouldRefreshLevelObjectPrefabs) { LevelObjectPrefab.LoadAll(); } - if (shouldRefreshLocationTypes) { LocationType.Init(); } - if (shouldRefreshMapGenerationParams) { MapGenerationParams.Init(); } - if (shouldRefreshLevelGenerationParams) { LevelGenerationParams.LoadPresets(); } - -#if CLIENT - if (shouldRefreshSoundPlayer) { SoundPlayer.Init().ForEach(_ => { return; }); } -#endif + RefreshContentPackageItems(filesToAdd.Concat(filesToRemove)); } public void SelectContentPackage(ContentPackage contentPackage) @@ -362,45 +317,9 @@ namespace Barotrauma SelectedContentPackages.Add(contentPackage); ContentPackage.SortContentPackages(); - bool shouldRefreshSubs = false; - bool shouldRefreshFabricationRecipes = false; - bool shouldRefreshSoundPlayer = false; - bool shouldRefreshRuinGenerationParams = false; - bool shouldRefreshScriptedEventSets = false; - bool shouldRefreshMissionPrefabs = false; - bool shouldRefreshLevelObjectPrefabs = false; - bool shouldRefreshLocationTypes = false; - bool shouldRefreshMapGenerationParams = false; - bool shouldRefreshLevelGenerationParams = false; - bool shouldRefreshAfflictions = false; + EnableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder)); - EnableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder), - ref shouldRefreshSubs, - ref shouldRefreshFabricationRecipes, - ref shouldRefreshSoundPlayer, - ref shouldRefreshRuinGenerationParams, - ref shouldRefreshScriptedEventSets, - ref shouldRefreshMissionPrefabs, - ref shouldRefreshLevelObjectPrefabs, - ref shouldRefreshLocationTypes, - ref shouldRefreshMapGenerationParams, - ref shouldRefreshLevelGenerationParams, - ref shouldRefreshAfflictions); - - if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } - if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } - if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } - if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } - if (shouldRefreshMissionPrefabs) { MissionPrefab.Init(); } - if (shouldRefreshLevelObjectPrefabs) { LevelObjectPrefab.LoadAll(); } - if (shouldRefreshLocationTypes) { LocationType.Init(); } - if (shouldRefreshMapGenerationParams) { MapGenerationParams.Init(); } - if (shouldRefreshLevelGenerationParams) { LevelGenerationParams.LoadPresets(); } - -#if CLIENT - if (shouldRefreshSoundPlayer) { SoundPlayer.Init().ForEach(_ => { return; }); } -#endif + RefreshContentPackageItems(contentPackage.Files); } } @@ -411,61 +330,14 @@ namespace Barotrauma SelectedContentPackages.Remove(contentPackage); ContentPackage.SortContentPackages(); - bool shouldRefreshSubs = false; - bool shouldRefreshFabricationRecipes = false; - bool shouldRefreshSoundPlayer = false; - bool shouldRefreshRuinGenerationParams = false; - bool shouldRefreshScriptedEventSets = false; - bool shouldRefreshMissionPrefabs = false; - bool shouldRefreshLevelObjectPrefabs = false; - bool shouldRefreshLocationTypes = false; - bool shouldRefreshMapGenerationParams = false; - bool shouldRefreshLevelGenerationParams = false; - bool shouldRefreshAfflictions = false; + DisableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder)); - DisableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder), - ref shouldRefreshSubs, - ref shouldRefreshFabricationRecipes, - ref shouldRefreshSoundPlayer, - ref shouldRefreshRuinGenerationParams, - ref shouldRefreshScriptedEventSets, - ref shouldRefreshMissionPrefabs, - ref shouldRefreshLevelObjectPrefabs, - ref shouldRefreshLocationTypes, - ref shouldRefreshMapGenerationParams, - ref shouldRefreshLevelGenerationParams, - ref shouldRefreshAfflictions); - - if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } - if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } - if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } - if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } - if (shouldRefreshMissionPrefabs) { MissionPrefab.Init(); } - if (shouldRefreshLevelObjectPrefabs) { LevelObjectPrefab.LoadAll(); } - if (shouldRefreshLocationTypes) { LocationType.Init(); } - if (shouldRefreshMapGenerationParams) { MapGenerationParams.Init(); } - if (shouldRefreshLevelGenerationParams) { LevelGenerationParams.LoadPresets(); } - -#if CLIENT - if (shouldRefreshSoundPlayer) { SoundPlayer.Init().ForEach(_ => { return; }); } -#endif + RefreshContentPackageItems(contentPackage.Files); } } - private void EnableContentPackageItems(IOrderedEnumerable files, - ref bool shouldRefreshSubs, - ref bool shouldRefreshFabricationRecipes, - ref bool shouldRefreshSoundPlayer, - ref bool shouldRefreshRuinGenerationParams, - ref bool shouldRefreshScriptedEventSets, - ref bool shouldRefreshMissionPrefabs, - ref bool shouldRefreshLevelObjectPrefabs, - ref bool shouldRefreshLocationTypes, - ref bool shouldRefreshMapGenerationParams, - ref bool shouldRefreshLevelGenerationParams, - ref bool shouldRefreshAfflictions) + private void EnableContentPackageItems(IOrderedEnumerable files) { foreach (ContentFile file in files) { @@ -474,6 +346,9 @@ namespace Barotrauma case ContentType.Character: CharacterPrefab.LoadFromFile(file); break; + case ContentType.Corpses: + CorpsePrefab.LoadFromFile(file); + break; case ContentType.NPCConversations: NPCConversation.LoadFromFile(file); break; @@ -482,7 +357,6 @@ namespace Barotrauma break; case ContentType.Item: ItemPrefab.LoadFromFile(file); - shouldRefreshFabricationRecipes = true; break; case ContentType.ItemAssembly: new ItemAssemblyPrefab(file.Path); @@ -490,40 +364,10 @@ namespace Barotrauma case ContentType.Structure: StructurePrefab.LoadFromFile(file); break; - case ContentType.Submarine: - shouldRefreshSubs = true; - break; case ContentType.Text: TextManager.LoadTextPack(file.Path); break; - case ContentType.Afflictions: - shouldRefreshAfflictions = true; - break; - case ContentType.RuinConfig: - shouldRefreshRuinGenerationParams = true; - break; - case ContentType.RandomEvents: - shouldRefreshScriptedEventSets = true; - break; - case ContentType.Missions: - shouldRefreshMissionPrefabs = true; - break; - case ContentType.LevelObjectPrefabs: - shouldRefreshLevelObjectPrefabs = true; - break; - case ContentType.LocationTypes: - shouldRefreshLocationTypes = true; - break; - case ContentType.MapGenerationParameters: - shouldRefreshMapGenerationParams = true; - break; - case ContentType.LevelGenerationParameters: - shouldRefreshLevelGenerationParams = true; - break; #if CLIENT - case ContentType.Sounds: - shouldRefreshSoundPlayer = true; - break; case ContentType.Particles: GameMain.ParticleManager?.LoadPrefabsFromFile(file); break; @@ -537,18 +381,7 @@ namespace Barotrauma } } - private void DisableContentPackageItems(IOrderedEnumerable files, - ref bool shouldRefreshSubs, - ref bool shouldRefreshFabricationRecipes, - ref bool shouldRefreshSoundPlayer, - ref bool shouldRefreshRuinGenerationParams, - ref bool shouldRefreshScriptedEventSets, - ref bool shouldRefreshMissionPrefabs, - ref bool shouldRefreshLevelObjectPrefabs, - ref bool shouldRefreshLocationTypes, - ref bool shouldRefreshMapGenerationParams, - ref bool shouldRefreshLevelGenerationParams, - ref bool shouldRefreshAfflictions) + private void DisableContentPackageItems(IOrderedEnumerable files) { foreach (ContentFile file in files) { @@ -557,6 +390,9 @@ namespace Barotrauma case ContentType.Character: CharacterPrefab.RemoveByFile(file.Path); break; + case ContentType.Corpses: + CorpsePrefab.RemoveByFile(file.Path); + break; case ContentType.NPCConversations: NPCConversation.RemoveByFile(file.Path); break; @@ -565,7 +401,6 @@ namespace Barotrauma break; case ContentType.Item: ItemPrefab.RemoveByFile(file.Path); - shouldRefreshFabricationRecipes = true; break; case ContentType.ItemAssembly: ItemAssemblyPrefab.Remove(file.Path); @@ -573,40 +408,10 @@ namespace Barotrauma case ContentType.Structure: StructurePrefab.RemoveByFile(file.Path); break; - case ContentType.Submarine: - shouldRefreshSubs = true; - break; case ContentType.Text: TextManager.RemoveTextPack(file.Path); break; - case ContentType.Afflictions: - shouldRefreshAfflictions = true; - break; - case ContentType.RuinConfig: - shouldRefreshRuinGenerationParams = true; - break; - case ContentType.RandomEvents: - shouldRefreshScriptedEventSets = true; - break; - case ContentType.Missions: - shouldRefreshMissionPrefabs = true; - break; - case ContentType.LevelObjectPrefabs: - shouldRefreshLevelObjectPrefabs = true; - break; - case ContentType.LocationTypes: - shouldRefreshLocationTypes = true; - break; - case ContentType.MapGenerationParameters: - shouldRefreshMapGenerationParams = true; - break; - case ContentType.LevelGenerationParameters: - shouldRefreshLevelGenerationParams = true; - break; #if CLIENT - case ContentType.Sounds: - shouldRefreshSoundPlayer = true; - break; case ContentType.Particles: GameMain.ParticleManager?.RemovePrefabsByFile(file.Path); break; @@ -620,39 +425,74 @@ namespace Barotrauma } } + private void RefreshContentPackageItems(IEnumerable files) + { + if (files.Any(f => f.Type == ContentType.Afflictions)) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } + if (files.Any(f => f.Type == ContentType.Submarine)) { SubmarineInfo.RefreshSavedSubs(); } + if (files.Any(f => f.Type == ContentType.Item)) { ItemPrefab.InitFabricationRecipes(); } + if (files.Any(f => f.Type == ContentType.RuinConfig)) { RuinGeneration.RuinGenerationParams.ClearAll(); } + if (files.Any(f => f.Type == ContentType.RandomEvents)) { ScriptedEventSet.LoadPrefabs(); } + if (files.Any(f => f.Type == ContentType.Missions)) { MissionPrefab.Init(); } + if (files.Any(f => f.Type == ContentType.LevelObjectPrefabs)) { LevelObjectPrefab.LoadAll(); } + if (files.Any(f => f.Type == ContentType.LocationTypes)) { LocationType.Init(); } + if (files.Any(f => f.Type == ContentType.MapGenerationParameters)) { MapGenerationParams.Init(); } + if (files.Any(f => f.Type == ContentType.LevelGenerationParameters)) { LevelGenerationParams.LoadPresets(); } + if (files.Any(f => f.Type == ContentType.TraitorMissions)) { TraitorMissionPrefab.Init(); } + if (files.Any(f => f.Type == ContentType.Orders)) { Order.Init(); } + if (files.Any(f => f.Type == ContentType.EventManagerSettings)) { EventManagerSettings.Init(); } + if (files.Any(f => f.Type == ContentType.WreckAIConfig)) { WreckAIConfig.LoadAll(); } + if (files.Any(f => f.Type == ContentType.SkillSettings)) { SkillSettings.Load(GameMain.Instance.GetFilesOfType(ContentType.SkillSettings)); } + +#if CLIENT + if (files.Any(f => f.Type == ContentType.Tutorials)) { Tutorial.Init(); } + if (files.Any(f => f.Type == ContentType.Sounds)) { SoundPlayer.Init().ForEach(_ => { return; }); } +#endif + } + + private readonly static ContentType[] hotswappableContentTypes = new ContentType[] + { + ContentType.Character, + ContentType.Corpses, + ContentType.NPCConversations, + ContentType.Jobs, + ContentType.Orders, + ContentType.EventManagerSettings, + ContentType.Item, + ContentType.ItemAssembly, + ContentType.Structure, + ContentType.Submarine, + ContentType.Text, + ContentType.Afflictions, + ContentType.RuinConfig, + ContentType.RandomEvents, + ContentType.Missions, + ContentType.LevelObjectPrefabs, + ContentType.LocationTypes, + ContentType.MapGenerationParameters, + ContentType.LevelGenerationParameters, + ContentType.Sounds, + ContentType.Particles, + ContentType.Decals, + ContentType.Outpost, + ContentType.Wreck, + ContentType.WreckAIConfig, + ContentType.BackgroundCreaturePrefabs, + ContentType.ServerExecutable, + ContentType.TraitorMissions, + ContentType.Tutorials, + ContentType.SkillSettings, + ContentType.None + }; + private void UpdateContentPackageDirtyFlag(ContentFile file) { - switch (file.Type) + if (!hotswappableContentTypes.Contains(file.Type)) { - case ContentType.Character: - case ContentType.NPCConversations: - case ContentType.Jobs: - case ContentType.Item: - case ContentType.ItemAssembly: - case ContentType.Structure: - case ContentType.Submarine: - case ContentType.Text: - case ContentType.Afflictions: - case ContentType.RuinConfig: - case ContentType.RandomEvents: - case ContentType.Missions: - case ContentType.LevelObjectPrefabs: - case ContentType.LocationTypes: - case ContentType.MapGenerationParameters: - case ContentType.LevelGenerationParameters: - case ContentType.Sounds: - case ContentType.Particles: - case ContentType.Decals: - case ContentType.Outpost: - case ContentType.Wreck: - case ContentType.BackgroundCreaturePrefabs: - case ContentType.ServerExecutable: - case ContentType.None: - break; //do nothing here if the content type is supported - default: + if (ContentPackage.MultiplayerIncompatibleContent.Contains(file.Type)) + { ContentPackageSelectionDirty = true; - ContentPackageSelectionDirtyNotification = true; - break; + } + ContentPackageSelectionDirtyNotification = true; } } @@ -682,6 +522,11 @@ namespace Barotrauma LocationType.Init(); MapGenerationParams.Init(); LevelGenerationParams.LoadPresets(); + TraitorMissionPrefab.Init(); + Order.Init(); + EventManagerSettings.Init(); + WreckAIConfig.LoadAll(); + SkillSettings.Load(GameMain.Instance.GetFilesOfType(ContentType.SkillSettings)); #if CLIENT GameMain.DecalManager.Prefabs.SortAll(); @@ -780,21 +625,21 @@ namespace Barotrauma LoadPlayerConfig(); - modsFolderWatcher = new FileSystemWatcher("Mods"); + modsFolderWatcher = new System.IO.FileSystemWatcher("Mods"); modsFolderWatcher.Filter = "*"; - modsFolderWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; + modsFolderWatcher.NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName; modsFolderWatcher.Created += OnModFolderUpdate; modsFolderWatcher.Deleted += OnModFolderUpdate; modsFolderWatcher.Renamed += OnModFolderUpdate; modsFolderWatcher.EnableRaisingEvents = true; } - private void OnModFolderUpdate(object sender, FileSystemEventArgs e) + private void OnModFolderUpdate(object sender, System.IO.FileSystemEventArgs e) { if (SuppressModFolderWatcher || (GameMain.NetworkMember?.IsClient ?? false)) { return; } switch (e.ChangeType) { - case WatcherChangeTypes.Created: + case System.IO.WatcherChangeTypes.Created: { string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath(); if (File.Exists(cpPath) && !ContentPackage.List.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath)) @@ -804,7 +649,7 @@ namespace Barotrauma } } break; - case WatcherChangeTypes.Deleted: + case System.IO.WatcherChangeTypes.Deleted: { string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath(); var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList(); @@ -827,9 +672,9 @@ namespace Barotrauma } } break; - case WatcherChangeTypes.Renamed: + case System.IO.WatcherChangeTypes.Renamed: { - RenamedEventArgs renameArgs = e as RenamedEventArgs; + System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs; string cpPath = Path.GetFullPath(Path.Combine(renameArgs.OldFullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath(); var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList(); @@ -980,13 +825,29 @@ namespace Barotrauma doc.Root.Add(keyMappingElement); for (int i = 0; i < keyMapping.Length; i++) { - if (keyMapping[i].MouseButton == MouseButton.None) + KeyOrMouse bind = keyMapping[i]; + if (bind.MouseButton == MouseButton.None) { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].Key)); + keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), bind.Key)); } else { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].MouseButton)); + keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), bind.MouseButton)); + } + } + + var inventoryKeyMappingElement = new XElement("inventorykeymapping"); + doc.Root.Add(inventoryKeyMappingElement); + for (int i = 0; i < inventoryKeyMapping.Length; i++) + { + KeyOrMouse bind = inventoryKeyMapping[i]; + if (bind.MouseButton == MouseButton.None) + { + inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.Key)); + } + else + { + inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton)); } } #endif @@ -1014,7 +875,7 @@ namespace Barotrauma new XAttribute("faceattachmentindex", CharacterFaceAttachmentIndex)); doc.Root.Add(playerElement); - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, OmitXmlDeclaration = true, @@ -1110,7 +971,7 @@ namespace Barotrauma SelectedContentPackages.Clear(); foreach (string path in contentPackagePaths) { - var matchingContentPackage = ContentPackage.List.Find(cp => System.IO.Path.GetFullPath(cp.Path).CleanUpPath() == path.CleanUpPath()); + var matchingContentPackage = ContentPackage.List.Find(cp => Barotrauma.IO.Path.GetFullPath(cp.Path).CleanUpPath() == path.CleanUpPath()); if (matchingContentPackage == null) { @@ -1279,6 +1140,7 @@ namespace Barotrauma new XAttribute("width", GraphicsWidth), new XAttribute("height", GraphicsHeight), new XAttribute("vsync", VSyncEnabled), + new XAttribute("compresstextures", TextureCompressionEnabled), new XAttribute("framelimit", Timing.FrameLimit), new XAttribute("displaymode", windowMode)); } @@ -1300,7 +1162,7 @@ namespace Barotrauma new XAttribute("voipattenuationenabled", VoipAttenuationEnabled), new XAttribute("usedirectionalvoicechat", UseDirectionalVoiceChat), new XAttribute("voicesetting", VoiceSetting), - new XAttribute("voicecapturedevice", VoiceCaptureDevice ?? ""), + new XAttribute("voicecapturedevice", System.Xml.XmlConvert.EncodeName(VoiceCaptureDevice ?? "")), new XAttribute("noisegatethreshold", NoiseGateThreshold)); XElement gSettings = doc.Root.Element("graphicssettings"); @@ -1340,6 +1202,21 @@ namespace Barotrauma keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].MouseButton)); } } + + var inventoryKeyMappingElement = new XElement("inventorykeymapping"); + doc.Root.Add(inventoryKeyMappingElement); + for (int i = 0; i < inventoryKeyMapping.Length; i++) + { + KeyOrMouse bind = inventoryKeyMapping[i]; + if (bind.MouseButton == MouseButton.None) + { + inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.Key)); + } + else + { + inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton)); + } + } #endif var gameplay = new XElement("gameplay"); @@ -1384,7 +1261,7 @@ namespace Barotrauma } doc.Root.Add(tutorialElement); - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, OmitXmlDeclaration = true, @@ -1485,6 +1362,7 @@ namespace Barotrauma GraphicsWidth = graphicsMode.GetAttributeInt("width", GraphicsWidth); GraphicsHeight = graphicsMode.GetAttributeInt("height", GraphicsHeight); VSyncEnabled = graphicsMode.GetAttributeBool("vsync", VSyncEnabled); + TextureCompressionEnabled = graphicsMode.GetAttributeBool("compresstextures", TextureCompressionEnabled); Timing.FrameLimit = graphicsMode.GetAttributeInt("framelimit", 200); XElement graphicsSettings = doc.Root.Element("graphicssettings"); @@ -1526,7 +1404,7 @@ namespace Barotrauma MuteOnFocusLost = audioSettings.GetAttributeBool("muteonfocuslost", MuteOnFocusLost); UseDirectionalVoiceChat = audioSettings.GetAttributeBool("usedirectionalvoicechat", UseDirectionalVoiceChat); - VoiceCaptureDevice = audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice); + VoiceCaptureDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice)); NoiseGateThreshold = audioSettings.GetAttributeFloat("noisegatethreshold", NoiseGateThreshold); MicrophoneVolume = audioSettings.GetAttributeFloat("microphonevolume", MicrophoneVolume); string voiceSettingStr = audioSettings.GetAttributeString("voicesetting", ""); @@ -1568,6 +1446,7 @@ namespace Barotrauma GraphicsWidth = 0; GraphicsHeight = 0; VSyncEnabled = true; + TextureCompressionEnabled = true; Timing.FrameLimit = 200; #if DEBUG EnableSplashScreen = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index e962781f8..ed58a87c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -150,7 +150,24 @@ namespace Barotrauma /// public override bool TryPutItem(Item item, Character user, List allowedSlots = null, bool createNetworkEvent = true) { - if (allowedSlots == null || !allowedSlots.Any()) return false; + if (allowedSlots == null || !allowedSlots.Any()) { return false; } + if (item == null) + { +#if DEBUG + throw new Exception("item null"); +#else + return false; +#endif + } + if (item.Removed) + { +#if DEBUG + throw new Exception("Tried to put a removed item (" + item.Name + ") in an inventory"); +#else + DebugConsole.ThrowError("Tried to put a removed item (" + item.Name + ") in an inventory.\n" + Environment.StackTrace); + return false; +#endif + } bool inSuitableSlot = false; bool inWrongSlot = false; @@ -167,7 +184,7 @@ namespace Barotrauma } } //all good - if (inSuitableSlot && !inWrongSlot) return true; + if (inSuitableSlot && !inWrongSlot) { return true; } //try to place the item in a LimbSlot.Any slot if that's allowed if (allowedSlots.Contains(InvSlotType.Any) && item.AllowedSlots.Contains(InvSlotType.Any)) @@ -184,6 +201,9 @@ namespace Barotrauma int placedInSlot = -1; foreach (InvSlotType allowedSlot in allowedSlots) { + if (allowedSlot.HasFlag(InvSlotType.RightHand) && character.AnimController.GetLimb(LimbType.RightHand) == null) { continue; } + if (allowedSlot.HasFlag(InvSlotType.LeftHand) && character.AnimController.GetLimb(LimbType.LeftHand) == null) { continue; } + //check if all the required slots are free bool free = true; for (int i = 0; i < capacity; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 181429589..7150c763e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -5,7 +5,7 @@ using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -29,6 +29,7 @@ namespace Barotrauma.Items.Components private Door door; private Body[] bodies; + private Fixture outsideBlocker; private Body doorBody; private bool docked; @@ -58,7 +59,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, false, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")] + [Editable, Serialize(false, true, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")] public bool MainDockingPort { get; @@ -113,6 +114,12 @@ namespace Barotrauma.Items.Components { if (DockingTarget != null) { + if (IsHorizontal) + { + DockingDir = 0; + DockingDir = GetDir(DockingTarget); + DockingTarget.DockingDir = -DockingDir; + } if (joint != null) { CreateJoint(joint is WeldJoint); @@ -198,18 +205,6 @@ namespace Barotrauma.Items.Components DockingDir = GetDir(DockingTarget); DockingTarget.DockingDir = -DockingDir; - if (door != null && DockingTarget.door != null) - { - WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => door.LinkedGap == wp.ConnectedGap); - WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.door.LinkedGap == wp.ConnectedGap); - - if (myWayPoint != null && targetWayPoint != null) - { - myWayPoint.linkedTo.Add(targetWayPoint); - targetWayPoint.linkedTo.Add(myWayPoint); - } - } - CreateJoint(false); #if SERVER @@ -259,6 +254,12 @@ namespace Barotrauma.Items.Components { item.CreateServerEvent(this); } +#else + if (GameMain.Client != null && GameMain.Client.MidRoundSyncing && + (item.Submarine == Submarine.MainSub || DockingTarget.item.Submarine == Submarine.MainSub)) + { + Screen.Selected.Cam.Position = Submarine.MainSub.WorldPosition; + } #endif } @@ -270,6 +271,20 @@ namespace Barotrauma.Items.Components { CreateHulls(); } + + if (door != null && DockingTarget.door != null) + { + WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => door.LinkedGap == wp.ConnectedGap); + WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.door.LinkedGap == wp.ConnectedGap); + + if (myWayPoint != null && targetWayPoint != null) + { + myWayPoint.FindHull(); + myWayPoint.linkedTo.Add(targetWayPoint); + targetWayPoint.FindHull(); + targetWayPoint.linkedTo.Add(myWayPoint); + } + } } @@ -461,6 +476,12 @@ namespace Barotrauma.Items.Components } } + if (leftSubRightSide == int.MinValue || rightSubLeftSide == int.MaxValue) + { + DebugConsole.NewMessage("Creating hulls between docking ports failed. Could not find a hull next to the docking port."); + return; + } + //expand left hull to the rightmost hull of the sub at the left side //(unless the difference is more than 100 units - if the distance is very large //there's something wrong with the positioning of the docking ports or submarine hulls) @@ -469,7 +490,8 @@ namespace Barotrauma.Items.Components { if (leftHullDiff > 100) { - DebugConsole.ThrowError("Creating hulls between docking ports failed. The leftmost docking port seems to be very far from any hulls in the left-side submarine."); + DebugConsole.NewMessage("Creating hulls between docking ports failed. The leftmost docking port seems to be very far from any hulls in the left-side submarine."); + return; } else { @@ -483,7 +505,8 @@ namespace Barotrauma.Items.Components { if (rightHullDiff > 100) { - DebugConsole.ThrowError("Creating hulls between docking ports failed. The rightmost docking port seems to be very far from any hulls in the right-side submarine."); + DebugConsole.NewMessage("Creating hulls between docking ports failed. The rightmost docking port seems to be very far from any hulls in the right-side submarine."); + return; } else { @@ -506,6 +529,16 @@ namespace Barotrauma.Items.Components } } + if (rightHullDiff <= 100 && hulls[0].Submarine != null) + { + outsideBlocker = hulls[0].Submarine.PhysicsBody.FarseerBody.CreateRectangle( + ConvertUnits.ToSimUnits(hullRects[0].Width + hullRects[1].Width), + ConvertUnits.ToSimUnits(hullRects[0].Height), + density: 0.0f, + offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Right, hullRects[0].Y - hullRects[0].Height / 2) - hulls[0].Submarine.HiddenSubPosition)); + outsideBlocker.UserData = this; + } + gap = new Gap(new Rectangle(hullRects[0].Right - 2, hullRects[0].Y, 4, hullRects[0].Height), true, subs[0]); } else @@ -540,6 +573,12 @@ namespace Barotrauma.Items.Components } } + if (upperSubBottom == int.MaxValue || lowerSubTop == int.MinValue) + { + DebugConsole.NewMessage("Creating hulls between docking ports failed. Could not find a hull next to the docking port."); + return; + } + //expand lower hull to the topmost hull of the lower sub //(unless the difference is more than 100 units - if the distance is very large //there's something wrong with the positioning of the docking ports or submarine hulls) @@ -548,7 +587,8 @@ namespace Barotrauma.Items.Components { if (lowerHullDiff > 100) { - DebugConsole.ThrowError("Creating hulls between docking ports failed. The lower docking port seems to be very far from any hulls in the lower submarine."); + DebugConsole.NewMessage("Creating hulls between docking ports failed. The lower docking port seems to be very far from any hulls in the lower submarine."); + return; } else { @@ -561,7 +601,8 @@ namespace Barotrauma.Items.Components { if (upperHullDiff > 100) { - DebugConsole.ThrowError("Creating hulls between docking ports failed. The upper docking port seems to be very far from any hulls in the upper submarine."); + DebugConsole.NewMessage("Creating hulls between docking ports failed. The upper docking port seems to be very far from any hulls in the upper submarine."); + return; } else { @@ -575,7 +616,8 @@ namespace Barotrauma.Items.Components int midHullDiff = ((hullRects[1].Y - hullRects[1].Height) - hullRects[0].Y) + 2; if (midHullDiff > 100) { - DebugConsole.ThrowError("Creating hulls between docking ports failed. The upper hull seems to be very far from the lower hull."); + DebugConsole.NewMessage("Creating hulls between docking ports failed. The upper hull seems to be very far from the lower hull."); + return; } else if (midHullDiff > 0) { @@ -584,15 +626,33 @@ namespace Barotrauma.Items.Components hullRects[1].Height += midHullDiff / 2 + 1; } + for (int i = 0; i < 2; i++) { hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition)); hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); + + for (int j = 0; j < 2; j++) + { + bodies[i + j * 2] = GameMain.World.CreateEdge( + ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y)), + ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y - hullRects[i].Height))); + } } - gap = new Gap(new Rectangle(hullRects[0].X, hullRects[0].Y+2, hullRects[0].Width, 4), false, subs[0]); + if (midHullDiff <= 100 && hulls[0].Submarine != null) + { + outsideBlocker = hulls[0].Submarine.PhysicsBody.FarseerBody.CreateRectangle( + ConvertUnits.ToSimUnits(hullRects[0].Width), + ConvertUnits.ToSimUnits(hullRects[0].Height + hullRects[1].Height), + density: 0.0f, + offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Center.X, hullRects[0].Y) - hulls[0].Submarine.HiddenSubPosition)); + outsideBlocker.UserData = this; + } + + gap = new Gap(new Rectangle(hullRects[0].X, hullRects[0].Y + 2, hullRects[0].Width, 4), false, subs[0]); } LinkHullsToGaps(); @@ -609,7 +669,7 @@ namespace Barotrauma.Items.Components foreach (Body body in bodies) { - if (body == null) continue; + if (body == null) { continue; } body.BodyType = BodyType.Static; body.Friction = 0.5f; @@ -720,7 +780,9 @@ namespace Barotrauma.Items.Components if (myWayPoint != null && targetWayPoint != null) { + myWayPoint.FindHull(); myWayPoint.linkedTo.Remove(targetWayPoint); + targetWayPoint.FindHull(); targetWayPoint.linkedTo.Remove(myWayPoint); } } @@ -769,6 +831,9 @@ namespace Barotrauma.Items.Components bodies = null; } + outsideBlocker?.Body.Remove(outsideBlocker); + outsideBlocker = null; + Item.Submarine.EnableObstructedWaypoints(); obstructedWayPointsDisabled = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 98aa09280..0d766b44c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -4,7 +4,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; #if CLIENT @@ -38,12 +38,17 @@ namespace Barotrauma.Items.Components } } + //how much "less stuck" partially doors get when opened + const float StuckReductionOnOpen = 30.0f; + private float resetPredictionTimer; private float toggleCooldownTimer; private Character lastUser; private float damageSoundCooldown; + private double lastBrokenTime; + private Rectangle doorRect; private bool isBroken; @@ -53,7 +58,7 @@ namespace Barotrauma.Items.Components get { return isBroken; } set { - if (isBroken == value) return; + if (isBroken == value) { return; } isBroken = value; if (isBroken) { @@ -63,6 +68,9 @@ namespace Barotrauma.Items.Components { EnableBody(); } +#if SERVER + item.CreateServerEvent(this); +#endif } } @@ -85,7 +93,7 @@ namespace Barotrauma.Items.Components if (isOpen || isBroken || !CanBeWelded) return; stuck = MathHelper.Clamp(value, 0.0f, 100.0f); if (stuck <= 0.0f) { IsStuck = false; } - if (stuck >= 100.0f) { IsStuck = true; } + if (stuck >= 99.0f) { IsStuck = true; } } } @@ -203,10 +211,16 @@ namespace Barotrauma.Items.Components break; } } + + IsActive = true; + } + public override void OnItemLoaded() + { + //do this here because the scale of the item might not be set to the final value yet in the constructor doorRect = new Rectangle( item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale), - item.Rect.Y - item.Rect.Height/2 + (int)(doorSprite.size.Y / 2.0f * item.Scale), + item.Rect.Y - item.Rect.Height / 2 + (int)(doorSprite.size.Y / 2.0f * item.Scale), (int)(doorSprite.size.X * item.Scale), (int)(doorSprite.size.Y * item.Scale)); @@ -224,8 +238,6 @@ namespace Barotrauma.Items.Components Body.SetTransformIgnoreContacts( ConvertUnits.ToSimUnits(new Vector2(doorRect.Center.X, doorRect.Y - doorRect.Height / 2)), 0.0f); - - IsActive = true; } public override void Move(Vector2 amount) @@ -295,6 +307,7 @@ namespace Barotrauma.Items.Components PickingTime = 0; ToggleState(ActionType.OnUse, character); PickingTime = originalPickingTime; + StopPicking(picker); } #if CLIENT else if (hasRequiredItems && character != null && character == Character.Controlled) @@ -313,8 +326,9 @@ namespace Barotrauma.Items.Components if (isBroken) { + lastBrokenTime = Timing.TotalTime; //the door has to be restored to 50% health before collision detection on the body is re-enabled - if (item.ConditionPercentage > 50.0f) + if (item.ConditionPercentage > 50.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { IsBroken = false; } @@ -363,7 +377,10 @@ namespace Barotrauma.Items.Components public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); - IsBroken = true; + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + { + IsBroken = true; + } } private void EnableBody() @@ -502,6 +519,7 @@ namespace Barotrauma.Items.Components foreach (Limb limb in c.AnimController.Limbs) { + if (limb.IsSevered) { continue; } if (PushBodyOutOfDoorway(c, limb.body, dir, simPos, simSize) && damageSoundCooldown <= 0.0f) { #if CLIENT @@ -564,7 +582,12 @@ namespace Barotrauma.Items.Components body.ApplyLinearImpulse(new Vector2(dir * 2.0f, isOpen ? 0.0f : -1.0f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } - c.SetStun(0.2f); + //don't stun if the door was broken a moment ago + //otherwise enabling the door's collider and pushing the character away will interrupt repairing + if (lastBrokenTime < Timing.TotalTime - 1.0f) + { + c.SetStun(0.2f); + } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index e08385827..582282b04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -1,13 +1,13 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Xml.Linq; namespace Barotrauma.Items.Components { - partial class ElectricalDischarger : Powered + partial class ElectricalDischarger : Powered, IServerSerializable { private static readonly List list = new List(); public static IEnumerable List @@ -48,14 +48,14 @@ namespace Barotrauma.Items.Components } } - [Serialize(500.0f, true, description: "How far the discharge can travel from the item."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)] + [Serialize(500.0f, true, description: "How far the discharge can travel from the item.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)] public float Range { get; set; } - [Serialize(25.0f, true, description: "How much further can the discharge be carried when moving across walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(25.0f, true, description: "How much further can the discharge be carried when moving across walls.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float RangeMultiplierInWalls { get; @@ -115,10 +115,15 @@ namespace Barotrauma.Items.Components //already active, do nothing if (IsActive) { return false; } + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } + CurrPowerConsumption = powerConsumption; charging = true; timer = Duration; IsActive = true; +#if SERVER + if (GameMain.Server != null) { item.CreateServerEvent(this); } +#endif return false; } @@ -150,14 +155,12 @@ namespace Barotrauma.Items.Components neededPower -= takePower; battery.Charge -= takePower / 3600.0f; #if SERVER - if (GameMain.Server != null) - { - battery.Item.CreateServerEvent(battery); - } + if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); } #endif } } Discharge(); + } else if (Voltage > MinVoltage) { @@ -478,5 +481,10 @@ namespace Barotrauma.Items.Components base.RemoveComponentSpecific(); list.Remove(this); } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + //no further data needed, the event just triggers the discharge + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 25935b392..df2becc74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -3,6 +3,7 @@ using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Xml.Linq; @@ -309,8 +310,13 @@ namespace Barotrauma.Items.Components { picker = character; - if (character != null) item.Submarine = character.Submarine; + if (item.Removed) + { + DebugConsole.ThrowError($"Attempted to equip a removed item ({item.Name})\n" + Environment.StackTrace); + return; + } + if (character != null) { item.Submarine = character.Submarine; } if (item.body == null) { if (body != null) @@ -325,8 +331,8 @@ namespace Barotrauma.Items.Components if (!item.body.Enabled) { - Limb rightHand = picker.AnimController.GetLimb(LimbType.RightHand); - item.SetTransform(rightHand.SimPosition, 0.0f); + Limb hand = picker.AnimController.GetLimb(LimbType.RightHand) ?? picker.AnimController.GetLimb(LimbType.LeftHand); + item.SetTransform(hand != null ? hand.SimPosition : character.SimPosition, 0.0f); } bool alreadyEquipped = character.HasEquippedItem(item); @@ -363,38 +369,61 @@ namespace Barotrauma.Items.Components IsActive = false; } - public bool CanBeAttached() + public bool CanBeAttached(Character user) { - if (!attachable || !Reattachable) return false; + if (!attachable || !Reattachable) { return false; } //can be attached anywhere in sub editor - if (Screen.Selected == GameMain.SubEditorScreen) return true; + if (Screen.Selected == GameMain.SubEditorScreen) { return true; } + + Vector2 attachPos = user == null ? item.WorldPosition : GetAttachPosition(user, useWorldCoordinates: true); //can be attached anywhere inside hulls - if (item.CurrentHull != null) return true; + if (item.CurrentHull != null && Submarine.RectContains(item.CurrentHull.WorldRect, attachPos)) { return true; } - return Structure.GetAttachTarget(item.WorldPosition) != null; + return Structure.GetAttachTarget(attachPos) != null; } public bool CanBeDeattached() { - if (!attachable || !attached) return true; + if (!attachable || !attached) { return true; } //allow deattaching everywhere in sub editor - if (Screen.Selected == GameMain.SubEditorScreen) return true; + if (Screen.Selected == GameMain.SubEditorScreen) { return true; } - //don't allow deattaching if part of a sub and outside hulls - return item.Submarine == null || item.CurrentHull != null; + if (item.GetComponent() != null) { return true; } + + //if the item has a connection panel and rewiring is disabled, don't allow deattaching + var connectionPanel = item.GetComponent(); + if (connectionPanel != null && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true))) + { + return false; + } + + if (item.CurrentHull == null) + { + return Structure.GetAttachTarget(item.WorldPosition) != null; + } + else + { + return true; + } } public override bool Pick(Character picker) { + if (item.Removed) + { + DebugConsole.ThrowError($"Attempted to pick up a removed item ({item.Name})\n" + Environment.StackTrace); + return false; + } + if (!attachable) { return base.Pick(picker); } - if (!CanBeDeattached()) return false; + if (!CanBeDeattached()) { return false; } if (Attached) { @@ -486,7 +515,7 @@ namespace Barotrauma.Items.Components if (character != null) { if (!character.IsKeyDown(InputType.Aim)) { return false; } - if (!CanBeAttached()) { return false; } + if (!CanBeAttached(character)) { return false; } if (GameMain.NetworkMember != null) { @@ -515,7 +544,7 @@ namespace Barotrauma.Items.Components else { item.Drop(character); - item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f); + item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f, findNewHull: false); } } @@ -524,16 +553,18 @@ namespace Barotrauma.Items.Components return true; } - private Vector2 GetAttachPosition(Character user) + private Vector2 GetAttachPosition(Character user, bool useWorldCoordinates = false) { - if (user == null) { return item.Position; } + if (user == null) { return useWorldCoordinates ? item.WorldPosition : item.Position; } Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition; mouseDiff = mouseDiff.ClampLength(MaxAttachDistance); + Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position; + return new Vector2( - MathUtils.RoundTowardsClosest(user.Position.X + mouseDiff.X, Submarine.GridSize.X), - MathUtils.RoundTowardsClosest(user.Position.Y + mouseDiff.Y, Submarine.GridSize.Y)); + MathUtils.RoundTowardsClosest(userPos.X + mouseDiff.X, Submarine.GridSize.X), + MathUtils.RoundTowardsClosest(userPos.Y + mouseDiff.Y, Submarine.GridSize.Y)); } public override void UpdateBroken(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 76a2db767..8f44ebff5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -95,6 +95,7 @@ namespace Barotrauma.Items.Components { foreach (Limb l in character.AnimController.Limbs) { + if (l.IsSevered) { continue; } if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) { continue; } if (l.type == LimbType.Head || l.type == LimbType.Torso) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index f9a0c0199..0684b67a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components if (PickingTime > 0.0f) { - if (picker.PickingItem == null && PickingTime <= float.MaxValue) + if ((picker.PickingItem == null || picker.PickingItem == item) && PickingTime <= float.MaxValue) { #if SERVER item.CreateServerEvent(this); @@ -114,10 +114,6 @@ namespace Barotrauma.Items.Components { activePicker = picker; picker.PickingItem = item; - - var leftHand = picker.AnimController.GetLimb(LimbType.LeftHand); - var rightHand = picker.AnimController.GetLimb(LimbType.RightHand); - pickTimer = 0.0f; while (pickTimer < requiredTime && Screen.Selected != GameMain.SubEditorScreen) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index e8060e3ba..606dc01c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components foreach (Limb limb in character.AnimController.Limbs) { - if (limb.WearingItems.Find(w => w.WearableComponent.Item == this.item) == null) continue; + if (limb.WearingItems.Find(w => w.WearableComponent.Item == item) == null) { continue; } limb.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index aa299a0a0..b7f3a36b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -90,6 +90,7 @@ namespace Barotrauma.Items.Components return MathHelper.ToRadians(MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure)); } + private readonly List limbBodies = new List(); public override bool Use(float deltaTime, Character character = null) { if (character == null || character.Removed) { return false; } @@ -104,9 +105,10 @@ namespace Barotrauma.Items.Components item.AiTarget.SightRange = item.AiTarget.MaxSightRange; } - List limbBodies = new List(); + limbBodies.Clear(); foreach (Limb l in character.AnimController.Limbs) { + if (l.IsSevered) { continue; } limbBodies.Add(l.body.FarseerBody); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 74cdf27b1..d1e714904 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components private Vector2 debugRayStartPos, debugRayEndPos; + private readonly List ignoredBodies = new List(); + [Serialize("Both", false, description: "Can the item be used in air, water or both.")] public UseEnvironment UsableIn { @@ -68,6 +70,12 @@ namespace Barotrauma.Items.Components [Serialize(false, false, description: "Can the item repair things through holes in walls.")] public bool RepairThroughHoles { get; set; } + [Serialize(true, false, description: "Can the item hit broken doors.")] + public bool HitItems { get; set; } + + [Serialize(false, false, description: "Can the item hit broken doors.")] + public bool HitBrokenDoors { get; set; } + [Serialize(0.0f, false, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")] public float FireProbability { get; set; } @@ -114,8 +122,7 @@ namespace Barotrauma.Items.Components } } item.IsShootable = true; - // TODO: should define this in xml if we have repair tools that don't require aim to use - item.RequireAimToUse = true; + item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true); InitProjSpecific(element); } @@ -124,16 +131,17 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { activeTimer -= deltaTime; - if (activeTimer <= 0.0f) IsActive = false; + if (activeTimer <= 0.0f) { IsActive = false; } } - private List ignoredBodies = new List(); public override bool Use(float deltaTime, Character character = null) { - if (character == null || character.Removed) return false; - if (item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) return false; + if (character != null) + { + if (item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) { return false; } + } - float degreeOfSuccess = DegreeOfSuccess(character); + float degreeOfSuccess = character == null ? 0.5f : DegreeOfSuccess(character); if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) { @@ -187,12 +195,15 @@ namespace Barotrauma.Items.Components (float)Math.Sin(angle)) * Range * item.body.Dir); ignoredBodies.Clear(); - foreach (Limb limb in character.AnimController.Limbs) + if (character != null) { - if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue; - ignoredBodies.Add(limb.body.FarseerBody); + foreach (Limb limb in character.AnimController.Limbs) + { + if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue; + ignoredBodies.Add(limb.body.FarseerBody); + } + ignoredBodies.Add(character.AnimController.Collider.FarseerBody); } - ignoredBodies.Add(character.AnimController.Collider.FarseerBody); IsActive = true; activeTimer = 0.1f; @@ -200,7 +211,8 @@ namespace Barotrauma.Items.Components debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStart); debugRayEndPos = ConvertUnits.ToDisplayUnits(rayEnd); - if (character.Submarine == null) + Submarine parentSub = character?.Submarine ?? item.Submarine; + if (parentSub == null) { foreach (Submarine sub in Submarine.Loaded) { @@ -216,7 +228,7 @@ namespace Barotrauma.Items.Components } else { - Repair(rayStart - character.Submarine.SimPosition, rayEnd - character.Submarine.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies); + Repair(rayStart - parentSub.SimPosition, rayEnd - parentSub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies); } UseProjSpecific(deltaTime, rayStart); @@ -314,6 +326,18 @@ namespace Barotrauma.Items.Components { if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; } if (f.Body?.UserData as string == "ruinroom") { return false; } + if (f.Body?.UserData is Item targetItem) + { + if (!HitItems) { return false; } + if (HitBrokenDoors) + { + if (targetItem.GetComponent() == null && targetItem.Condition <= 0) { return false; } + } + else + { + if (targetItem.Condition <= 0) { return false; } + } + } return f.Body?.UserData != null; }, allowInsideFixture: true)); @@ -440,7 +464,8 @@ namespace Barotrauma.Items.Components } else if (targetBody.UserData is Item targetItem) { - + if (!HitItems) { return false; } + var levelResource = targetItem.GetComponent(); if (levelResource != null && levelResource.Attached && levelResource.requiredItems.Any() && @@ -459,6 +484,15 @@ namespace Barotrauma.Items.Components if (!targetItem.Prefab.DamagedByRepairTools) { return false; } + if (HitBrokenDoors) + { + if (targetItem.GetComponent() == null && targetItem.Condition <= 0) { return false; } + } + else + { + if (targetItem.Condition <= 0) { return false; } + } + targetItem.IsHighlighted = true; ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects); @@ -647,26 +681,26 @@ namespace Barotrauma.Items.Components } #if CLIENT + if (user == null) { return; } // Hard-coded progress bars for welding doors stuck. // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. foreach (ISerializableEntity target in targets) { - if (target is Door door) + if (!(target is Door door)) { continue; } + + if (!door.CanBeWelded) { continue; } + for (int i = 0; i < effect.propertyNames.Length; i++) { - if (!door.CanBeWelded) continue; - for (int i = 0; i < effect.propertyNames.Length; i++) + string propertyName = effect.propertyNames[i]; + if (propertyName != "stuck") { continue; } + if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } + object value = property.GetValue(target); + if (door.Stuck > 0) { - string propertyName = effect.propertyNames[i]; - if (propertyName != "stuck") { continue; } - if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } - object value = property.GetValue(target); - if (door.Stuck > 0) - { - var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White); - if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } - } + var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White); + if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } } - } + } } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 8faa8e1b4..e907e3610 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -421,7 +421,10 @@ namespace Barotrauma.Items.Components case "activate": case "use": case "trigger_in": - item.Use(1.0f, sender); + if (signal != "0") + { + item.Use(1.0f, sender); + } break; case "toggle": if (signal != "0") @@ -734,12 +737,15 @@ namespace Barotrauma.Items.Components public virtual void Load(XElement componentElement, bool usePrefabValues) { - if (componentElement != null && !usePrefabValues) + if (componentElement != null) { foreach (XAttribute attribute in componentElement.Attributes()) { if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) { continue; } - property.TrySetValue(this, attribute.Value); + if (property.OverridePrefabValues || !usePrefabValues) + { + property.TrySetValue(this, attribute.Value); + } } ParseMsg(); OverrideRequiredItems(componentElement); @@ -908,49 +914,56 @@ namespace Barotrauma.Items.Components #region AI related protected const float AIUpdateInterval = 0.2f; protected float aiUpdateTimer; - private int itemIndex; - private List ignoredContainers = new List(); private Character previousUser; protected bool FindSuitableContainer(Character character, Func priority, out Item suitableContainer) { - if (previousUser != character) - { - ignoredContainers.Clear(); - previousUser = character; - } suitableContainer = null; - if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: priority)) + if (character.AIController is HumanAIController aiController) { - suitableContainer = targetContainer; - return true; + if (previousUser != character) + { + previousUser = character; + itemIndex = 0; + } + if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: aiController.IgnoredItems, customPriorityFunction: priority)) + { + suitableContainer = targetContainer; + return true; + } } return false; } protected AIObjectiveContainItem AIContainItems(ItemContainer container, Character character, AIObjective objective, int itemCount, bool equip, bool removeEmpty) where T : ItemComponent { - var containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, objective.objectiveManager) + AIObjectiveContainItem containObjective = null; + if (character.AIController is HumanAIController aiController) { - targetItemCount = itemCount, - Equip = equip, - RemoveEmpty = removeEmpty, - GetItemPriority = i => + containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, objective.objectiveManager) { - if (i.ParentInventory?.Owner is Item) + targetItemCount = itemCount, + Equip = equip, + RemoveEmpty = removeEmpty, + GetItemPriority = i => { - //don't take items from other items of the same type - if (((Item)i.ParentInventory.Owner).GetComponent() != null) + if (i.ParentInventory?.Owner is Item) { - return 0.0f; + //don't take items from other items of the same type + if (((Item)i.ParentInventory.Owner).GetComponent() != null) + { + return 0.0f; + } } + return 1.0f; } - return 1.0f; - } - }; - // TODO: are we sure that we want to abandon the objective here? - containObjective.Abandoned += () => objective.Abandon = true; - objective.AddSubObjective(containObjective); + }; + containObjective.Abandoned += () => + { + aiController.IgnoredItems.Add(container.Item); + }; + objective.AddSubObjective(containObjective); + } return containObjective; } @@ -959,68 +972,71 @@ namespace Barotrauma.Items.Components /// protected bool AIDecontainEmptyItems(Character character, AIObjective objective, bool equip, ItemContainer sourceContainer = null) { - ItemContainer sourceC = sourceContainer ?? (item.OwnInventory?.Owner is Item it ? it.GetComponent() : null); - var containedItems = sourceContainer != null ? sourceContainer.Inventory.Items : item.OwnInventory.Items; - foreach (Item containedItem in containedItems) + if (character.AIController is HumanAIController aiController) { - if (containedItem != null && containedItem.Condition <= 0.0f) + ItemContainer sourceC = sourceContainer ?? (item.OwnInventory?.Owner is Item it ? it.GetComponent() : null); + var containedItems = sourceContainer != null ? sourceContainer.Inventory.Items : item.OwnInventory.Items; + foreach (Item containedItem in containedItems) { - if (FindSuitableContainer(character, - i => - { - var container = i.GetComponent(); - if (container == null) { return 0; } - if (container.Inventory.IsFull()) { return 0; } + if (containedItem != null && containedItem.Condition <= 0.0f) + { + if (FindSuitableContainer(character, + i => + { + var container = i.GetComponent(); + if (container == null) { return 0; } + if (container.Inventory.IsFull()) { return 0; } // Ignore containers that are identical to the source container if (sourceC != null && container.Item.Prefab == sourceC.Item.Prefab) { return 0; } - if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined)) - { - if (isRestrictionsDefined) + if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined)) { - return 4; - } - else - { - if (containedItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary)) + if (isRestrictionsDefined) { - return isPreferencesDefined ? isSecondary ? 2 : 3 : 1; + return 4; } else { - return isPreferencesDefined ? 0 : 1; + if (containedItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary)) + { + return isPreferencesDefined ? isSecondary ? 2 : 3 : 1; + } + else + { + return isPreferencesDefined ? 0 : 1; + } } } - } - else + else + { + return 0; + } + }, out Item targetContainer)) + { + var decontainObjective = new AIObjectiveDecontainItem(character, containedItem, objective.objectiveManager, sourceC, targetContainer?.GetComponent()) { - return 0; - } - }, out Item targetContainer)) - { - var decontainObjective = new AIObjectiveDecontainItem(character, containedItem, objective.objectiveManager, sourceC, targetContainer?.GetComponent()) - { - Equip = equip - }; - decontainObjective.Abandoned += () => - { - itemIndex = 0; - if (targetContainer != null) - { - ignoredContainers.Add(targetContainer); - } - }; - decontainObjective.Completed += () => - { - if (targetContainer == null) + Equip = equip + }; + decontainObjective.Abandoned += () => { itemIndex = 0; - } - }; - objective.AddSubObjectiveInQueue(decontainObjective); - } - else - { - return false; + if (targetContainer != null) + { + aiController.IgnoredItems.Add(targetContainer); + } + }; + decontainObjective.Completed += () => + { + if (targetContainer == null) + { + itemIndex = 0; + } + }; + objective.AddSubObjectiveInQueue(decontainObjective); + } + else + { + return false; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 4bea6fd32..78210b5d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -80,6 +80,13 @@ namespace Barotrauma.Items.Components set { itemRotation = MathHelper.ToRadians(value); } } + [Serialize("", false, description: "Specify an item for the container to spawn with.")] + public string SpawnWithId + { + get; + set; + } + public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined) { isRestrictionsDefined = containableRestrictions.Any(); @@ -143,7 +150,7 @@ namespace Barotrauma.Items.Components } //no need to Update() if this item has no statuseffects and no physics body - IsActive = itemsWithStatusEffects.Count > 0 || containedItem.body != null; + IsActive = itemsWithStatusEffects.Count > 0 || Inventory.Items.Any(it => it?.body != null); } public void OnItemRemoved(Item containedItem) @@ -151,7 +158,7 @@ namespace Barotrauma.Items.Components itemsWithStatusEffects.RemoveAll(i => i.First == containedItem); //deactivate if the inventory is empty - IsActive = itemsWithStatusEffects.Count > 0 || containedItem.body != null; + IsActive = itemsWithStatusEffects.Count > 0 || Inventory.Items.Any(it => it?.body != null); } public bool CanBeContained(Item item) @@ -200,6 +207,22 @@ namespace Barotrauma.Items.Components } } + public override void OnItemLoaded() + { + base.OnItemLoaded(); + if (SpawnWithId.Length > 0) + { + ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId); + if (prefab != null) + { + if (Inventory != null && Inventory.Items.Any(it => it == null)) + { + Entity.Spawner?.AddToSpawnQueue(prefab, Inventory); + } + } + } + } + public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) { return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); @@ -295,7 +318,7 @@ namespace Barotrauma.Items.Components foreach (Item contained in Inventory.Items) { - if (contained == null) continue; + if (contained == null) { continue; } if (contained.body != null) { try @@ -311,6 +334,7 @@ namespace Barotrauma.Items.Components GameAnalyticsSDK.Net.EGAErrorSeverity.Error, "SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace); } + contained.body.Submarine = item.Submarine; } contained.Rect = diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index ab5aa5c90..4a69f5499 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -39,8 +39,6 @@ namespace Barotrauma.Items.Components private Item focusTarget; private float targetRotation; - private bool state; - public Vector2 UserPos { get { return userPos; } @@ -61,6 +59,18 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(false, false, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.")] + public bool State + { + get; + set; + } + + public bool ControlCharacterPose + { + get { return limbPositions.Count > 0; } + } + public Controller(Item item, XElement element) : base(item, element) { @@ -99,7 +109,7 @@ namespace Barotrauma.Items.Components if (IsToggle) { - item.SendSignal(0, state ? "1" : "0", "signal_out", sender: null); + item.SendSignal(0, State ? "1" : "0", "signal_out", sender: null); } if (user == null @@ -272,7 +282,7 @@ namespace Barotrauma.Items.Components return true; } - private Item GetFocusTarget() + public Item GetFocusTarget() { item.SendSignal(0, MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), "position_out", user); @@ -294,7 +304,7 @@ namespace Barotrauma.Items.Components { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - state = !state; + State = !State; #if SERVER item.CreateServerEvent(this); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index ec7020565..64c1233eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -113,7 +113,7 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - if (targetItem.Prefab.DeconstructItems.Any()) + if (targetItem.Prefab.AllowDeconstruct) { //drop all items that are inside the deconstructed item foreach (ItemContainer ic in targetItem.GetComponents()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index df57597b1..171d8ba6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components private float prevVoltage; private float controlLockTimer; - + [Editable(0.0f, 10000000.0f), Serialize(2000.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] public float MaxForce @@ -93,8 +93,8 @@ namespace Barotrauma.Items.Components controlLockTimer -= deltaTime; currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption; - //pumps consume more power when in a bad condition - currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); + //engines consume more power when in a bad condition + item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); if (powerConsumption == 0.0f) { Voltage = 1.0f; } @@ -119,12 +119,15 @@ namespace Barotrauma.Items.Components float max = 1 + maxChangeSpeed; UpdateAITargets(Math.Clamp(noise, min, max), deltaTime); #if CLIENT - for (int i = 0; i < 5; i++) + particleTimer -= deltaTime; + if (particleTimer <= 0.0f) { + Vector2 particleVel = -currForce.ClampLength(5000.0f) / 5.0f; GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos, - -currForce / 5.0f + new Vector2(Rand.Range(-100.0f, 100.0f), Rand.Range(-50f, 50f)), + particleVel * Rand.Range(0.9f, 1.1f), 0.0f, item.CurrentHull); - } + particleTimer = 1.0f / particlesPerSec; + } #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 746bca34a..6a74bc728 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -171,7 +171,7 @@ namespace Barotrauma.Items.Components outputContainer.Inventory.Locked = true; currPowerConsumption = powerConsumption; - currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); + item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); if (GameMain.NetworkMember?.IsServer ?? true) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs index 49c294f4a..78bee5143 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components private set; } - [Editable, Serialize(400.0f, true, description: "How much oxygen the machine generates when operating at full power.")] + [Editable, Serialize(400.0f, true, description: "How much oxygen the machine generates when operating at full power.", alwaysUseInstanceValues: true)] public float GeneratedAmount { get { return generatedAmount; } @@ -42,7 +42,7 @@ namespace Barotrauma.Items.Components CurrFlow = 0.0f; currPowerConsumption = powerConsumption; //consume more power when in a bad condition - currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); + item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); if (powerConsumption <= 0.0f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index fa2c78107..c183afb02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(80.0f, false, description: "How fast the item pumps water in/out when operating at 100%.")] + [Editable, Serialize(80.0f, false, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)] public float MaxFlow { get { return maxFlow; } @@ -45,6 +45,7 @@ namespace Barotrauma.Items.Components } public bool HasPower => IsActive && Voltage >= MinVoltage; + public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f; public Pump(Item item, XElement element) : base(item, element) @@ -68,7 +69,7 @@ namespace Barotrauma.Items.Components currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f); //pumps consume more power when in a bad condition - currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); + item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); if (!HasPower) { return; } @@ -130,7 +131,7 @@ namespace Barotrauma.Items.Components if (objective.Option.Equals("stoppumping", StringComparison.OrdinalIgnoreCase)) { #if SERVER - if (FlowPercentage > 0.0f) + if (objective.Override || FlowPercentage > 0.0f) { item.CreateServerEvent(this); } @@ -141,7 +142,7 @@ namespace Barotrauma.Items.Components else { #if SERVER - if (!IsActive || FlowPercentage > -100.0f) + if (objective.Override || !IsActive || FlowPercentage > -100.0f) { item.CreateServerEvent(this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index e758ee3e4..725058ca2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, float.MaxValue), Serialize(10000.0f, true, description: "How much power (kW) the reactor generates when operating at full capacity.")] + [Editable(0.0f, float.MaxValue), Serialize(10000.0f, true, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)] public float MaxPowerOutput { get { return maxPowerOutput; } @@ -330,6 +330,8 @@ namespace Barotrauma.Items.Components } item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null); + item.SendSignal(0, ((int)-CurrPowerConsumption).ToString(), "power_value_out", null); + item.SendSignal(0, ((int)load).ToString(), "load_value_out", null); UpdateFailures(deltaTime); #if CLIENT @@ -622,7 +624,8 @@ namespace Barotrauma.Items.Components AutoTemp = false; targetFissionRate = 0.0f; targetTurbineOutput = 0.0f; - break; + unsentChanges = true; + return true; } if (autoTemp != prevAutoTemp || @@ -653,17 +656,23 @@ namespace Barotrauma.Items.Components } break; case "set_fissionrate": - if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) + if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) { - FissionRate = newFissionRate; + targetFissionRate = newFissionRate; unsentChanges = true; +#if CLIENT + FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; +#endif } break; case "set_turbineoutput": - if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) + if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) { - TurbineOutput = newTurbineOutput; + targetTurbineOutput = newTurbineOutput; unsentChanges = true; +#if CLIENT + TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; +#endif } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 2fb664778..ecf9b8a36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -108,10 +108,17 @@ namespace Barotrauma.Items.Components public Vector2 TargetVelocity { - get { return targetVelocity;} - set + get { return targetVelocity; } + set { - if (!MathUtils.IsValid(value)) return; + if (!MathUtils.IsValid(value)) + { + if (!MathUtils.IsValid(targetVelocity)) + { + targetVelocity = Vector2.Zero; + } + return; + } targetVelocity.X = MathHelper.Clamp(value.X, -100.0f, 100.0f); targetVelocity.Y = MathHelper.Clamp(value.Y, -100.0f, 100.0f); } @@ -285,16 +292,13 @@ namespace Barotrauma.Items.Components if (AutoPilot) { UpdateAutoPilot(deltaTime); - targetVelocity = targetVelocity.ClampLength(MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f); + TargetVelocity = TargetVelocity.ClampLength(MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f); } else { if (user != null && user.Info != null && user.SelectedConstruction == item) { - user.Info.IncreaseSkillLevel( - "helm", - SkillSettings.Current.SkillIncreasePerSecondWhenSteering / Math.Max(userSkill, 1.0f) * deltaTime, - user.WorldPosition + Vector2.UnitY * 150.0f); + IncreaseSkillLevel(user, deltaTime); } Vector2 velocityDiff = steeringInput - targetVelocity; @@ -323,6 +327,18 @@ namespace Barotrauma.Items.Components item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", null); } + private void IncreaseSkillLevel(Character user, float deltaTime) + { + if (user?.Info == null) { return; } + + float userSkill = user.GetSkillLevel("helm") / 100.0f; + user.Info.IncreaseSkillLevel( + "helm", + SkillSettings.Current.SkillIncreasePerSecondWhenSteering / Math.Max(userSkill, 1.0f) * deltaTime, + user.WorldPosition + Vector2.UnitY * 150.0f); + + } + private void UpdateAutoPilot(float deltaTime) { if (controlledSub == null) { return; } @@ -443,41 +459,46 @@ namespace Barotrauma.Items.Components //steer away from other subs foreach (Submarine sub in Submarine.Loaded) { - if (sub == controlledSub) continue; - if (controlledSub.DockedTo.Contains(sub)) continue; - - float thisSize = Math.Max(controlledSub.Borders.Width, controlledSub.Borders.Height); - float otherSize = Math.Max(sub.Borders.Width, sub.Borders.Height); - + if (sub == controlledSub) { continue; } + if (controlledSub.DockedTo.Contains(sub)) { continue; } + Point sizeSum = controlledSub.Borders.Size + sub.Borders.Size; + Vector2 minDist = sizeSum.ToVector2() / 2; Vector2 diff = controlledSub.WorldPosition - sub.WorldPosition; - float dist = diff == Vector2.Zero ? 0.0f : diff.Length(); - - //far enough -> ignore - if (dist > thisSize + otherSize) continue; - - Vector2 dir = dist <= 0.0001f ? Vector2.UnitY : diff / dist; - float dot = controlledSub.Velocity == Vector2.Zero ? - 0.0f : Vector2.Dot(Vector2.Normalize(controlledSub.Velocity), -dir); - - //heading away -> ignore - if (dot < 0.0f) continue; - - targetVelocity += diff * 200.0f; + float xDist = Math.Abs(diff.X); + float yDist = Math.Abs(diff.Y); + Vector2 maxAvoidDistance = minDist * 2; + if (xDist > maxAvoidDistance.X || yDist > maxAvoidDistance.Y) + { + //far enough -> ignore + continue; + } + float dot = controlledSub.Velocity == Vector2.Zero ? 0.0f : Vector2.Dot(Vector2.Normalize(controlledSub.Velocity), -diff); + if (dot < 0.0f) + { + //heading away -> ignore + continue; + } + float distanceFactor = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(maxAvoidDistance.X + maxAvoidDistance.Y, minDist.X + minDist.Y, xDist + yDist)); + float velocityFactor = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 3, controlledSub.Velocity.Length())); + TargetVelocity += 100 * Vector2.Normalize(diff) * distanceFactor * velocityFactor; } - //clamp velocity magnitude to 100.0f - float velMagnitude = targetVelocity.Length(); + //clamp velocity magnitude to 100.0f (Is this required? The X and Y components are clamped in the property setter) + float velMagnitude = TargetVelocity.Length(); if (velMagnitude > 100.0f) { - targetVelocity *= 100.0f / velMagnitude; + TargetVelocity *= 100.0f / velMagnitude; } } private void UpdatePath() { if (Level.Loaded == null) { return; } - - if (pathFinder == null) pathFinder = new PathFinder(WayPoint.WayPointList, false); + + if (pathFinder == null) + { + pathFinder = new PathFinder(WayPoint.WayPointList, false); + } Vector2 target; if (LevelEndSelected) @@ -553,6 +574,7 @@ namespace Barotrauma.Items.Components unsentChanges = true; AutoPilot = true; } + IncreaseSkillLevel(user, deltaTime); switch (objective.Option.ToLowerInvariant()) { case "maintainposition": diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index e136155a2..a01f75343 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -316,6 +316,8 @@ namespace Barotrauma.Items.Components { if (recipient.Item == item || recipient.Item == source) { continue; } + source?.LastSentSignalRecipients.Add(recipient.Item); + foreach (ItemComponent ic in recipient.Item.Components) { //other junction boxes don't need to receive the signal in the pass-through signal connections diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index dce150ab1..d4afdb642 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -241,7 +241,6 @@ namespace Barotrauma.Items.Components private void Launch(Vector2 impulse) { hits.Clear(); - MaxTargetsToHit = 2; if (item.AiTarget != null) { @@ -487,6 +486,10 @@ namespace Barotrauma.Items.Components return true; } } + else if (target.Body.UserData is Item item) + { + if (item.Condition <= 0.0f) { return false; } + } //ignore character colliders (the projectile only hits limbs) if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character) @@ -634,6 +637,7 @@ namespace Barotrauma.Items.Components item.body.LinearVelocity *= 0.1f; } else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit && + target.Body.Mass > item.body.Mass * 0.5f && (DoesStick || (StickToCharacters && target.Body.UserData is Limb) || (StickToStructures && target.Body.UserData is Structure) || diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 1b8e07500..5cc9f2d32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -55,8 +55,8 @@ namespace Barotrauma.Items.Components set; } - [Serialize(80.0f, true, description: "The condition of the item has to be below this for AI characters to repair it. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] - public float AIRepairThreshold + [Serialize(80.0f, true, description: "The condition of the item has to be below this for it to become repairable. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + public float RepairThreshold { get; set; @@ -112,13 +112,14 @@ namespace Barotrauma.Items.Components element.GetAttributeString("name", ""); //backwards compatibility - var showRepairUIAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("showrepairuithreshold", StringComparison.OrdinalIgnoreCase)); - if (showRepairUIAttribute != null) + var repairThresholdAttribute = + element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("showrepairuithreshold", StringComparison.OrdinalIgnoreCase)) ?? + element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("airepairth44reshold", StringComparison.OrdinalIgnoreCase)); + if (repairThresholdAttribute != null) { - float repairThreshold; - if (Single.TryParse(showRepairUIAttribute.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out repairThreshold)) + if (float.TryParse(repairThresholdAttribute.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out float repairThreshold)) { - AIRepairThreshold = repairThreshold; + RepairThreshold = repairThreshold; } } @@ -273,7 +274,7 @@ namespace Barotrauma.Items.Components float successFactor = requiredSkills.Count == 0 ? 1.0f : DegreeOfSuccess(CurrentFixer, requiredSkills); //item must have been below the repair threshold for the player to get an achievement or XP for repairing it - if (!item.IsFullCondition) + if (item.ConditionPercentage < RepairThreshold) { wasBroken = true; } @@ -309,11 +310,10 @@ namespace Barotrauma.Items.Components SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f), CurrentFixer.WorldPosition + Vector2.UnitY * 100.0f); } - SteamAchievementManager.OnItemRepaired(item, CurrentFixer); - deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); - wasBroken = false; } + deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); + wasBroken = false; StopRepairing(CurrentFixer); } } @@ -358,6 +358,14 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime); + public void AdjustPowerConsumption(ref float powerConsumption) + { + if (item.ConditionPercentage < RepairThreshold) + { + powerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); + } + } + private bool ShouldDeteriorate() { if (LastActiveTime > Timing.TotalTime) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs index 550fce815..4cfe3b515 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.")] + [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { get { return timeFrame; } @@ -23,14 +23,14 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("1", true, description: "The signal sent when both inputs have received a non-zero signal.")] + [InGameEditable, Serialize("1", true, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)] public string Output { get { return output; } set { output = value; } } - [InGameEditable, Serialize("", true, description: "The signal sent when both inputs have not received a non-zero signal (if empty, no signal is sent).")] + [InGameEditable, Serialize("", true, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs index f0f042a40..b1c4a63b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [Serialize(999999.0f, true, description: "The output of the item is restricted below this value."), + [Serialize(999999.0f, true, description: "The output of the item is restricted below this value.", alwaysUseInstanceValues: true), InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMax { @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(-999999.0f, true, description: "The output of the item is restricted above this value."), + [Serialize(-999999.0f, true, description: "The output of the item is restricted above this value.", alwaysUseInstanceValues: true), InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMin { @@ -32,8 +32,8 @@ namespace Barotrauma.Items.Components } [InGameEditable(DecimalCount = 2), - Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the sum of the signals." + - " If set to 0, the inputs must be received at the same time.")] + Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the result." + + " If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { get { return timeFrame; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 6d48bb623..5881d2a13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components private List disconnectedWireIds; - [Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.")] + [Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)] public bool Locked { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs index 1078353f8..63842df7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components private DelayedSignal prevQueuedSignal; private float delay; - [InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, true, description: "How long the item delays the signals (in seconds).")] + [InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, true, description: "How long the item delays the signals (in seconds).", alwaysUseInstanceValues: true)] public float Delay { get { return delay; } @@ -43,14 +43,14 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when a new one is received.")] + [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when a new one is received.", alwaysUseInstanceValues: true)] public bool ResetWhenSignalReceived { get; set; } - [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when the incoming signal changes.")] + [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when the incoming signal changes.", alwaysUseInstanceValues: true)] public bool ResetWhenDifferentSignalReceived { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs index 467418095..6ba37f496 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs @@ -15,21 +15,21 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signals are equal.")] + [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the condition is met.", alwaysUseInstanceValues: true)] public string Output { get { return output; } set { output = value; } } - [InGameEditable, Serialize("", true, description: "The signal this item outputs when the received signals are not equal.")] + [InGameEditable, Serialize("", true, description: "The signal this item outputs when the condition is not met.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } set { falseOutput = value; } } - [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.")] + [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { get { return timeFrame; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs index 59b81abce..4d8fba217 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components class ExponentiationComponent : ItemComponent { private float exponent; - [InGameEditable, Serialize(1.0f, false, description: "The exponent of the operation.")] + [InGameEditable, Serialize(1.0f, false, description: "The exponent of the operation.", alwaysUseInstanceValues: true)] public float Exponent { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs index a34a96c7b..2c529781e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Items.Components SquareRoot } - [Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.")] + [Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function { get; set; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 9aba89bd0..4f92e27b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components public PhysicsBody ParentBody; - [Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive."), + [Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] public float Range { @@ -43,7 +43,7 @@ namespace Barotrauma.Items.Components public float Rotation; [Editable, Serialize(true, true, description: "Should structures cast shadows when light from this light source hits them. " + - "Disabling shadows increases the performance of the game, and is recommended for lights with a short range.")] + "Disabling shadows increases the performance of the game, and is recommended for lights with a short range.", alwaysUseInstanceValues: true)] public bool CastShadows { get { return castShadows; } @@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components } [Editable, Serialize(false, true, description: "Lights drawn behind submarines don't cast any shadows and are much faster to draw than shadow-casting lights. " + - "It's recommended to enable this on decorative lights outside the submarine's hull.")] + "It's recommended to enable this on decorative lights outside the submarine's hull.", alwaysUseInstanceValues: true)] public bool DrawBehindSubs { get { return drawBehindSubs; } @@ -70,7 +70,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "Is the light currently on.")] + [Editable, Serialize(false, true, description: "Is the light currently on.", alwaysUseInstanceValues: true)] public bool IsOn { get { return IsActive; } @@ -110,7 +110,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("255,255,255,255", true, description: "The color of the emitted light (R,G,B,A).")] + [InGameEditable, Serialize("255,255,255,255", true, description: "The color of the emitted light (R,G,B,A).", alwaysUseInstanceValues: true)] public Color LightColor { get { return lightColor; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs index ebfd0b2c4..0fd1c5a51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class MemoryComponent : ItemComponent { - [InGameEditable, Serialize("", true, description: "The currently stored signal the item outputs.")] + [InGameEditable, Serialize("", true, description: "The currently stored signal the item outputs.", alwaysUseInstanceValues: true)] public string Value { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs index d4e926de0..a66276988 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components class ModuloComponent : ItemComponent { private float modulus; - [InGameEditable, Serialize(1.0f, false, description: "The modulus of the operation. Must be non-zero.")] + [InGameEditable, Serialize(1.0f, false, description: "The modulus of the operation. Must be non-zero.", alwaysUseInstanceValues: true)] public float Modulus { get { return modulus; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 9126c5884..9f10c0973 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -18,14 +18,14 @@ namespace Barotrauma.Items.Components [Serialize(false, false, 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; } - [Editable, Serialize(false, true, description: "Should the sensor only detect the movement of humans?")] + [Editable, Serialize(false, true, description: "Should the sensor only detect the movement of humans?", alwaysUseInstanceValues: true)] public bool OnlyHumans { get; set; } - [Editable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?")] + [Editable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)] public bool IgnoreDead { get; @@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components } - [InGameEditable, Serialize(0.0f, true, description: "Horizontal detection range.")] + [InGameEditable, Serialize(0.0f, true, description: "Horizontal detection range.", alwaysUseInstanceValues: true)] public float RangeX { get { return rangeX; } @@ -45,7 +45,7 @@ namespace Barotrauma.Items.Components #endif } } - [InGameEditable, Serialize(0.0f, true, description: "Vertical movement detection range.")] + [InGameEditable, Serialize(0.0f, true, description: "Vertical movement detection range.", alwaysUseInstanceValues: true)] public float RangeY { get { return rangeY; } @@ -67,13 +67,13 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.")] + [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)] public string Output { get; set; } - [InGameEditable, Serialize("", true, description: "The signal the item outputs when it has not detected movement.")] + [InGameEditable, Serialize("", true, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } - [Editable(DecimalCount = 3), Serialize(0.01f, true, description: "How fast the objects within the detector's range have to be moving (in m/s).")] + [Editable(DecimalCount = 3), Serialize(0.01f, true, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] public float MinimumVelocity { get; @@ -132,7 +132,8 @@ namespace Barotrauma.Items.Components foreach (Limb limb in c.AnimController.Limbs) { - if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) continue; + if (limb.IsSevered) { continue; } + if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) { continue; } if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) { MotionDetected = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs index 3b2b7ece7..caacada54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs @@ -23,14 +23,14 @@ namespace Barotrauma.Items.Components [InGameEditable, Serialize(WaveType.Pulse, true, description: "What kind of a signal the item outputs." + " Pulse: periodically sends out a signal of 1." + " Sine: sends out a sine wave oscillating between -1 and 1." + - " Square: sends out a signal that alternates between 0 and 1.")] + " Square: sends out a signal that alternates between 0 and 1.", alwaysUseInstanceValues: true)] public WaveType OutputType { get; set; } - [InGameEditable(DecimalCount = 2), Serialize(1.0f, true, description: "How fast the signal oscillates, or how fast the pulses are sent (in Hz).")] + [InGameEditable(DecimalCount = 2), Serialize(1.0f, true, description: "How fast the signal oscillates, or how fast the pulses are sent (in Hz).", alwaysUseInstanceValues: true)] public float Frequency { get { return frequency; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index e40ca77ed..a541bc98f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -16,16 +16,16 @@ namespace Barotrauma.Items.Components private bool nonContinuousOutputSent; - [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.")] + [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.", alwaysUseInstanceValues: true)] public string Output { get; set; } - [Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.")] + [Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } - [InGameEditable, Serialize(true, true, description: "Should the component keep sending the output even after it stops receiving a signal, or only send an output when it receives a signal.")] + [InGameEditable, Serialize(true, true, description: "Should the component keep sending the output even after it stops receiving a signal, or only send an output when it receives a signal.", alwaysUseInstanceValues: true)] public bool ContinuousOutput { get; set; } - [InGameEditable, Serialize("", true, description: "The regular expression used to check the incoming signals.")] + [InGameEditable, Serialize("", true, description: "The regular expression used to check the incoming signals.", alwaysUseInstanceValues: true)] public string Expression { get { return expression; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 8061c0789..7f5019107 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -37,7 +37,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "Can the relay currently pass power and signals through it.")] + [Editable, Serialize(false, true, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] public bool IsOn { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs index 13e2acc3c..eb725383d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs @@ -4,12 +4,12 @@ namespace Barotrauma.Items.Components { class SignalCheckComponent : ItemComponent { - [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the target signal.")] + [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the target signal.", alwaysUseInstanceValues: true)] public string Output { get; set; } - [InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the target signal.")] + [InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the target signal.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } - [InGameEditable, Serialize("", true, description: "The value to compare the received signals against.")] + [InGameEditable, Serialize("", true, description: "The value to compare the received signals against.", alwaysUseInstanceValues: true)] public string TargetSignal { get; set; } public SignalCheckComponent(Item item, XElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs index feed5a033..7a85ef85f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs @@ -1,25 +1,51 @@ -using System.Linq; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Items.Components { class SmokeDetector : ItemComponent { - [Serialize(50.0f, false, description: "How large the fire has to be for the detector to react to it.")] - public float FireSizeThreshold - { - get; set; - } + const float FireCheckInterval = 1.0f; + private float fireCheckTimer; + + private bool fireInRange; + + [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)] + public string Output { get; set; } + + [InGameEditable, Serialize("0", true, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] + public string FalseOutput { get; set; } public SmokeDetector(Item item, XElement element) - : base (item, element) + : base(item, element) { IsActive = true; } + private bool IsFireInRange() + { + if (item.CurrentHull == null || item.InWater) { return false; } + + var connectedHulls = item.CurrentHull.GetConnectedHulls(includingThis: true, searchDepth: 10, ignoreClosedGaps: true); + foreach (Hull hull in connectedHulls) + { + foreach (FireSource fireSource in hull.FireSources) + { + if (fireSource.IsInDamageRange(item.WorldPosition, fireSource.DamageRange * 2.0f)) { return true; } + } + } + + return false; + } + public override void Update(float deltaTime, Camera cam) { - item.SendSignal(0, item.CurrentHull != null && item.CurrentHull.FireSources.Any(fs => fs.Size.X > FireSizeThreshold) ? "1" : "0", "signal_out", null); + fireCheckTimer -= deltaTime; + if (fireCheckTimer <= 0.0f) + { + fireInRange = IsFireInRange(); + fireCheckTimer = FireCheckInterval; + } + item.SendSignal(0, fireInRange ? "1" : "0", "signal_out", null); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index dcad68832..dad4b40fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components } private string welcomeMessage; - [InGameEditable, Serialize("", true, "Message to be displayed on the terminal display when it is first opened.", translationTextTag = "terminalwelcomemsg.")] + [InGameEditable, Serialize("", true, "Message to be displayed on the terminal display when it is first opened.", translationTextTag = "terminalwelcomemsg.", AlwaysUseInstanceValues = true)] public string WelcomeMessage { get { return welcomeMessage; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index 14d183268..640a7f624 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -19,14 +19,14 @@ namespace Barotrauma.Items.Components protected float[] receivedSignal = new float[2]; - [Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.")] + [Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function { get; set; } - [InGameEditable, Serialize(false, true, description: "If set to true, the trigonometric function uses radians instead of degrees.")] + [InGameEditable, Serialize(false, true, description: "If set to true, the trigonometric function uses radians instead of degrees.", alwaysUseInstanceValues: true)] public bool UseRadians { get; set; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index d5ad89235..830441855 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -10,10 +10,10 @@ namespace Barotrauma.Items.Components private bool isInWater; private float stateSwitchDelay; - [InGameEditable, Serialize("1", true, description: "The signal the item sends out when it's underwater.")] + [InGameEditable, Serialize("1", true, description: "The signal the item sends out when it's underwater.", alwaysUseInstanceValues: true)] public string Output { get; set; } - [InGameEditable, Serialize("0", true, description: "The signal the item sends out when it's not underwater.")] + [InGameEditable, Serialize("0", true, description: "The signal the item sends out when it's not underwater.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } public WaterDetector(Item item, XElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index e5f76281f..c3dcced4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components { partial class WifiComponent : ItemComponent { - private static List list = new List(); + private static readonly List list = new List(); private float range; @@ -19,10 +19,10 @@ namespace Barotrauma.Items.Components private string prevSignal; - [Serialize(Character.TeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.")] + [Serialize(Character.TeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)] public Character.TeamType TeamID { get; set; } - [Editable, Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.")] + [Editable, Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.", alwaysUseInstanceValues: true)] public float Range { get { return range; } @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize(1, true, description: "WiFi components can only communicate with components that use the same channel.")] + [InGameEditable, Serialize(1, true, description: "WiFi components can only communicate with components that use the same channel.", alwaysUseInstanceValues: true)] public int Channel { get { return channel; } @@ -46,7 +46,7 @@ namespace Barotrauma.Items.Components } - [Serialize(false, false, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.")] + [Editable, Serialize(false, true, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.", alwaysUseInstanceValues: true)] public bool AllowCrossTeamCommunication { get; @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components } [Editable, Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " + - "as chat messages in the chatbox of the player holding the item.")] + "as chat messages in the chatbox of the player holding the item.", alwaysUseInstanceValues: true)] public bool LinkToChat { get; @@ -120,7 +120,7 @@ namespace Barotrauma.Items.Components public void TransmitSignal(int stepsTaken, string signal, Item source, Character sender, bool sendToChat, float signalStrength = 1.0f) { var senderComponent = source?.GetComponent(); - if (senderComponent != null && !CanReceive(senderComponent)) return; + if (senderComponent != null && !CanReceive(senderComponent)) { return; } bool chatMsgSent = false; @@ -148,33 +148,32 @@ namespace Barotrauma.Items.Components if (LinkToChat && wifiComp.LinkToChat && chatMsgCooldown <= 0.0f && sendToChat) { if (wifiComp.item.ParentInventory != null && - wifiComp.item.ParentInventory.Owner != null && - GameMain.NetworkMember != null) + wifiComp.item.ParentInventory.Owner != null) { string chatMsg = signal; if (senderComponent != null) { chatMsg = ChatMessage.ApplyDistanceEffect(chatMsg, 1.0f - sentSignalStrength); } - if (chatMsg.Length > ChatMessage.MaxLength) chatMsg = chatMsg.Substring(0, ChatMessage.MaxLength); - if (string.IsNullOrEmpty(chatMsg)) continue; + if (chatMsg.Length > ChatMessage.MaxLength) { chatMsg = chatMsg.Substring(0, ChatMessage.MaxLength); } + if (string.IsNullOrEmpty(chatMsg)) { continue; } #if CLIENT if (wifiComp.item.ParentInventory.Owner == Character.Controlled) { if (GameMain.Client == null) - GameMain.NetworkMember.AddChatMessage(signal, ChatMessageType.Radio, source == null ? "" : source.Name); + { + GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(source?.Name ?? "", signal, ChatMessageType.Radio, sender: null); + } } -#endif - -#if SERVER +#elif SERVER if (GameMain.Server != null) { Client recipientClient = GameMain.Server.ConnectedClients.Find(c => c.Character == wifiComp.item.ParentInventory.Owner); if (recipientClient != null) { GameMain.Server.SendDirectChatMessage( - ChatMessage.Create(source == null ? "" : source.Name, chatMsg, ChatMessageType.Radio, null), recipientClient); + ChatMessage.Create(source?.Name ?? "", chatMsg, ChatMessageType.Radio, null), recipientClient); } } #endif @@ -193,8 +192,20 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { - if (connection == null || connection.Name != "signal_in") return; - TransmitSignal(stepsTaken, signal, source, sender, true, signalStrength); + if (connection == null) { return; } + + switch (connection.Name) + { + case "signal_in": + TransmitSignal(stepsTaken, signal, source, sender, true, signalStrength); + break; + case "set_channel": + if (int.TryParse(signal, out int newChannel)) + { + Channel = newChannel; + } + break; + } } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index d41d80358..601c02216 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -40,6 +40,8 @@ namespace Barotrauma.Items.Components } } + private bool shouldClearConnections = true; + const float MaxAttachDistance = 150.0f; const float MinNodeDistance = 7.0f; @@ -180,8 +182,6 @@ namespace Barotrauma.Items.Components newConnection.Item.Position : newConnection.Item.Position - refSub.HiddenSubPosition; - nodePos = RoundNode(nodePos); - if (nodes.Count > 0 && nodes[0] == nodePos) { break; } if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { break; } @@ -246,7 +246,7 @@ namespace Barotrauma.Items.Components public override void Equip(Character character) { - ClearConnections(character); + if (shouldClearConnections) { ClearConnections(character); } IsActive = true; } @@ -258,7 +258,7 @@ namespace Barotrauma.Items.Components public override void Drop(Character dropper) { - ClearConnections(dropper); + if (shouldClearConnections) { ClearConnections(dropper); } IsActive = false; } @@ -340,7 +340,7 @@ namespace Barotrauma.Items.Components else { #if CLIENT - bool disableGrid = SubEditorScreen.IsSubEditor() && (PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)); + bool disableGrid = SubEditorScreen.IsSubEditor() && PlayerInput.IsShiftDown(); newNodePos = disableGrid ? item.Position : RoundNode(item.Position); #else newNodePos = RoundNode(item.Position); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 49361bd90..ee6c8a0d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; @@ -40,6 +40,8 @@ namespace Barotrauma.Items.Components private Character user; + private float resetUserTimer; + public float Rotation { get { return rotation; } @@ -102,7 +104,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(1, false, description: "How projectiles the weapon launches when fired once.")] + [Serialize(1, false, description: "How many projectiles the weapon launches when fired once.")] public int ProjectileCount { get; @@ -116,7 +118,8 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize("0.0,0.0", true, description: "The range at which the barrel can rotate. TODO")] + [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), + Serialize("0.0,0.0", true, description: "The range at which the barrel can rotate.", alwaysUseInstanceValues: true)] public Vector2 RotationLimits { get @@ -196,7 +199,7 @@ namespace Barotrauma.Items.Components } private float baseRotationRad; - [Editable(0.0f, 360.0f), Serialize(0.0f, true, description: "The angle of the turret's base in degrees.")] + [Editable(0.0f, 360.0f), Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)] public float BaseRotation { get { return MathHelper.ToDegrees(baseRotationRad); } @@ -285,6 +288,11 @@ namespace Barotrauma.Items.Components { user = null; } + else + { + resetUserTimer -= deltaTime; + if (resetUserTimer <= 0.0f) { user = null; } + } ApplyStatusEffects(ActionType.OnActive, deltaTime, null); @@ -713,6 +721,7 @@ namespace Barotrauma.Items.Components PowerContainer batteryToLoad = null; foreach (PowerContainer battery in batteries) { + if (battery.Item.NonInteractable) { continue; } if (batteryToLoad == null || battery.Charge < lowestCharge) { batteryToLoad = battery; @@ -733,20 +742,22 @@ namespace Barotrauma.Items.Components int maxProjectileCount = 0; foreach (MapEntity e in item.linkedTo) { - if (!(e is Item projectileContainer)) continue; - - var containedItems = projectileContainer.ContainedItems; - if (containedItems != null) + if (item.NonInteractable) { continue; } + if (e is Item projectileContainer) { - var container = projectileContainer.GetComponent(); - maxProjectileCount += container.Capacity; + var containedItems = projectileContainer.ContainedItems; + if (containedItems != null) + { + var container = projectileContainer.GetComponent(); + maxProjectileCount += container.Capacity; - int projectiles = containedItems.Count(it => it.Condition > 0.0f); - usableProjectileCount += projectiles; + int projectiles = containedItems.Count(it => it.Condition > 0.0f); + usableProjectileCount += projectiles; + } } } - if (usableProjectileCount == 0 || (usableProjectileCount < maxProjectileCount && objective.Option.Equals("fireatwill", StringComparison.OrdinalIgnoreCase))) + if (usableProjectileCount == 0) { ItemContainer container = null; Item containerItem = null; @@ -754,11 +765,16 @@ namespace Barotrauma.Items.Components { containerItem = e as Item; if (containerItem == null) { continue; } + if (containerItem.NonInteractable) { continue; } + if (character.AIController is HumanAIController aiController && aiController.IgnoredItems.Contains(containerItem)) { continue; } container = containerItem.GetComponent(); if (container != null) { break; } } - if (container == null || container.ContainableItems.Count == 0) { return true; } - + if (container == null || container.ContainableItems.Count == 0) + { + character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f); + return true; + } if (objective.SubObjectives.None()) { if (!AIDecontainEmptyItems(character, objective, equip: true, sourceContainer: container)) @@ -769,10 +785,25 @@ namespace Barotrauma.Items.Components if (objective.SubObjectives.None()) { var loadItemsObjective = AIContainItems(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true); - loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; - character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "loadturret", 30.0f); + if (loadItemsObjective == null) + { + if (usableProjectileCount == 0) + { + character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f); + return true; + } + } + else + { + loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; + character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "loadturret", 30.0f); + return false; + } + } + if (objective.SubObjectives.Any()) + { + return false; } - return false; } //enough shells and power @@ -803,7 +834,10 @@ namespace Barotrauma.Items.Components character.AIController.SelectTarget(closestEnemy.AiTarget); character.CursorPosition = closestEnemy.WorldPosition; - if (item.Submarine != null) { character.CursorPosition -= item.Submarine.Position; } + if (character.Submarine != null) + { + character.CursorPosition -= character.Submarine.Position; + } float enemyAngle = MathUtils.VectorToAngle(closestEnemy.WorldPosition - item.WorldPosition); float turretAngle = -rotation; @@ -857,11 +891,8 @@ namespace Barotrauma.Items.Components return false; } } - if (objective.Option.Equals("fireatwill", StringComparison.OrdinalIgnoreCase)) - { - character?.Speak(TextManager.GetWithVariable("DialogFireTurret", "[itemname]", item.Name, true), null, 0.0f, "fireturret", 5.0f); - character.SetInput(InputType.Shoot, true, true); - } + character?.Speak(TextManager.GetWithVariable("DialogFireTurret", "[itemname]", item.Name, true), null, 0.0f, "fireturret", 5.0f); + character.SetInput(InputType.Shoot, true, true); return false; } @@ -979,10 +1010,12 @@ namespace Barotrauma.Items.Components IsActive = true; } user = sender; + resetUserTimer = 10.0f; break; case "trigger_in": item.Use((float)Timing.Step, sender); user = sender; + resetUserTimer = 10.0f; //triggering the Use method through item.Use will fail if the item is not characterusable and the signal was sent by a character //so lets do it manually if (!characterUsable && sender != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index c95adedb1..fd717b1c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; @@ -221,11 +221,11 @@ namespace Barotrauma.Items.Components get { return variant; } set { -#if SERVER + if (variant == value) { return; } +#if SERVER variant = value; item.CreateServerEvent(this); #elif CLIENT - if (variant == value) { return; } Character character = picker; if (character != null) @@ -370,11 +370,12 @@ namespace Barotrauma.Items.Components public override void Unequip(Character character) { - if (picker == null) return; + if (character == null || character.Removed) { return; } + if (picker == null) { return; } for (int i = 0; i < wearableSprites.Length; i++) { Limb equipLimb = character.AnimController.GetLimb(limbType[i]); - if (equipLimb == null) continue; + if (equipLimb == null) { continue; } if (wearableSprites[i].LightComponent != null) { @@ -385,7 +386,6 @@ namespace Barotrauma.Items.Components #if CLIENT equipLimb.UpdateWearableTypesToHide(); #endif - limb[i] = null; } @@ -419,9 +419,14 @@ namespace Barotrauma.Items.Components { base.RemoveComponentSpecific(); + Unequip(picker); + foreach (WearableSprite wearableSprite in wearableSprites) { - if (wearableSprite != null && wearableSprite.Sprite != null) wearableSprite.Sprite.Remove(); + if (wearableSprite != null && wearableSprite.Sprite != null) + { + wearableSprite.Sprite.Remove(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 790e618f7..ec8ad9a57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -17,17 +17,6 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { - public enum ActionType - { - Always, OnPicked, OnUse, OnSecondaryUse, - OnWearing, OnContaining, OnContained, OnNotContained, - OnActive, OnFailure, OnBroken, - OnFire, InWater, NotInWater, - OnImpact, - OnEating, - OnDeath = OnBroken, - OnDamaged - } partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable { @@ -171,7 +160,7 @@ namespace Barotrauma set { description = value; } } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, true, alwaysUseInstanceValues: true)] public bool NonInteractable { get; @@ -1087,7 +1076,7 @@ namespace Barotrauma public Item GetRootContainer() { - if (Container == null) return null; + if (Container == null) { return null; } Item rootContainer = Container; while (rootContainer.Container != null) @@ -1098,7 +1087,16 @@ namespace Barotrauma return rootContainer; } - public bool IsOwnedBy(Character character) => FindParentInventory(i => i.Owner == character) != null; + public bool IsOwnedBy(Entity entity) => FindParentInventory(i => i.Owner == entity) != null; + + public Entity GetRootInventoryOwner() + { + if (ParentInventory == null) { return this; } + if (ParentInventory.Owner is Character) { return ParentInventory.Owner; } + var rootContainer = GetRootContainer(); + if (rootContainer?.ParentInventory?.Owner is Character) { return rootContainer.ParentInventory.Owner; } + return rootContainer ?? this; + } public Inventory FindParentInventory(Func predicate) { @@ -1457,6 +1455,14 @@ namespace Barotrauma body.SetTransform(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation); } + if (Submarine != prevSub && ContainedItems != null) + { + foreach (Item containedItem in ContainedItems) + { + containedItem.Submarine = Submarine; + } + } + Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition); rect.X = (int)(displayPos.X - rect.Width / 2.0f); rect.Y = (int)(displayPos.Y + rect.Height / 2.0f); @@ -1705,7 +1711,22 @@ namespace Barotrauma } } } - + + public Controller FindController() + { + //try finding the controller with the simpler non-recursive method first + var controllers = GetConnectedComponents(); + if (controllers.None()) { controllers = GetConnectedComponents(recursive: true); } + return controllers.Count < 2 ? controllers.FirstOrDefault() : + (controllers.FirstOrDefault(c => c.GetFocusTarget() == this) ?? controllers.FirstOrDefault()); + } + + public bool TryFindController(out Controller controller) + { + controller = FindController(); + return controller != null; + } + public void SendSignal(int stepsTaken, string signal, string connectionName, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f) { if (connections == null) { return; } @@ -1779,7 +1800,8 @@ namespace Barotrauma #if CLIENT bool hasRequiredSkills = true; Skill requiredSkill = null; -#endif +#endif + if (NonInteractable) { return false; } foreach (ItemComponent ic in components) { bool pickHit = false, selectHit = false; @@ -2089,13 +2111,19 @@ namespace Barotrauma public void Equip(Character character) { - foreach (ItemComponent ic in components) ic.Equip(character); + if (Removed) + { + DebugConsole.ThrowError($"Tried to equip a removed item ({Name}).\n{Environment.StackTrace}"); + return; + } + + foreach (ItemComponent ic in components) { ic.Equip(character); } } public void Unequip(Character character) { character.DeselectItem(this); - foreach (ItemComponent ic in components) ic.Unequip(character); + foreach (ItemComponent ic in components) { ic.Unequip(character); } } public List> GetProperties() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 2b89250fa..337a33fdc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Xml.Linq; using System.Linq; using Barotrauma.Items.Components; @@ -223,6 +223,12 @@ namespace Barotrauma private set; } + public bool AllowDeconstruct + { + get; + private set; + } + //how close the Character has to be to the item to pick it up [Serialize(120.0f, false)] public float InteractDistance @@ -246,6 +252,9 @@ namespace Barotrauma private set; } + [Serialize(false, false, description: "Hides the condition bar displayed at the bottom of the inventory slot the item is in.")] + public bool HideConditionBar { get; set; } + //if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item //if false, trigger areas define areas that can be used to highlight the item [Serialize(true, false)] @@ -300,6 +309,13 @@ namespace Barotrauma private set; } + [Serialize(1f, false)] + public float ExplosionDamageMultiplier + { + get; + private set; + } + [Serialize(false, false)] public bool DamagedByProjectiles { @@ -691,7 +707,8 @@ namespace Barotrauma var brokenSprite = new BrokenItemSprite( new Sprite(subElement, brokenSpriteFolder, lazyLoad: true), subElement.GetAttributeFloat("maxcondition", 0.0f), - subElement.GetAttributeBool("fadein", false)); + subElement.GetAttributeBool("fadein", false), + subElement.GetAttributePoint("offset", Point.Zero)); int spriteIndex = 0; for (int i = 0; i < BrokenSprites.Count && BrokenSprites[i].MaxCondition < brokenSprite.MaxCondition; i++) @@ -741,7 +758,7 @@ namespace Barotrauma #endif case "deconstruct": DeconstructTime = subElement.GetAttributeFloat("time", 1.0f); - + AllowDeconstruct = true; foreach (XElement deconstructItem in subElement.Elements()) { if (deconstructItem.Attribute("name") != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 1ab304ba9..affe3215e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; @@ -146,7 +146,7 @@ namespace Barotrauma { id += 1; IDfound = dictionary.ContainsKey(id); - } while (IDfound); + } while (IDfound || id == NullEntityID || id == EntitySpawnerID); return id; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 34b4f47f9..979420832 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -136,7 +136,7 @@ namespace Barotrauma if (powered == null || !powered.VulnerableToEMP) continue; if (item.Repairables.Any()) { - item.Condition -= 100 * EmpStrength * distFactor; + item.Condition -= item.MaxCondition * EmpStrength * distFactor; } //discharge batteries @@ -157,42 +157,47 @@ namespace Barotrauma if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { - if (flames) + foreach (Item item in Item.ItemList) { - foreach (Item item in Item.ItemList) + if (item.Condition <= 0.0f) { continue; } + if (Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.5f) { continue; } + if (flames && !item.FireProof) { - if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) { continue; } - //don't apply OnFire effects if the item is inside a fireproof container //(or if it's inside a container that's inside a fireproof container, etc) Item container = item.Container; bool fireProof = false; while (container != null) { - if (container.FireProof) { fireProof = true; break; } + if (container.FireProof) + { + fireProof = true; + break; + } container = container.Container; } - - if (fireProof || Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.5f) { continue; } - - item.ApplyStatusEffects(ActionType.OnFire, 1.0f); - if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (!fireProof) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire }); + item.ApplyStatusEffects(ActionType.OnFire, 1.0f); + if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire }); + } } + } - if (item.Prefab.DamagedByExplosions && !item.Indestructible) + if (item.Prefab.DamagedByExplosions && !item.Indestructible) + { + float limbRadius = item.body == null ? 0.0f : item.body.GetMaxExtent(); + float dist = Vector2.Distance(item.WorldPosition, worldPosition); + dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius)); + if (dist > attack.Range) { - float limbRadius = item.body == null ? 0.0f : item.body.GetMaxExtent(); - float dist = Vector2.Distance(item.WorldPosition, worldPosition); - dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius)); - - if (dist > attack.Range) { continue; } - - float distFactor = 1.0f - dist / attack.Range; - float damageAmount = attack.GetItemDamage(1.0f); - item.Condition -= damageAmount * distFactor; + continue; } + float distFactor = 1.0f - dist / attack.Range; + float damageAmount = attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier; + item.Condition -= damageAmount * distFactor; } } } @@ -200,10 +205,9 @@ namespace Barotrauma partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull); - public static void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) { - if (attack.Range <= 0.0f) return; + if (attack.Range <= 0.0f) { return; } //long range for the broad distance check, because large characters may still be in range even if their collider isn't float broadRange = Math.Max(attack.Range * 10.0f, 10000.0f); @@ -226,13 +230,14 @@ namespace Barotrauma explosionPos = ConvertUnits.ToSimUnits(explosionPos); Dictionary distFactors = new Dictionary(); + Dictionary damages = new Dictionary(); foreach (Limb limb in c.AnimController.Limbs) { float dist = Vector2.Distance(limb.WorldPosition, worldPosition); //calculate distance from the "outer surface" of the physics body //doesn't take the rotation of the limb into account, but should be accurate enough for this purpose - float limbRadius = Math.Max(Math.Max(limb.body.width * 0.5f, limb.body.height * 0.5f), limb.body.radius); + float limbRadius = limb.body.GetMaxExtent(); dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius)); if (dist > attack.Range) { continue; } @@ -240,14 +245,18 @@ namespace Barotrauma float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion by 90% - if (Submarine.CheckVisibility(limb.SimPosition, explosionPos) != null) distFactor *= 0.1f; + if (Submarine.CheckVisibility(limb.SimPosition, explosionPos) != null) + { + distFactor *= 0.1f; + } distFactors.Add(limb, distFactor); List modifiedAfflictions = new List(); + int limbCount = c.AnimController.Limbs.Count(l => !l.IsSevered && !l.ignoreCollisions); foreach (Affliction affliction in attack.Afflictions.Keys) { - modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / c.AnimController.Limbs.Length)); + modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / limbCount)); } c.LastDamageSource = damageSource; if (attacker == null) @@ -255,14 +264,18 @@ namespace Barotrauma if (damageSource is Item item) { attacker = item.GetComponent()?.User; - if (attacker == null) attacker = item.GetComponent()?.User; + if (attacker == null) + { + attacker = item.GetComponent()?.User; + } } } //use a position slightly from the limb's position towards the explosion //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods Vector2 hitPos = limb.WorldPosition + (worldPosition - limb.WorldPosition) / dist * 0.01f; - c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker); + AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker); + damages.Add(limb, attackResult.Damage); if (attack.StatusEffects != null && attack.StatusEffects.Any()) { @@ -279,22 +292,27 @@ namespace Barotrauma if (limb.WorldPosition != worldPosition && !MathUtils.NearlyEqual(force, 0.0f)) { Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition); - if (!MathUtils.IsValid(limbDiff)) limbDiff = Rand.Vector(1.0f); + if (!MathUtils.IsValid(limbDiff)) { limbDiff = Rand.Vector(1.0f); } Vector2 impulse = limbDiff * distFactor * force; Vector2 impulsePoint = limb.SimPosition - limbDiff * limbRadius; - limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.2f); } } //sever joints - if (c.IsDead && attack.SeverLimbsProbability > 0.0f) + if (attack.SeverLimbsProbability > 0.0f) { foreach (Limb limb in c.AnimController.Limbs) { - if (!distFactors.ContainsKey(limb)) { continue; } - if (Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability * distFactors[limb]) + if (limb.character.Removed || limb.Removed) { continue; } + if (limb.IsSevered) { continue; } + if (!c.IsDead && !limb.CanBeSeveredAlive) { continue; } + if (distFactors.TryGetValue(limb, out float distFactor)) { - c.TrySeverLimbJoints(limb, 1.0f); + if (damages.TryGetValue(limb, out float damage)) + { + c.TrySeverLimbJoints(limb, attack.SeverLimbsProbability * distFactor, damage, allowBeheading: true); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index 08feb0f1c..0e0b69192 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -2,12 +2,14 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using Barotrauma.Extensions; #if CLIENT using Barotrauma.Sounds; using Barotrauma.Lights; using Barotrauma.Particles; #endif using FarseerPhysics; +using System.Linq; namespace Barotrauma { @@ -15,6 +17,7 @@ namespace Barotrauma { const float OxygenConsumption = 50.0f; const float GrowSpeed = 20.0f; + const float MaxDamageRange = 250.0f; protected Hull hull; @@ -65,7 +68,7 @@ namespace Barotrauma public virtual float DamageRange { - get { return (float)Math.Sqrt(size.X) * 20.0f; } + get { return Math.Min((float)Math.Sqrt(size.X) * 10.0f, MaxDamageRange); } } public Hull Hull @@ -123,8 +126,8 @@ namespace Barotrauma { i = Math.Min(i, fireSources.Count - 1); j = Math.Min(j, i - 1); - - if (!fireSources[i].CheckOverLap(fireSources[j])) continue; + + if (!fireSources[i].CheckOverLap(fireSources[j])) { continue; } float leftEdge = Math.Min(fireSources[i].position.X, fireSources[j].position.X); @@ -133,12 +136,10 @@ namespace Barotrauma - leftEdge; fireSources[j].position.X = leftEdge; - #if CLIENT fireSources[j].burnDecals.AddRange(fireSources[i].burnDecals); fireSources[j].burnDecals.Sort((d1, d2) => { return Math.Sign(d1.WorldPosition.X - d2.WorldPosition.X); }); #endif - fireSources[i].Remove(); } } @@ -210,21 +211,32 @@ namespace Barotrauma private void DamageCharacters(float deltaTime) { - if (size.X <= 0.0f) return; + if (size.X <= 0.0f) { return; } for (int i = 0; i < Character.CharacterList.Count; i++) { Character c = Character.CharacterList[i]; - if (c.AnimController.CurrentHull == null || c.IsDead) continue; + if (c.CurrentHull == null || c.IsDead) { continue; } - if (!IsInDamageRange(c, DamageRange)) continue; + if (!IsInDamageRange(c, DamageRange)) { continue; } - float dmg = (float)Math.Sqrt(size.X) * deltaTime / c.AnimController.Limbs.Length; + //GetApproximateDistance returns float.MaxValue if there's no path through open gaps between the hulls (e.g. if there's a door/wall in between) + if (hull.GetApproximateDistance(Position, c.Position, c.CurrentHull, 10000.0f) > size.X + DamageRange) + { + return; + } + + float dmg = (float)Math.Sqrt(Math.Min(500, size.X)) * deltaTime / c.AnimController.Limbs.Count(l => !l.IsSevered); foreach (Limb limb in c.AnimController.Limbs) { + if (limb.IsSevered) { continue; } c.LastDamageSource = null; - c.DamageLimb(WorldPosition, limb, new List() { AfflictionPrefab.Burn.Instantiate(dmg) }, 0.0f, false, 0.0f); + c.DamageLimb(WorldPosition, limb, AfflictionPrefab.Burn.Instantiate(dmg).ToEnumerable(), 0.0f, false, 0.0f); } +#if CLIENT + //let clients display the client-side damage immediately, otherwise they may not be able to react to the damage fast enough + c.CharacterHealth.DisplayedVitality = c.Vitality; +#endif c.ApplyStatusEffects(ActionType.OnFire, deltaTime); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 57ba7a095..c0581a19a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -33,6 +33,8 @@ namespace Barotrauma //the force of the water flow which is exerted on physics bodies private Vector2 flowForce; private Hull flowTargetHull; + + private float openedTimer = 1.0f; private float higherSurface; private float lowerSurface; @@ -54,7 +56,11 @@ namespace Barotrauma public float Open { get { return open; } - set { open = MathHelper.Clamp(value, 0.0f, 1.0f); } + set + { + if (value > open) { openedTimer = 1.0f; } + open = MathHelper.Clamp(value, 0.0f, 1.0f); + } } public float Size => IsHorizontal ? Rect.Height : Rect.Width; @@ -124,6 +130,7 @@ namespace Barotrauma InsertToList(); outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * 2.0f, Vector2.UnitX * 2.0f); + outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})"; outsideCollisionBlocker.BodyType = BodyType.Static; outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall; outsideCollisionBlocker.CollidesWith = Physics.CollisionCharacter; @@ -277,57 +284,18 @@ namespace Barotrauma flowForce.X = MathHelper.Clamp(flowForce.X, -MaxFlowForce, MaxFlowForce); flowForce.Y = MathHelper.Clamp(flowForce.Y, -MaxFlowForce, MaxFlowForce); - lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f); + if (openedTimer > 0.0f && flowForce.Length() > lerpedFlowForce.Length()) + { + //if the gap has just been opened/created, allow it to exert a large force instantly without any smoothing + lerpedFlowForce = flowForce; + } + else + { + lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f); + } + openedTimer -= deltaTime; EmitParticles(deltaTime); - - if (flowTargetHull != null && lerpedFlowForce.LengthSquared() > 0.0001f) - { - foreach (Character character in Character.CharacterList) - { - if (character.CurrentHull == null) continue; - if (character.CurrentHull != linkedTo[0] as Hull && - (linkedTo.Count < 2 || character.CurrentHull != linkedTo[1] as Hull)) - { - continue; - } - - foreach (Limb limb in character.AnimController.Limbs) - { - if (!limb.inWater) continue; - - float dist = Vector2.Distance(limb.WorldPosition, WorldPosition); - if (dist > lerpedFlowForce.Length()) continue; - - Vector2 force = lerpedFlowForce / (float)Math.Max(Math.Sqrt(dist), 20.0f) * 0.025f; - - //vertical gaps only apply forces if the character is roughly above/below the gap - if (!IsHorizontal) - { - float xDist = Math.Abs(limb.WorldPosition.X - WorldPosition.X); - if (xDist > rect.Width || rect.Width == 0) break; - - force *= 1.0f - xDist / rect.Width; - } - - if (!MathUtils.IsValid(force)) - { - string errorMsg = "Attempted to apply invalid flow force to the character \"" + character.Name + - "\", gap pos: " + WorldPosition + - ", limb pos: " + limb.WorldPosition + - ", flowforce: " + flowForce + ", lerpedFlowForce:" + lerpedFlowForce + - ", dist: " + dist; - - DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Gap.Update:InvalidFlowForce:" + character.Name, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - errorMsg); - continue; - } - character.AnimController.Collider.ApplyForce(force * limb.body.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - } - } - } } partial void EmitParticles(float deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 3de38ad57..d840a1b80 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -22,9 +22,9 @@ namespace Barotrauma public const float OxygenConsumptionSpeed = 700.0f; public const int WaveWidth = 32; - public static float WaveStiffness = 0.02f; - public static float WaveSpread = 0.05f; - public static float WaveDampening = 0.05f; + public static float WaveStiffness = 0.01f; + public static float WaveSpread = 0.02f; + public static float WaveDampening = 0.02f; //how much excess water the room can contain, relative to the volume of the room. //needed to make it possible for pressure to "push" water up through U-shaped hull configurations @@ -83,6 +83,21 @@ namespace Barotrauma } } + private Color ambientLight; + + [Editable, Serialize("0,0,0,0", true)] + public Color AmbientLight + { + get { return ambientLight; } + set + { + ambientLight = value; +#if CLIENT + lastAmbientLightEditTime = Timing.TotalTime; +#endif + } + } + public override Rectangle Rect { get @@ -501,54 +516,34 @@ namespace Barotrauma rightDelta[i] = WaveSpread * (waveY[i] - waveY[i + 1]); waveVel[i + 1] += rightDelta[i]; } - - for (int i = 1; i < waveY.Length - 1; i++) - { - waveY[i - 1] += leftDelta[i]; - waveY[i + 1] += rightDelta[i]; - } } //make waves propagate through horizontal gaps foreach (Gap gap in ConnectedGaps) { - if (!gap.IsRoomToRoom || !gap.IsHorizontal || gap.Open <= 0.0f) continue; - if (surface > gap.Rect.Y || surface < gap.Rect.Y - gap.Rect.Height) continue; - - Hull hull2 = this == gap.linkedTo[0] as Hull ? (Hull)gap.linkedTo[1] : (Hull)gap.linkedTo[0]; - float otherSurfaceY = hull2.surface; - if (otherSurfaceY > gap.Rect.Y || otherSurfaceY < gap.Rect.Y - gap.Rect.Height) continue; - - float surfaceDiff = (surface - otherSurfaceY) * gap.Open; if (this != gap.linkedTo[0] as Hull) { - //the first hull linked to the gap handles the wave propagation, - //the second just updates the surfaces to the same level - if (surfaceDiff < 32.0f) - { - hull2.waveY[hull2.waveY.Length - 1] = surfaceDiff * 0.5f; - waveY[0] = -surfaceDiff * 0.5f; - } + //let the first linked hull handle the water propagation continue; } + if (!gap.IsRoomToRoom || !gap.IsHorizontal || gap.Open <= 0.0f) { continue; } + if (surface > gap.Rect.Y || surface < gap.Rect.Y - gap.Rect.Height) { continue; } + + Hull hull2 = this == gap.linkedTo[0] as Hull ? (Hull)gap.linkedTo[1] : (Hull)gap.linkedTo[0]; + float otherSurfaceY = hull2.surface; + if (otherSurfaceY > gap.Rect.Y || otherSurfaceY < gap.Rect.Y - gap.Rect.Height) { continue; } + + float surfaceDiff = (surface - otherSurfaceY) * gap.Open; for (int j = 0; j < 2; j++) { - int i = waveY.Length - 1; + rightDelta[waveY.Length - 1] = WaveSpread * (hull2.waveY[0] - waveY[waveY.Length - 1] - surfaceDiff) * 0.5f; + waveVel[waveY.Length - 1] += rightDelta[waveY.Length - 1]; + waveY[waveY.Length - 1] += rightDelta[waveY.Length - 1]; - leftDelta[i] = WaveSpread * (waveY[i] - waveY[i - 1]); - waveVel[i - 1] += leftDelta[i]; - - rightDelta[i] = WaveSpread * (waveY[i] - hull2.waveY[0] + surfaceDiff); - hull2.waveVel[0] += rightDelta[i]; - - i = 0; - - hull2.leftDelta[i] = WaveSpread * (hull2.waveY[i] - waveY[waveY.Length - 1] - surfaceDiff); - waveVel[waveVel.Length - 1] += hull2.leftDelta[i]; - - hull2.rightDelta[i] = WaveSpread * (hull2.waveY[i] - hull2.waveY[i + 1]); - hull2.waveVel[i + 1] += hull2.rightDelta[i]; + hull2.leftDelta[0] = WaveSpread * (waveY[waveY.Length - 1] - hull2.waveY[0] + surfaceDiff) * 0.5f; + hull2.waveVel[0] += hull2.leftDelta[0]; + hull2.waveY[0] += hull2.leftDelta[0]; } if (surfaceDiff < 32.0f) @@ -557,13 +552,19 @@ namespace Barotrauma hull2.waveY[0] = surfaceDiff * 0.5f; waveY[waveY.Length - 1] = -surfaceDiff * 0.5f; } - else + } + + + //apply spread (two iterations) + for (int j = 0; j < 2; j++) + { + for (int i = 1; i < waveY.Length - 1; i++) { - hull2.waveY[0] += rightDelta[waveY.Length - 1]; - waveY[waveY.Length - 1] += hull2.leftDelta[0]; + waveY[i - 1] += leftDelta[i]; + waveY[i + 1] += rightDelta[i]; } } - + if (waterVolume < Volume) { LethalPressure -= 10.0f * deltaTime; @@ -609,37 +610,33 @@ namespace Barotrauma FireSources.Remove(fire); } - private HashSet adjacentHulls = new HashSet(); - public IEnumerable GetConnectedHulls(bool includingThis, int? searchDepth = null) + private readonly HashSet adjacentHulls = new HashSet(); + public IEnumerable GetConnectedHulls(bool includingThis, int? searchDepth = null, bool ignoreClosedGaps = false) { adjacentHulls.Clear(); int startStep = 0; - searchDepth = searchDepth ?? 100; - return GetAdjacentHulls(includingThis, adjacentHulls, ref startStep, searchDepth.Value); + searchDepth ??= 100; + GetAdjacentHulls(adjacentHulls, ref startStep, searchDepth.Value, ignoreClosedGaps); + if (!includingThis) { adjacentHulls.Remove(this); } + return adjacentHulls; } - private HashSet GetAdjacentHulls(bool includingThis, HashSet connectedHulls, ref int step, int searchDepth) + private void GetAdjacentHulls(HashSet connectedHulls, ref int step, int searchDepth, bool ignoreClosedGaps = false) { - if (includingThis) - { - connectedHulls.Add(this); - } - if (step > searchDepth) - { - return connectedHulls; - } + connectedHulls.Add(this); + if (step > searchDepth) { return; } foreach (Gap g in ConnectedGaps) { + if (ignoreClosedGaps && g.Open <= 0.0f) { continue; } for (int i = 0; i < 2 && i < g.linkedTo.Count; i++) { if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull)) { step++; - hull.GetAdjacentHulls(true, connectedHulls, ref step, searchDepth); + hull.GetAdjacentHulls(connectedHulls, ref step, searchDepth, ignoreClosedGaps); } } } - return connectedHulls; } /// @@ -666,7 +663,7 @@ namespace Barotrauma if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) { //gap blocked if the door is not open or the predicted state is not open - if (!g.ConnectedDoor.IsOpen || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) + if ((!g.ConnectedDoor.IsOpen && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) { if (g.ConnectedDoor.OpenState < 0.1f) continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 2cd3b6d46..16ce350bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -10,11 +10,14 @@ namespace Barotrauma { partial class ItemAssemblyPrefab : MapEntityPrefab { - private string name; + private readonly string name; public override string Name { get { return name; } } public static readonly PrefabCollection Prefabs = new PrefabCollection(); + public static readonly string VanillaSaveFolder = Path.Combine("Content", "Items", "Assemblies"); + public static readonly string SaveFolder = "ItemAssemblies"; + private bool disposed = false; public override void Dispose() { @@ -50,12 +53,25 @@ namespace Barotrauma name = TextManager.Get("EntityName." + identifier, returnNull: true) ?? originalName; Description = TextManager.Get("EntityDescription." + identifier, returnNull: true) ?? Description; + List containedItemIDs = new List(); + foreach (XElement entityElement in doc.Root.Elements()) + { + var containerElement = entityElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("itemcontainer", StringComparison.OrdinalIgnoreCase)); + if (containerElement == null) { continue; } + + var itemIds = containerElement.GetAttributeIntArray("contained", new int[0]); + containedItemIDs.AddRange(itemIds.Select(id => (ushort)id)); + } + int minX = int.MaxValue, minY = int.MaxValue; int maxX = int.MinValue, maxY = int.MinValue; DisplayEntities = new List>(); foreach (XElement entityElement in doc.Root.Elements()) { - string identifier = entityElement.GetAttributeString("identifier", ""); + ushort id = (ushort)entityElement.GetAttributeInt("ID", 0); + if (id > 0 && containedItemIDs.Contains(id)) { continue; } + + string identifier = entityElement.GetAttributeString("identifier", entityElement.Name.ToString().ToLowerInvariant()); MapEntityPrefab mapEntity = List.FirstOrDefault(p => p.Identifier == identifier); if (mapEntity == null) { @@ -64,9 +80,9 @@ namespace Barotrauma } Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty); - if (mapEntity != null && !entityElement.GetAttributeBool("hideinassemblypreview", false)) + if (mapEntity != null && !entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase))) { - DisplayEntities.Add(new Pair(mapEntity, rect)); + if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) { DisplayEntities.Add(new Pair(mapEntity, rect)); } minX = Math.Min(minX, rect.X); minY = Math.Min(minY, rect.Y - rect.Height); maxX = Math.Max(maxX, rect.Right); @@ -74,7 +90,9 @@ namespace Barotrauma } } - Bounds = new Rectangle(minX, minY, maxX - minX, maxY - minY); + Bounds = minX == int.MaxValue ? + new Rectangle(0, 0, 1, 1) : + new Rectangle(minX, minY, maxX - minX, maxY - minY); Prefabs.Add(this, false); } @@ -142,11 +160,14 @@ namespace Barotrauma List itemAssemblyFiles = new List(); - //find assembly files in the item assembly folder - string directoryPath = Path.Combine("Content", "Items", "Assemblies"); - if (Directory.Exists(directoryPath)) + //find assembly files in the item assembly folders + if (Directory.Exists(VanillaSaveFolder)) { - itemAssemblyFiles.AddRange(Directory.GetFiles(directoryPath)); + itemAssemblyFiles.AddRange(Directory.GetFiles(VanillaSaveFolder)); + } + if (Directory.Exists(SaveFolder)) + { + itemAssemblyFiles.AddRange(Directory.GetFiles(SaveFolder)); } //find assembly files in selected content packages diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 0870154a1..b2403d1f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -27,19 +27,19 @@ namespace Barotrauma foreach (GraphEdge ge in graphEdges) { - if (Vector2.DistanceSquared(ge.Point1, ge.Point2) < 0.001f) continue; + if (Vector2.DistanceSquared(ge.Point1, ge.Point2) < 0.001f) { continue; } for (int i = 0; i < 2; i++) { Site site = (i == 0) ? ge.Site1 : ge.Site2; - int x = (int)(Math.Floor((site.Coord.X-borders.X) / gridCellSize)); - int y = (int)(Math.Floor((site.Coord.Y-borders.Y) / gridCellSize)); + int x = (int)(Math.Floor((site.Coord.X - borders.X) / gridCellSize)); + int y = (int)(Math.Floor((site.Coord.Y - borders.Y) / gridCellSize)); - x = MathHelper.Clamp(x, 0, cellGrid.GetLength(0)-1); - y = MathHelper.Clamp(y, 0, cellGrid.GetLength(1)-1); - - VoronoiCell cell = cellGrid[x,y].Find(c => c.Site == site); + x = MathHelper.Clamp(x, 0, cellGrid.GetLength(0) - 1); + y = MathHelper.Clamp(y, 0, cellGrid.GetLength(1) - 1); + + VoronoiCell cell = cellGrid[x, y].Find(c => c.Site == site); if (cell == null) { @@ -60,14 +60,62 @@ namespace Barotrauma } } + //add edges to the borders of the graph + foreach (var cell in cells) + { + Vector2? point1 = null, point2 = null; + foreach (GraphEdge ge in cell.Edges) + { + if (MathUtils.NearlyEqual(ge.Point1.X, borders.X) || MathUtils.NearlyEqual(ge.Point1.X, borders.Right) || + MathUtils.NearlyEqual(ge.Point1.Y, borders.Y) || MathUtils.NearlyEqual(ge.Point1.Y, borders.Bottom)) + { + if (point1 == null) + { + point1 = ge.Point1; + } + else if (point2 == null) + { + if (MathUtils.NearlyEqual(point1.Value, ge.Point1)) { continue; } + point2 = ge.Point1; + } + } + if (MathUtils.NearlyEqual(ge.Point2.X, borders.X) || MathUtils.NearlyEqual(ge.Point2.X, borders.Right) || + MathUtils.NearlyEqual(ge.Point2.Y, borders.Y) || MathUtils.NearlyEqual(ge.Point2.Y, borders.Bottom)) + { + if (point1 == null) + { + point1 = ge.Point2; + } + else + { + if (MathUtils.NearlyEqual(point1.Value, ge.Point2)) { continue; } + point2 = ge.Point2; + } + } + if (point1.HasValue && point2.HasValue) + { + Debug.Assert(point1 != point2); + var newEdge = new GraphEdge(point1.Value, point2.Value) + { + Cell1 = cell, + IsSolid = true, + Site1 = cell.Site, + OutsideLevel = true + }; + cell.Edges.Add(newEdge); + break; + } + } + } + return cells; } private static Vector2 GetEdgeNormal(GraphEdge edge, VoronoiCell cell = null) { - if (cell == null) cell = edge.AdjacentCell(null); - if (cell == null) return Vector2.UnitX; + if (cell == null) { cell = edge.AdjacentCell(null); } + if (cell == null) { return Vector2.UnitX; } CompareCCW compare = new CompareCCW(cell.Center); if (compare.Compare(edge.Point1, edge.Point2) == -1) @@ -77,9 +125,7 @@ namespace Barotrauma edge.Point2 = temp; } - Vector2 normal = Vector2.Zero; - - normal = Vector2.Normalize(edge.Point2 - edge.Point1); + Vector2 normal = Vector2.Normalize(edge.Point2 - edge.Point1); Vector2 diffToCell = Vector2.Normalize(cell.Center - edge.Point2); normal = new Vector2(-normal.Y, normal.X); @@ -136,7 +182,7 @@ namespace Barotrauma currentCell.CellType = CellType.Path; pathCells.Add(currentCell); - int currentTargetIndex = 1; + int currentTargetIndex = 0; int iterationsLeft = cells.Count; @@ -148,7 +194,7 @@ namespace Barotrauma foreach (GraphEdge edge in currentCell.Edges) { var adjacentCell = edge.AdjacentCell(currentCell); - if (limits.Contains(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y)) + if (adjacentCell != null && limits.Contains(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y)) { allowedEdges.Add(edge); } @@ -161,6 +207,7 @@ namespace Barotrauma for (int i = 0; i < currentCell.Edges.Count; i++) { var adjacentCell = currentCell.Edges[i].AdjacentCell(currentCell); + if (adjacentCell == null) { continue; } double dist = MathUtils.Distance( adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, targetCells[currentTargetIndex].Site.Coord.X, targetCells[currentTargetIndex].Site.Coord.Y); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index b111dc68c..22622d71b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -18,6 +18,10 @@ namespace Barotrauma //all entities are disabled after they reach this depth public const int MaxEntityDepth = -300000; public const float ShaftHeight = 1000.0f; + /// + /// The level generator won't try to adjust the width of the main path above this limit. + /// + public const int MaxSubmarineWidth = 16000; public static Level Loaded { @@ -141,8 +145,6 @@ namespace Barotrauma get { return positionsOfInterest; } } - public readonly List UsedPositions = new List(); - public Submarine StartOutpost { get; private set; } public Submarine EndOutpost { get; private set; } @@ -324,26 +326,25 @@ namespace Barotrauma SeaFloorTopPos = generationParams.SeaFloorDepth + generationParams.MountainHeightMax + generationParams.SeaFloorVariance; int minWidth = 6500; - int maxWidth = 50000; if (Submarine.MainSub != null) { Rectangle dockedSubBorders = Submarine.MainSub.GetDockedBorders(); dockedSubBorders.Inflate(dockedSubBorders.Size.ToVector2() * 0.15f); minWidth = Math.Max(minWidth, Math.Max(dockedSubBorders.Width, dockedSubBorders.Height)); - minWidth = Math.Min(minWidth, maxWidth); + minWidth = Math.Min(minWidth, MaxSubmarineWidth); } Rectangle pathBorders = borders; - pathBorders.Inflate(-minWidth * 2, -minWidth); + pathBorders.Inflate(-Math.Min(minWidth * 2, MaxSubmarineWidth), -minWidth); Debug.Assert(pathBorders.Width > 0 && pathBorders.Height > 0, "The size of the level's path area was negative."); startPosition = new Point( - Rand.Range(minWidth, minWidth * 2, Rand.RandSync.Server), + minWidth, Rand.Range(borders.Height / 2, borders.Height - minWidth * 2, Rand.RandSync.Server)); endPosition = new Point( - borders.Width - Rand.Range(minWidth, minWidth * 2, Rand.RandSync.Server), + borders.Width - minWidth, Rand.Range(borders.Height / 2, borders.Height - minWidth * 2, Rand.RandSync.Server)); //---------------------------------------------------------------------------------- @@ -356,19 +357,25 @@ namespace Barotrauma Point nodeInterval = generationParams.MainPathNodeIntervalRange; for (int x = startPosition.X + nodeInterval.X; - x < endPosition.X - nodeInterval.X; + x < endPosition.X - nodeInterval.X; x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.Server)) { pathNodes.Add(new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.Server))); } - pathNodes.Add(new Point(endPosition.X, borders.Height)); - - if (pathNodes.Count <= 2) + if (pathNodes.Count == 1) { - pathNodes.Insert(1, borders.Center); + pathNodes.Add(new Point(pathBorders.Center.X, pathBorders.Y)); + } + //if all nodes ended up high up in the level, move one down to make sure we utilize the full height of the level + else if (pathNodes.GetRange(1, pathNodes.Count - 1).All(p => p.Y > pathBorders.Y + pathBorders.Height * 0.25f)) + { + int nodeIndex = Rand.Range(1, pathNodes.Count, Rand.RandSync.Server); + pathNodes[nodeIndex] = new Point(pathNodes[nodeIndex].X, pathBorders.Y); } + pathNodes.Add(new Point(endPosition.X, borders.Height)); + GenerateTunnels(pathNodes, minWidth); //---------------------------------------------------------------------------------- @@ -547,21 +554,21 @@ namespace Barotrauma { foreach (GraphEdge edge in cell.Edges) { - if (mirroredEdges.Contains(edge)) continue; + if (mirroredEdges.Contains(edge)) { continue; } edge.Point1.X = borders.Width - edge.Point1.X; edge.Point2.X = borders.Width - edge.Point2.X; - if (!mirroredSites.Contains(edge.Site1)) + if (edge.Site1 != null && !mirroredSites.Contains(edge.Site1)) { //make sure that sites right at the edge of a grid cell end up in the same cell as in the non-mirrored level if (edge.Site1.Coord.X % GridCellSize < 1.0f && - edge.Site1.Coord.X % GridCellSize >= 0.0f) edge.Site1.Coord.X += 1.0f; + edge.Site1.Coord.X % GridCellSize >= 0.0f) { edge.Site1.Coord.X += 1.0f; } edge.Site1.Coord.X = borders.Width - edge.Site1.Coord.X; mirroredSites.Add(edge.Site1); } - if (!mirroredSites.Contains(edge.Site2)) + if (edge.Site2 != null && !mirroredSites.Contains(edge.Site2)) { if (edge.Site2.Coord.X % GridCellSize < 1.0f && - edge.Site2.Coord.X % GridCellSize >= 0.0f) edge.Site2.Coord.X += 1.0f; + edge.Site2.Coord.X % GridCellSize >= 0.0f) { edge.Site2.Coord.X += 1.0f; } edge.Site2.Coord.X = borders.Width - edge.Site2.Coord.X; mirroredSites.Add(edge.Site2); } @@ -1583,8 +1590,8 @@ namespace Barotrauma var subDoc = SubmarineInfo.OpenFile(contentFile.Path); Rectangle borders = Submarine.GetBorders(subDoc.Root); string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); - // Add some vertical margin so that the wreck doesn't block the path entirely. It's still possible that some larger subs can't pass by. - Point paddedDimensions = new Point(borders.Width, borders.Height + 3000); + // Add some margin so that the wreck doesn't block the path entirely. It's still possible that some larger subs can't pass by. + Point paddedDimensions = new Point(borders.Width + 3000, borders.Height + 3000); tempSW.Restart(); // For storing the translations. Used only for debugging. var positions = new List(); @@ -1593,6 +1600,7 @@ namespace Barotrauma int attemptsLeft = maxAttempts; bool success = false; Vector2 spawnPoint = Vector2.Zero; + var allCells = Loaded.GetAllCells(); while (attemptsLeft > 0) { if (attemptsLeft < maxAttempts) @@ -1621,7 +1629,7 @@ namespace Barotrauma tempSW.Stop(); if (success) { - Debug.WriteLine($"Wreck {wreckName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds.ToString()} (ms)"); + Debug.WriteLine($"Wreck {wreckName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)"); tempSW.Restart(); SubmarineInfo info = new SubmarineInfo(contentFile.Path) { @@ -1630,7 +1638,7 @@ namespace Barotrauma Submarine wreck = new Submarine(info); wreck.MakeWreck(); tempSW.Stop(); - Debug.WriteLine($"Wreck {wreck.Info.Name} loaded in { tempSW.ElapsedMilliseconds.ToString()} (ms)"); + Debug.WriteLine($"Wreck {wreck.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)"); wrecks.Add(wreck); wreck.SetPosition(spawnPoint); wreckPositions.Add(wreck, positions); @@ -1643,7 +1651,8 @@ namespace Barotrauma hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.Server); } } - if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability) + // Only spawn thalamus when the wreck has some thalamus items defined. + if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability && wreck.GetItems(false).Any(i => i.Prefab.Category == MapEntityCategory.Thalamus)) { if (!wreck.CreateWreckAI()) { @@ -1839,8 +1848,7 @@ namespace Barotrauma { return true; } - var cells = Loaded.GetAllCells().Where(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance); - return cells.Any(c => c.BodyVertices.Any(v => bounds.ContainsWorld(v))); + return cells.Any(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance && c.BodyVertices.Any(v => bounds.ContainsWorld(v))); } } totalSW.Stop(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 97de9243d..bf7440d9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -238,7 +238,8 @@ namespace Barotrauma } - [Editable, Serialize("5000, 10000", true, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")] + [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), + Serialize("5000, 10000", true, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")] public Point MainPathNodeIntervalRange { get { return mainPathNodeIntervalRange; } @@ -256,7 +257,8 @@ namespace Barotrauma set { smallTunnelCount = MathHelper.Clamp(value, 0, 100); } } - [Editable, Serialize("5000, 10000", true, description: "The minimum and maximum length of small tunnels placed along the main path.")] + [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), + Serialize("5000, 10000", true, description: "The minimum and maximum length of small tunnels placed along the main path.")] public Point SmallTunnelLengthRange { get { return smallTunnelLengthRange; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index ceebafd2c..951521cbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -358,19 +358,23 @@ namespace Barotrauma return; } - //check if there are any other contacts with the entity + //check if there are contacts with any other fixture of the trigger //(the OnSeparation callback happens when two fixtures separate, //e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body) ContactEdge contactEdge = fixtureA.Body.ContactList; while (contactEdge != null) { if (contactEdge.Contact != null && + contactEdge.Contact.Enabled && contactEdge.Contact.IsTouching) { - var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ? - contactEdge.Contact.FixtureB : - contactEdge.Contact.FixtureA); - if (otherEntity == entity) return; + if (contactEdge.Contact.FixtureA != fixtureA && contactEdge.Contact.FixtureB != fixtureA) + { + var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ? + contactEdge.Contact.FixtureB : + contactEdge.Contact.FixtureA); + if (otherEntity == entity) { return; } + } } contactEdge = contactEdge.Next; } @@ -418,10 +422,20 @@ namespace Barotrauma public void Update(float deltaTime) { - if (ParentTrigger != null && !ParentTrigger.IsTriggered) return; + if (ParentTrigger != null && !ParentTrigger.IsTriggered) { return; } triggerers.RemoveWhere(t => t.Removed); + if (physicsBody != null) + { + //failsafe to ensure triggerers get removed when they're far from the trigger + float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(physicsBody.GetMaxExtent() * 5), 5000.0f); + triggerers.RemoveWhere(t => + { + return Vector2.Distance(t.WorldPosition, WorldPosition) > maxExtent; + }); + } + bool isNotClient = true; #if CLIENT isNotClient = GameMain.Client == null; @@ -511,6 +525,7 @@ namespace Barotrauma ApplyForce(character.AnimController.Collider, deltaTime); foreach (Limb limb in character.AnimController.Limbs) { + if (limb.IsSevered) { continue; } ApplyForce(limb.body, deltaTime); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index 4eb5b637f..c5445756e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -1,8 +1,8 @@ -using Microsoft.Xna.Framework; +using Barotrauma.IO; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml; using System.Xml.Linq; namespace Barotrauma.RuinGeneration @@ -174,7 +174,7 @@ namespace Barotrauma.RuinGeneration public static void SaveAll() { - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs index 2a96202f1..4b6b241da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs @@ -654,13 +654,14 @@ namespace Barotrauma.RuinGeneration { connectionPanel.Locked = true; connectionPanel.CanBeSelected = false; + connectionPanel.Item.ShouldBeSaved = false; } - - // Hide wires for now + // Hide wires if (ic is Wire wire) { wire.Hidden = true; wire.CanBeSelected = false; + wire.Item.ShouldBeSaved = false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 458abde12..4463d9c06 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs index 1deaed05e..e5dc0990c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index a6cf23e7e..271567653 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -174,7 +174,11 @@ namespace Barotrauma int newWidth = ResizeHorizontal ? rect.Width : (int)(defaultRect.Width * relativeScale); int newHeight = ResizeVertical ? rect.Height : (int)(defaultRect.Height * relativeScale); Rect = new Rectangle(rect.X, rect.Y, newWidth, newHeight); - if (Sections != null) + if (StairDirection != Direction.None) + { + CreateStairBodies(); + } + else if (Sections != null) { UpdateSections(); } @@ -283,6 +287,13 @@ namespace Barotrauma } } + [Serialize(false, true), Editable] + public bool NoAITarget + { + get; + private set; + } + public Dictionary SerializableProperties { get; @@ -354,6 +365,7 @@ namespace Barotrauma } StairDirection = Prefab.StairDirection; + NoAITarget = Prefab.NoAITarget; SerializableProperties = SerializableProperty.GetProperties(this); InitProjSpecific(); @@ -381,7 +393,7 @@ namespace Barotrauma } // Only add ai targets automatically to submarine/outpost walls - if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !Prefab.NoAITarget) + if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !NoAITarget) { aiTarget = new AITarget(this) { @@ -423,6 +435,7 @@ namespace Barotrauma private void CreateStairBodies() { Bodies = new List(); + bodyDebugDimensions.Clear(); float stairAngle = MathHelper.ToRadians(Math.Min(Prefab.StairAngle, 75.0f)); @@ -440,7 +453,7 @@ namespace Barotrauma newBody.Friction = 0.8f; newBody.UserData = this; - newBody.Position = ConvertUnits.ToSimUnits(stairPos) + BodyOffset; + newBody.Position = ConvertUnits.ToSimUnits(stairPos) + BodyOffset * Scale; bodyDebugDimensions.Add(new Vector2(bodyWidth, bodyHeight)); @@ -567,12 +580,12 @@ namespace Barotrauma { foreach (MapEntity mapEntity in mapEntityList) { - if (!(mapEntity is Structure structure)) continue; - if (!structure.Prefab.AllowAttachItems) continue; - if (structure.Bodies != null && structure.Bodies.Count > 0) continue; + if (!(mapEntity is Structure structure)) { continue; } + if (!structure.Prefab.AllowAttachItems) { continue; } + if (structure.Bodies != null && structure.Bodies.Count > 0) { continue; } Rectangle worldRect = mapEntity.WorldRect; - if (worldPosition.X < worldRect.X || worldPosition.X > worldRect.Right) continue; - if (worldPosition.Y > worldRect.Y || worldPosition.Y < worldRect.Y - worldRect.Height) continue; + if (worldPosition.X < worldRect.X || worldPosition.X > worldRect.Right) { continue; } + if (worldPosition.Y > worldRect.Y || worldPosition.Y < worldRect.Y - worldRect.Height) { continue; } return structure; } return null; @@ -818,7 +831,10 @@ namespace Barotrauma Vector2 sectionPos = new Vector2( Sections[sectionIndex].rect.X + Sections[sectionIndex].rect.Width / 2.0f, Sections[sectionIndex].rect.Y - Sections[sectionIndex].rect.Height / 2.0f); - if (world && Submarine != null) sectionPos += Submarine.Position; + if (world && Submarine != null) + { + sectionPos += Submarine.Position; + } return sectionPos; } else @@ -839,7 +855,10 @@ namespace Barotrauma (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter; - if (world && Submarine != null) sectionPos += Submarine.Position; + if (world && Submarine != null) + { + sectionPos += Submarine.Position; + } return sectionPos; } @@ -1255,6 +1274,11 @@ namespace Barotrauma s.UseDropShadow = prefab.Body; } + if (element.Attribute("noaitarget") == null) + { + s.NoAITarget = prefab.NoAITarget; + } + return s; } @@ -1327,6 +1351,7 @@ namespace Barotrauma SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement); Sprite.ReloadXML(); SpriteDepth = Sprite.Depth; + NoAITarget = Prefab.NoAITarget; } public override void Update(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 51df613ad..7a828eaf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -4,7 +4,7 @@ using System; using System.Linq; using System.Collections.Generic; using System.Xml.Linq; -using System.IO; +using Barotrauma.IO; #if CLIENT using Microsoft.Xna.Framework.Graphics; #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 41f130f42..67d14e802 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -7,7 +7,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.ComponentModel; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -740,7 +740,7 @@ namespace Barotrauma public void FlipX(List parents = null) { - if (parents == null) parents = new List(); + if (parents == null) { parents = new List(); } parents.Add(this); flippedX = !flippedX; @@ -748,20 +748,25 @@ namespace Barotrauma Item.UpdateHulls(); List bodyItems = Item.ItemList.FindAll(it => it.Submarine == this && it.body != null); - List subEntities = MapEntity.mapEntityList.FindAll(me => me.Submarine == this); foreach (MapEntity e in subEntities) { if (e is Item) continue; - if (e is LinkedSubmarine) + if (e is LinkedSubmarine linkedSub) { - Submarine sub = ((LinkedSubmarine)e).Sub; - if (!parents.Contains(sub)) + Submarine sub = linkedSub.Sub; + if (sub == null) + { + Vector2 relative1 = linkedSub.Position - SubBody.Position; + relative1.X = -relative1.X; + linkedSub.Rect = new Rectangle((relative1 + SubBody.Position).ToPoint(), linkedSub.Rect.Size); + } + else if (!parents.Contains(sub)) { Vector2 relative1 = sub.SubBody.Position - SubBody.Position; relative1.X = -relative1.X; - sub.SetPosition(relative1 + SubBody.Position); + sub.SetPosition(relative1 + SubBody.Position, new List(parents)); sub.FlipX(parents); } } @@ -779,7 +784,7 @@ namespace Barotrauma Vector2 pos = new Vector2(subBody.Position.X, subBody.Position.Y); subBody.Body.Remove(); subBody = new SubmarineBody(this); - SetPosition(pos); + SetPosition(pos, new List(parents.Where(p => p != this))); if (entityGrid != null) { @@ -812,20 +817,24 @@ namespace Barotrauma Item.UpdateHulls(); Gap.UpdateHulls(); +#if CLIENT + Lights.ConvexHull.RecalculateAll(this); +#endif } public void Update(float deltaTime) { //if (PlayerInput.KeyHit(InputType.Crouch) && (this == MainSub)) FlipX(); - if (Level.Loaded == null || subBody == null) { return; } - if (Info.IsWreck) { WreckAI?.Update(deltaTime); } - if (WorldPosition.Y < Level.MaxEntityDepth && + if (subBody?.Body == null) { return; } + + if (Level.Loaded != null && + WorldPosition.Y < Level.MaxEntityDepth && subBody.Body.Enabled && (GameMain.NetworkMember?.RespawnManager == null || this != GameMain.NetworkMember.RespawnManager.RespawnShuttle)) { @@ -852,17 +861,17 @@ namespace Barotrauma return; } + subBody.Body.LinearVelocity = new Vector2( LockX ? 0.0f : subBody.Body.LinearVelocity.X, LockY ? 0.0f : subBody.Body.LinearVelocity.Y); - subBody.Update(deltaTime); for (int i = 0; i < 2; i++) { - if (MainSubs[i] == null) continue; - if (this != MainSubs[i] && MainSubs[i].DockedTo.Contains(this)) return; + if (MainSubs[i] == null) { continue; } + if (this != MainSubs[i] && MainSubs[i].DockedTo.Contains(this)) { return; } } //send updates more frequently if moving fast @@ -884,9 +893,9 @@ namespace Barotrauma prevPosition = position; } - public void SetPosition(Vector2 position, List checkd=null) + public void SetPosition(Vector2 position, List checkd = null) { - if (!MathUtils.IsValid(position)) return; + if (!MathUtils.IsValid(position)) { return; } if (checkd == null) { checkd = new List(); } if (checkd.Contains(this)) { return; } @@ -898,7 +907,7 @@ namespace Barotrauma foreach (Submarine dockedSub in DockedTo) { - if (dockedSub.Info.IsOutpost) + if (dockedSub.PhysicsBody.BodyType == BodyType.Static) { if (ConnectedDockingPorts.TryGetValue(dockedSub, out DockingPort port)) { @@ -1169,7 +1178,7 @@ namespace Barotrauma foreach (Hull hull in matchingHulls) { - if (string.IsNullOrEmpty(hull.RoomName) || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(hull.RoomName))// || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase)) { hull.RoomName = hull.CreateRoomName(); } @@ -1229,7 +1238,7 @@ namespace Barotrauma Info.CheckSubsLeftBehind(element); } - public bool SaveAs(string filePath, MemoryStream previewImage = null) + public bool SaveAs(string filePath, System.IO.MemoryStream previewImage = null) { var newInfo = new SubmarineInfo(this) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index ffbfa18ff..e8ffa5a61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Collision; using FarseerPhysics.Common; @@ -302,7 +303,7 @@ namespace Barotrauma //------------------------- //if outside left or right edge of the level - if (Position.X < 0 || Position.X > Level.Loaded.Size.X) + if (Level.Loaded != null && (Position.X < 0 || Position.X > Level.Loaded.Size.X)) { Rectangle worldBorders = Borders; worldBorders.Location += MathUtils.ToPoint(Position); @@ -386,12 +387,13 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { - if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine) continue; + if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine) { continue; } foreach (Limb limb in c.AnimController.Limbs) { + if (limb.IsSevered) { continue; } //if the character isn't inside the bounding box, continue - if (!Submarine.RectContains(worldBorders, limb.WorldPosition)) continue; + if (!Submarine.RectContains(worldBorders, limb.WorldPosition)) { continue; } //cast a line from the position of the character to the same direction as the translation of the sub //and see where it intersects with the bounding box @@ -450,16 +452,18 @@ namespace Barotrauma private void UpdateDepthDamage(float deltaTime) { if (Position.Y > DamageDepth) { return; } - +#if CLIENT + if (GameMain.GameSession.GameMode is SubTestMode) { return; } +#endif float depth = DamageDepth - Position.Y; depthDamageTimer -= deltaTime; - if (depthDamageTimer > 0.0f) return; + if (depthDamageTimer > 0.0f) { return; } foreach (Structure wall in Structure.WallList) { - if (wall.Submarine != submarine) continue; + if (wall.Submarine != submarine) { continue; } if (wall.Health < depth * 0.01f) { @@ -499,10 +503,14 @@ namespace Barotrauma } return collision; } - if (f2.Body.UserData is Character character) + else if (f2.Body.UserData is Character character) { return CheckCharacterCollision(contact, character); } + else if (f2.UserData is Items.Components.DockingPort) + { + return false; + } lock (impactQueue) { @@ -634,7 +642,7 @@ namespace Barotrauma float damageAmount = contactDot * Body.Mass / limb.character.Mass; limb.character.LastDamageSource = submarine; limb.character.DamageLimb(ConvertUnits.ToDisplayUnits(collision.ImpactPos), limb, - new List() { AfflictionPrefab.InternalDamage.Instantiate(damageAmount) }, 0.0f, true, 0.0f); + AfflictionPrefab.ImpactDamage.Instantiate(damageAmount).ToEnumerable(), 0.0f, true, 0.0f); if (limb.character.IsDead) { @@ -693,8 +701,8 @@ namespace Barotrauma //find all contacts between this sub and level walls List levelContacts = new List(); - ContactEdge contactEdge = Body.FarseerBody.ContactList; - while (contactEdge.Next != null) + ContactEdge contactEdge = Body?.FarseerBody?.ContactList; + while (contactEdge?.Next != null) { if (contactEdge.Contact.Enabled && contactEdge.Other.UserData is VoronoiCell && @@ -706,7 +714,7 @@ namespace Barotrauma contactEdge = contactEdge.Next; } - if (levelContacts.Count == 0) return; + if (levelContacts.Count == 0) { return; } //if this sub is in contact with the level, apply artifical impacts //to both subs to prevent the other sub from bouncing on top of this one @@ -795,6 +803,7 @@ namespace Barotrauma foreach (Limb limb in c.AnimController.Limbs) { + if (limb.IsSevered) { continue; } limb.body.ApplyLinearImpulse(limb.Mass * impulse, 10.0f); } c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 1c9b47b7e..e0a2279d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.ComponentModel; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -233,7 +233,7 @@ namespace Barotrauma for (int i = 0; i <= maxLoadRetries; i++) { doc = OpenFile(FilePath, out Exception e); - if (e != null && !(e is IOException)) { break; } + if (e != null && !(e is System.IO.IOException)) { break; } if (doc != null || i == maxLoadRetries || !File.Exists(FilePath)) { break; } DebugConsole.NewMessage("Opening submarine file \"" + FilePath + "\" failed, retrying in 250 ms..."); Thread.Sleep(250); @@ -369,13 +369,16 @@ namespace Barotrauma //saving/loading ---------------------------------------------------- - public bool SaveAs(string filePath, MemoryStream previewImage=null) + public bool SaveAs(string filePath, System.IO.MemoryStream previewImage=null) { var newElement = new XElement(SubmarineElement.Name, - SubmarineElement.Attributes().Where(a => !string.Equals(a.Name.LocalName, "previewimage", StringComparison.InvariantCultureIgnoreCase)), + SubmarineElement.Attributes().Where(a => !string.Equals(a.Name.LocalName, "previewimage", StringComparison.InvariantCultureIgnoreCase) && + !string.Equals(a.Name.LocalName, "name", StringComparison.InvariantCultureIgnoreCase)), SubmarineElement.Elements()); XDocument doc = new XDocument(newElement); + doc.Root.Add(new XAttribute("name", Name)); + if (previewImage != null) { doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); @@ -459,7 +462,7 @@ namespace Barotrauma subDirectories = Directory.GetDirectories(SavePath).Where(s => { DirectoryInfo dir = new DirectoryInfo(s); - return (dir.Attributes & FileAttributes.Hidden) == 0; + return (dir.Attributes & System.IO.FileAttributes.Hidden) == 0; }).ToArray(); } catch (Exception e) @@ -559,12 +562,12 @@ namespace Barotrauma if (extension == ".sub") { - Stream stream = null; + System.IO.Stream stream = null; try { stream = SaveUtil.DecompressFiletoStream(file); } - catch (FileNotFoundException e) + catch (System.IO.FileNotFoundException e) { exception = e; DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (File not found) " + Environment.StackTrace, e); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index c3fe670a4..a60cd2c67 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -124,13 +124,15 @@ namespace Barotrauma #if CLIENT if (iconSprites == null) { - iconSprites = new Dictionary() + iconSprites = new Dictionary() { - { SpawnType.Path, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,0,128,128)) }, - { SpawnType.Human, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,0,128,128)) }, - { SpawnType.Enemy, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256,0,128,128)) }, - { SpawnType.Cargo, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) }, - { SpawnType.Corpse, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512,0,128,128)) } + { "Path", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,0,128,128)) }, + { "Human", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,0,128,128)) }, + { "Enemy", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256,0,128,128)) }, + { "Cargo", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) }, + { "Corpse", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512,0,128,128)) }, + { "Ladder", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,128,128,128)) }, + { "Door", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,128,128,128)) } }; } #endif @@ -592,6 +594,11 @@ namespace Barotrauma return assignedWayPoints; } + public void FindHull() + { + currentHull = Hull.FindHull(WorldPosition, CurrentHull); + } + public override void OnMapLoaded() { currentHull = Hull.FindHull(WorldPosition, currentHull); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 0ece35d95..7402c316d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.IO.Pipes; using System.Linq; using System.Text; @@ -12,8 +12,8 @@ namespace Barotrauma.Networking { static partial class ChildServerRelay { - private static Stream writeStream; - private static Stream readStream; + private static System.IO.Stream writeStream; + private static System.IO.Stream readStream; private static volatile bool shutDown; public static bool HasShutDown { @@ -233,7 +233,12 @@ namespace Barotrauma.Networking { writeStream?.Write(msg, 0, msg.Length); } - catch (IOException) + catch (ObjectDisposedException) + { + shutDown = true; + break; + } + catch (System.IO.IOException) { shutDown = true; break; @@ -263,7 +268,12 @@ namespace Barotrauma.Networking lengthBytes[1] = (byte)0; writeStream?.Write(lengthBytes, 0, 2); } - catch (IOException) + catch (ObjectDisposedException) + { + shutDown = true; + break; + } + catch (System.IO.IOException) { shutDown = true; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index f84dc043a..d77dc356a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs index 001826a89..b8688b981 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; -using System.Xml; using System.Xml.Linq; namespace Barotrauma @@ -116,7 +115,7 @@ namespace Barotrauma doc = XMLExtensions.TryLoadXml(ConfigFile); break; } - catch (IOException) + catch (System.IO.IOException) { if (i == maxLoadRetries) { break; } DebugConsole.NewMessage("Opening karma settings file \"" + ConfigFile + "\" failed, retrying in 250 ms..."); @@ -171,7 +170,7 @@ namespace Barotrauma doc.Root.Add(preset.Value); } - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true @@ -184,11 +183,11 @@ namespace Barotrauma { using (var writer = XmlWriter.Create(ConfigFile, settings)) { - doc.Save(writer); + doc.SaveSafe(writer); } break; } - catch (IOException) + catch (System.IO.IOException) { if (i == maxLoadRetries) { throw; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index 8372bc26f..40c7a1104 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -1,7 +1,7 @@ using Lidgren.Network; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.IO.Compression; using System.Runtime.InteropServices; using System.Text; @@ -519,7 +519,7 @@ namespace Barotrauma.Networking } else { - using (MemoryStream output = new MemoryStream()) + using (System.IO.MemoryStream output = new System.IO.MemoryStream()) { using (DeflateStream dstream = new DeflateStream(output, CompressionLevel.Fastest)) { @@ -613,9 +613,9 @@ namespace Barotrauma.Networking if (isCompressed) { byte[] decompressedData; - using (MemoryStream input = new MemoryStream(inBuf, startPos, inLength)) + using (System.IO.MemoryStream input = new System.IO.MemoryStream(inBuf, startPos, inLength)) { - using (MemoryStream output = new MemoryStream()) + using (System.IO.MemoryStream output = new System.IO.MemoryStream()) { using (DeflateStream dstream = new DeflateStream(input, CompressionMode.Decompress)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index 5a0d44c5c..a8f02444a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; namespace Barotrauma.Networking diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 49d1c1ecf..4062779b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Net; using System.Security.Cryptography; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs index 6e9ab0a9d..7c24c7f02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs @@ -125,12 +125,15 @@ namespace Barotrauma.Networking msg.Write((UInt16)LatestBufferID); msg.Write(ForceLocal); msg.WritePadBits(); - for (int i = 0; i < BUFFER_COUNT; i++) + lock (buffers) { - int index = (newestBufferInd + i + 1) % BUFFER_COUNT; + for (int i = 0; i < BUFFER_COUNT; i++) + { + int index = (newestBufferInd + i + 1) % BUFFER_COUNT; - msg.Write((byte)bufferLengths[index]); - msg.Write(buffers[index], 0, bufferLengths[index]); + msg.Write((byte)bufferLengths[index]); + msg.Write(buffers[index], 0, bufferLengths[index]); + } } } @@ -144,10 +147,13 @@ namespace Barotrauma.Networking ForceLocal = msg.ReadBoolean(); msg.ReadPadBits(); firstRead = false; - for (int i = 0; i < BUFFER_COUNT; i++) + lock (buffers) { - bufferLengths[i] = msg.ReadByte(); - buffers[i] = msg.ReadBytes(bufferLengths[i]); + for (int i = 0; i < BUFFER_COUNT; i++) + { + bufferLengths[i] = msg.ReadByte(); + buffers[i] = msg.ReadBytes(bufferLengths[i]); + } } newestBufferInd = BUFFER_COUNT - 1; LatestBufferID = incLatestBufferID; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/WhiteList.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/WhiteList.cs index d4a02a715..cb197f3b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/WhiteList.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/WhiteList.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; namespace Barotrauma.Networking diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs index b19aa5e29..2c32ebdff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs @@ -16,7 +16,7 @@ namespace Barotrauma public const Category CollisionLevel = Category.Cat8; public const Category CollisionRepair = Category.Cat9; - public static float DisplayToRealWorldRatio = 1.0f / 80.0f; + public static float DisplayToRealWorldRatio = 1.0f / 100.0f; public const float DisplayToSimRation = 100.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 2a0115df7..bed982de1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -430,7 +430,7 @@ namespace Barotrauma /// For rectangles, the front is either at the top or at the right, depending on which one of the two is greater: width or height. /// The rotation is in radians. /// - public Vector2 GetLocalFront(float spritesheetRotation = 0) + public Vector2 GetLocalFront(float? spritesheetRotation = null) { Vector2 pos; switch (bodyShape) @@ -445,12 +445,12 @@ namespace Barotrauma pos = new Vector2(0.0f, radius); break; case Shape.Rectangle: - pos = new Vector2(0.0f, Math.Max(height, width) / 2.0f); + pos = height > width ? new Vector2(0, height / 2) : new Vector2(width / 2, 0); break; default: throw new NotImplementedException(); } - return spritesheetRotation == 0 ? pos : Vector2.Transform(pos, Matrix.CreateRotationZ(-spritesheetRotation)); + return spritesheetRotation.HasValue ? Vector2.Transform(pos, Matrix.CreateRotationZ(-spritesheetRotation.Value)) : pos; } public float GetMaxExtent() diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index e3714694e..ec801f1f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -168,20 +168,13 @@ namespace Voronoi2 } midPoint /= vertices.Length; - - for (int i = 1; i < vertices.Length; i++ ) + for (int i = 0; i < vertices.Length; i++) { - GraphEdge ge = new GraphEdge(vertices[i-1], vertices[i]); - + GraphEdge ge = new GraphEdge(vertices[i], vertices[MathUtils.PositiveModulo(i + 1, vertices.Length)]); System.Diagnostics.Debug.Assert(ge.Point1 != ge.Point2); - Edges.Add(ge); } - GraphEdge lastEdge = new GraphEdge(vertices[0], vertices[vertices.Length-1]); - - Edges.Add(lastEdge); - Site = new Site(); Site.SetPoint(midPoint); } @@ -198,9 +191,8 @@ namespace Voronoi2 { foreach (GraphEdge edge in Edges) { - if (MathUtils.LinesIntersect(point, Center, edge.Point1 + Translation, edge.Point2 + Translation)) return false; + if (MathUtils.LinesIntersect(point, Center, edge.Point1 + Translation, edge.Point2 + Translation)) { return false; } } - return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 1be30c7bb..9be5973f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -135,12 +135,6 @@ namespace Barotrauma sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("ParticleUpdate", sw.ElapsedTicks); sw.Restart(); - - GameMain.LightManager.Update((float)deltaTime); - - sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("LightUpdate", sw.ElapsedTicks); - sw.Restart(); if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 37b24658a..38d644e26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -20,6 +20,11 @@ namespace Barotrauma public float MinValueFloat = float.MinValue, MaxValueFloat = float.MaxValue; public float ValueStep; + /// + /// Labels of the components of a vector property (defaults to x,y,z,w) + /// + public string[] VectorComponentLabels; + /// /// Currently implemented only for int fields. TODO: implement the remaining types (SerializableEntityEditor) /// @@ -57,6 +62,11 @@ namespace Barotrauma public bool isSaveable; public string translationTextTag; + /// + /// If set to true, the instance values saved in a submarine file will always override the prefab values, even if using a mod that normally overrides instance values. + /// + public bool AlwaysUseInstanceValues; + public string Description; /// @@ -65,13 +75,15 @@ namespace Barotrauma /// The property is set to this value during deserialization if the value is not defined in XML. /// Is the value saved to XML when serializing. /// If set to anything else than null, SerializableEntityEditors will show what the text gets translated to or warn if the text is not found in the language files. + /// If set to true, the instance values saved in a submarine file will always override the prefab values, even if using a mod that normally overrides instance values. /// Setting the value to a non-empty string will let the user select the text from one whose tag starts with the given string (e.g. RoomName. would show all texts with a RoomName.* tag) - public Serialize(object defaultValue, bool isSaveable, string description = "", string translationTextTag = null) + public Serialize(object defaultValue, bool isSaveable, string description = "", string translationTextTag = null, bool alwaysUseInstanceValues = false) { this.defaultValue = defaultValue; this.isSaveable = isSaveable; this.translationTextTag = translationTextTag; - this.Description = description; + Description = description; + AlwaysUseInstanceValues = alwaysUseInstanceValues; } } @@ -98,6 +110,8 @@ namespace Barotrauma public readonly AttributeCollection Attributes; public readonly Type PropertyType; + public readonly bool OverridePrefabValues; + public PropertyInfo PropertyInfo { get; private set; } public SerializableProperty(PropertyDescriptor property) @@ -107,6 +121,7 @@ namespace Barotrauma PropertyInfo = property.ComponentType.GetProperty(property.Name); PropertyType = property.PropertyType; Attributes = property.Attributes; + OverridePrefabValues = GetAttribute()?.AlwaysUseInstanceValues ?? false; } public T GetAttribute() where T : Attribute @@ -116,7 +131,7 @@ namespace Barotrauma if (a is T) return (T)a; } - return default(T); + return default; } public void SetValue(object parentObject, object val) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 3c3f636d2..4ed031b1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using Barotrauma.IO; using System.Collections.Generic; using System.Globalization; using System.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs index dbaceaa53..4b93e0dc1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs @@ -1,21 +1,38 @@ using System.Collections.Generic; using System.Xml.Linq; using System.Linq; +using System; namespace Barotrauma { partial class ConditionalSprite { public readonly List conditionals = new List(); - public bool IsActive => Target != null && conditionals.All(c => c.Matches(Target)); + public bool IsActive + { + get + { + if (Target == null) { return false; } + return Comparison == PropertyConditional.Comparison.And ? conditionals.All(c => c.Matches(Target)) : conditionals.Any(c => c.Matches(Target)); + } + } + + public readonly PropertyConditional.Comparison Comparison; + public readonly bool Exclusive; public ISerializableEntity Target { get; private set; } public Sprite Sprite { get; private set; } public DeformableSprite DeformableSprite { get; private set; } public Sprite ActiveSprite => Sprite ?? DeformableSprite.Sprite; - public ConditionalSprite(XElement element, ISerializableEntity target, string path = "", string file = "", bool lazyLoad = false) + public ConditionalSprite(XElement element, ISerializableEntity target, string file = "", bool lazyLoad = false) { Target = target; + Exclusive = element.GetAttributeBool("exclusive", Exclusive); + string comparison = element.GetAttributeString("comparison", null); + if (comparison != null) + { + Enum.TryParse(comparison, ignoreCase: true, out Comparison); + } foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -30,10 +47,10 @@ namespace Barotrauma } break; case "sprite": - Sprite = new Sprite(subElement, path, file, lazyLoad: lazyLoad); + Sprite = new Sprite(subElement, file: file, lazyLoad: lazyLoad); break; case "deformablesprite": - DeformableSprite = new DeformableSprite(subElement, filePath: path, lazyLoad: lazyLoad); + DeformableSprite = new DeformableSprite(subElement, filePath: file, lazyLoad: lazyLoad); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs index 81ed6f9e8..7fcc37437 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs @@ -18,12 +18,12 @@ namespace Barotrauma public Sprite Sprite { get; private set; } - public DeformableSprite(XElement element, int? subdivisionsX = null, int? subdivisionsY = null, string filePath = "", bool lazyLoad = false) + public DeformableSprite(XElement element, int? subdivisionsX = null, int? subdivisionsY = null, string filePath = "", bool lazyLoad = false, bool invert = false) { Sprite = new Sprite(element, file: filePath, lazyLoad: lazyLoad); - InitProjSpecific(element, subdivisionsX, subdivisionsY, lazyLoad); + InitProjSpecific(element, subdivisionsX, subdivisionsY, lazyLoad, invert); } - partial void InitProjSpecific(XElement element, int? subdivisionsX, int? subdivisionsY, bool lazyLoad = false); + partial void InitProjSpecific(XElement element, int? subdivisionsX, int? subdivisionsY, bool lazyLoad, bool invert); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index b1aaba6f6..fa7e514ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Xml.Linq; using System.Linq; using Barotrauma.Extensions; -using System.IO; +using Barotrauma.IO; using System; using SpriteParams = Barotrauma.RagdollParams.SpriteParams; #if CLIENT @@ -112,6 +112,8 @@ namespace Barotrauma public string FullPath { get; private set; } + public bool Compress { get; private set; } + public override string ToString() { return FilePath + ": " + sourceRect; @@ -149,6 +151,7 @@ namespace Barotrauma { sourceVector = overrideElement.GetAttributeVector4("sourcerect", Vector4.Zero); } + Compress = SourceElement.GetAttributeBool("compress", true); bool shouldReturn = false; if (!lazyLoad) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index ae67f66f2..2233141e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -81,7 +81,7 @@ namespace Barotrauma if (element.StartTimer > 0.0f) { continue; } - element.Parent.Apply(1.0f, element.Entity, element.Targets, element.WorldPosition); + element.Parent.Apply(deltaTime, element.Entity, element.Targets, element.WorldPosition); DelayList.Remove(element); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 7eb494f1f..5bc04a1a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -193,44 +193,64 @@ namespace Barotrauma string[] readTags = valStr.Split(','); int matches = 0; foreach (string tag in readTags) - if (target is Item item && item.HasTag(tag)) matches++; - + { + if (target is Item item && item.HasTag(tag)) + { + matches++; + } + } //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. return Operator == OperatorType.Equals ? matches >= readTags.Length : matches <= 0; } case ConditionType.HasStatusTag: if (target == null) { return Operator == OperatorType.NotEquals; } - - List durations = StatusEffect.DurationList.FindAll(d => d.Targets.Contains(target)); - List delays = DelayedEffect.DelayList.FindAll(d => d.Targets.Contains(target)); - bool success = false; - if (durations.Count > 0 || delays.Count > 0) + if (StatusEffect.DurationList.Any(d => d.Targets.Contains(target)) || DelayedEffect.DelayList.Any(d => d.Targets.Contains(target))) { string[] readTags = valStr.Split(','); - foreach (DurationListElement duration in durations) + foreach (DurationListElement duration in StatusEffect.DurationList) { + if (!duration.Targets.Contains(target)) { continue; } int matches = 0; foreach (string tag in readTags) - if (duration.Parent.HasTag(tag)) matches++; - + { + if (duration.Parent.HasTag(tag)) + { + matches++; + } + } success = Operator == OperatorType.Equals ? matches >= readTags.Length : matches <= 0; if (cancelStatusEffect > 0 && success) + { StatusEffect.DurationList.Remove(duration); - if (cancelStatusEffect != 2) //cancelStatusEffect 1 = only cancel once, cancelStatusEffect 2 = cancel all of matching tags + } + if (cancelStatusEffect != 2) + { + //cancelStatusEffect 1 = only cancel once, cancelStatusEffect 2 = cancel all of matching tags return success; + } } - foreach (DelayedListElement delay in delays) + foreach (DelayedListElement delay in DelayedEffect.DelayList) { + if (!delay.Targets.Contains(target)) { continue; } int matches = 0; foreach (string tag in readTags) - if (delay.Parent.HasTag(tag)) matches++; - + { + if (delay.Parent.HasTag(tag)) + { + matches++; + } + } success = Operator == OperatorType.Equals ? matches >= readTags.Length : matches <= 0; if (cancelStatusEffect > 0 && success) + { DelayedEffect.DelayList.Remove(delay); - if (cancelStatusEffect != 2) //ditto + } + if (cancelStatusEffect != 2) + { + //ditto return success; + } } } else if (Operator == OperatorType.NotEquals) @@ -242,7 +262,7 @@ namespace Barotrauma case ConditionType.SpeciesName: if (target == null) { return Operator == OperatorType.NotEquals; } if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == (targetCharacter.SpeciesName == valStr); + return (Operator == OperatorType.Equals) == targetCharacter.SpeciesName.Equals(valStr, StringComparison.OrdinalIgnoreCase); case ConditionType.EntityType: switch (valStr) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index bc28e243b..564ffad69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -104,7 +104,7 @@ namespace Barotrauma } } - class CharacterSpawnInfo : ISerializableEntity + public class CharacterSpawnInfo : ISerializableEntity { public string Name => $"Character Spawn Info ({SpeciesName})"; public Dictionary SerializableProperties { get; set; } @@ -188,6 +188,11 @@ namespace Barotrauma private set; } + public IEnumerable SpawnCharacters + { + get { return spawnCharacters; } + } + private readonly List> reduceAffliction; //only applicable if targeting NearbyCharacters or NearbyItems @@ -313,7 +318,7 @@ namespace Barotrauma break; case "conditionalcomparison": case "comparison": - if (!Enum.TryParse(attribute.Value, out conditionalComparison)) + if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalComparison)) { DebugConsole.ThrowError("Invalid conditional comparison type \"" + attribute.Value + "\" in StatusEffect (" + parentDebugName + ")"); } @@ -688,7 +693,7 @@ namespace Barotrauma } Vector2 position = worldPosition ?? (entity.Removed ? Vector2.Zero : entity.WorldPosition); - if (targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) + if (worldPosition == null && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) { if (entity is Character c) { @@ -745,13 +750,20 @@ namespace Barotrauma { if (target is Entity targetEntity) { - if (targetEntity.Removed) continue; + if (targetEntity.Removed) { continue; } + } + + if (target is Limb limb) + { + position = limb.WorldPosition + Offset; } for (int i = 0; i < propertyNames.Length; i++) { - if (target == null || target.SerializableProperties == null || - !target.SerializableProperties.TryGetValue(propertyNames[i], out SerializableProperty property)) continue; + if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[i], out SerializableProperty property)) + { + continue; + } ApplyToProperty(target, property, propertyEffects[i], deltaTime); } } @@ -767,7 +779,10 @@ namespace Barotrauma foreach (Affliction affliction in Afflictions) { Affliction multipliedAffliction = affliction; - if (!disableDeltaTime) multipliedAffliction = affliction.CreateMultiplied(deltaTime); + if (!disableDeltaTime) + { + multipliedAffliction = affliction.CreateMultiplied(deltaTime); + } if (target is Character character) { @@ -775,18 +790,21 @@ namespace Barotrauma character.LastDamageSource = entity; foreach (Limb limb in character.AnimController.Limbs) { + if (limb.Removed) { continue; } + if (limb.IsSevered) { continue; } if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } - limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); - limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability); + AttackResult result = limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, result.Damage, allowBeheading: true); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } } } else if (target is Limb limb) { + if (limb.IsSevered) { continue; } if (limb.character.Removed || limb.Removed) { continue; } - limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); - limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability); + AttackResult result = limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, result.Damage, allowBeheading: true); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 618ec32a6..0fc57d9d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -67,16 +67,19 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { - if (c.IsDead) continue; + if (c.IsDead) { continue; } //achievement for descending below crush depth and coming back - if (c.WorldPosition.Y < SubmarineBody.DamageDepth || (c.Submarine != null && c.Submarine.WorldPosition.Y < SubmarineBody.DamageDepth)) + if (Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) { - roundData.EnteredCrushDepth.Add(c); - } - else if (c.WorldPosition.Y > SubmarineBody.DamageDepth * 0.5f) - { - //all characters that have entered crush depth and are still alive get an achievement - if (roundData.EnteredCrushDepth.Contains(c)) UnlockAchievement(c, "survivecrushdepth"); + if (c.WorldPosition.Y < SubmarineBody.DamageDepth || (c.Submarine != null && c.Submarine.WorldPosition.Y < SubmarineBody.DamageDepth)) + { + roundData.EnteredCrushDepth.Add(c); + } + else if (c.WorldPosition.Y > SubmarineBody.DamageDepth * 0.5f) + { + //all characters that have entered crush depth and are still alive get an achievement + if (roundData.EnteredCrushDepth.Contains(c)) UnlockAchievement(c, "survivecrushdepth"); + } } } @@ -106,7 +109,7 @@ namespace Barotrauma //achievement for descending ridiculously deep float realWorldDepth = Math.Abs(sub.Position.Y - Level.Loaded.Size.Y) * Physics.DisplayToRealWorldRatio; - if (realWorldDepth > 5000.0f) + if (realWorldDepth > 5000.0f && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) { //all conscious characters inside the sub get an achievement UnlockAchievement("subdeep", true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); @@ -375,6 +378,12 @@ namespace Barotrauma { UnlockAchievement(charactersInSub[0], "lastmanstanding"); } +#if CLIENT + else if (GameMain.GameSession.CrewManager.GetCharacters().Count() == 1) + { + UnlockAchievement(charactersInSub[0], "lonesailor"); + } +#else //lone sailor achievement if alone in the sub and there are no other characters with the same team ID else if (!Character.CharacterList.Any(c => c != charactersInSub[0] && @@ -383,6 +392,8 @@ namespace Barotrauma { UnlockAchievement(charactersInSub[0], "lonesailor"); } +#endif + } foreach (Character character in charactersInSub) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs index d27d9d074..d201dc125 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs index 26ad16718..06526e2ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Xml.Linq; +using Barotrauma.IO; namespace Barotrauma { @@ -34,7 +35,7 @@ namespace Barotrauma { //try fixing legacy EnglishVanilla path string newPath = "Content/Texts/English/EnglishVanilla.xml"; - if (System.IO.File.Exists(newPath)) + if (Barotrauma.IO.File.Exists(newPath)) { DebugConsole.NewMessage("Content package is using the obsolete text file path \"" + filePath + "\". Attempting to load from \"" + newPath + "\"..."); this.FilePath = filePath = newPath; @@ -172,9 +173,7 @@ namespace Barotrauma } } - System.IO.StreamWriter file = new System.IO.StreamWriter(@"duplicate_" + Language.ToLower() + "_" + index + ".txt"); - file.WriteLine(sb.ToString()); - file.Close(); + File.WriteAllText(@"duplicate_" + Language.ToLower() + "_" + index + ".txt", sb.ToString()); } public void WriteToCSV(int index) @@ -199,9 +198,7 @@ namespace Barotrauma } } - System.IO.StreamWriter file = new System.IO.StreamWriter(@"csv_" + Language.ToLower() + "_" + index + ".csv"); - file.WriteLine(sb.ToString()); - file.Close(); + File.WriteAllText(@"csv_" + Language.ToLower() + "_" + index + ".csv", sb.ToString()); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs new file mode 100644 index 000000000..509008557 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -0,0 +1,546 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; + +namespace Barotrauma.IO +{ + static class Validation + { + static readonly string[] unwritableDirs = new string[] { "Content", "Data/ContentPackages" }; + + public static bool CanWrite(string path, bool canWarn = true) + { + path = System.IO.Path.GetFullPath(path).CleanUpPath(); + + foreach (string unwritableDir in unwritableDirs) + { + string dir = System.IO.Path.GetFullPath(unwritableDir).CleanUpPath(); + + if (path.StartsWith(dir, StringComparison.InvariantCultureIgnoreCase)) + { +#if DEBUG + if (canWarn) + { + DebugConsole.NewMessage($"WARNING: writing to \"{path}\" is disallowed in Release builds!\n{Environment.StackTrace}", Color.Orange); + } + return true; +#else + return false; +#endif + } + } + + return true; + } + } + + public static class SafeXML + { + public static void SaveSafe(this System.Xml.Linq.XDocument doc, string path) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot save XML document to \"{path}\": failed validation"); + return; + } + doc.Save(path); + } + + public static void SaveSafe(this System.Xml.Linq.XDocument doc, XmlWriter writer) + { + doc.WriteTo(writer); + } + + public static void WriteTo(this System.Xml.Linq.XDocument doc, XmlWriter writer) + { + writer.Write(doc); + } + } + + public class XmlWriter : IDisposable + { + public readonly System.Xml.XmlWriter Writer; + + public XmlWriter(string path, System.Xml.XmlWriterSettings settings) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot write XML document to \"{path}\": failed validation"); + Writer = null; + return; + } + Writer = System.Xml.XmlWriter.Create(path, settings); + } + + public static XmlWriter Create(string path, System.Xml.XmlWriterSettings settings) + { + return new XmlWriter(path, settings); + } + + public void Write(System.Xml.Linq.XDocument doc) + { + if (Writer == null) + { + DebugConsole.ThrowError("Cannot write to invalid XmlWriter"); + return; + } + doc.WriteTo(Writer); + } + + public void Flush() + { + if (Writer == null) + { + DebugConsole.ThrowError("Cannot flush invalid XmlWriter"); + return; + } + Writer.Flush(); + } + + public void Dispose() + { + if (Writer == null) + { + DebugConsole.ThrowError("Cannot dispose invalid XmlWriter"); + return; + } + Writer.Dispose(); + } + } + + public static class Path + { + public static readonly char DirectorySeparatorChar = System.IO.Path.DirectorySeparatorChar; + public static readonly char AltDirectorySeparatorChar = System.IO.Path.AltDirectorySeparatorChar; + + public static string GetExtension(string path) + { + return System.IO.Path.GetExtension(path); + } + + public static string GetFileNameWithoutExtension(string path) + { + return System.IO.Path.GetFileNameWithoutExtension(path); + } + + public static string GetPathRoot(string path) + { + return System.IO.Path.GetPathRoot(path); + } + + public static string GetDirectoryName(string path) + { + return System.IO.Path.GetDirectoryName(path); + } + + public static string GetFileName(string path) + { + return System.IO.Path.GetFileName(path); + } + + public static string GetFullPath(string path) + { + return System.IO.Path.GetFullPath(path); + } + + public static string Combine(params string[] s) + { + return System.IO.Path.Combine(s); + } + + public static string GetTempFileName() + { + return System.IO.Path.GetTempFileName(); + } + + public static bool IsPathRooted(string path) + { + return System.IO.Path.IsPathRooted(path); + } + public static IEnumerable GetInvalidFileNameChars() + { + return System.IO.Path.GetInvalidFileNameChars(); + } + + } + + public static class Directory + { + public static string GetCurrentDirectory() + { + return System.IO.Directory.GetCurrentDirectory(); + } + + public static void SetCurrentDirectory(string path) + { + System.IO.Directory.SetCurrentDirectory(path); + } + + public static IEnumerable GetFiles(string path) + { + return System.IO.Directory.GetFiles(path); + } + + public static IEnumerable GetFiles(string path, string pattern, System.IO.SearchOption option = System.IO.SearchOption.AllDirectories) + { + return System.IO.Directory.GetFiles(path, pattern, option); + } + + public static IEnumerable GetDirectories(string path) + { + return System.IO.Directory.GetDirectories(path); + } + + public static IEnumerable GetFileSystemEntries(string path) + { + return System.IO.Directory.GetFileSystemEntries(path); + } + + public static IEnumerable EnumerateDirectories(string path, string pattern) + { + return System.IO.Directory.EnumerateDirectories(path, pattern); + } + + public static IEnumerable EnumerateFiles(string path, string pattern) + { + return System.IO.Directory.EnumerateFiles(path, pattern); + } + + public static bool Exists(string path) + { + return System.IO.Directory.Exists(path); + } + + public static System.IO.DirectoryInfo CreateDirectory(string path) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot create directory \"{path}\": failed validation"); + return null; + } + return System.IO.Directory.CreateDirectory(path); + } + + public static void Delete(string path, bool recursive=true) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot delete directory \"{path}\": failed validation"); + return; + } + //TODO: validate recursion? + System.IO.Directory.Delete(path, recursive); + } + } + + public static class File + { + public static bool Exists(string path) + { + return System.IO.File.Exists(path); + } + + public static void Copy(string src, string dest, bool overwrite=false) + { + if (!Validation.CanWrite(dest)) + { + DebugConsole.ThrowError($"Cannot copy \"{src}\" to \"{dest}\": failed validation"); + return; + } + System.IO.File.Copy(src, dest, overwrite); + } + + public static void Move(string src, string dest) + { + if (!Validation.CanWrite(src)) + { + DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": src failed validation"); + return; + } + if (!Validation.CanWrite(dest)) + { + DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": dest failed validation"); + return; + } + System.IO.File.Move(src, dest); + } + + public static void Delete(string path) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot delete file \"{path}\": failed validation"); + return; + } + System.IO.File.Delete(path); + } + + public static DateTime GetLastWriteTime(string path) + { + return System.IO.File.GetLastWriteTime(path); + } + + public static FileStream Open(string path, System.IO.FileMode mode, System.IO.FileAccess access = System.IO.FileAccess.ReadWrite) + { + switch (mode) + { + case System.IO.FileMode.Create: + case System.IO.FileMode.CreateNew: + case System.IO.FileMode.OpenOrCreate: + case System.IO.FileMode.Append: + case System.IO.FileMode.Truncate: + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot open \"{path}\" in {mode} mode: failed validation"); + return null; + } + break; + } + return new FileStream(path, System.IO.File.Open(path, mode, + !Validation.CanWrite(path, false) ? + System.IO.FileAccess.Read : + access)); + } + + public static FileStream OpenRead(string path) + { + return Open(path, System.IO.FileMode.Open, System.IO.FileAccess.Read); + } + + public static FileStream OpenWrite(string path) + { + return Open(path, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write); + } + + public static FileStream Create(string path) + { + return Open(path, System.IO.FileMode.Create, System.IO.FileAccess.Write); + } + + public static void WriteAllBytes(string path, byte[] contents) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot write all bytes to \"{path}\": failed validation"); + return; + } + System.IO.File.WriteAllBytes(path, contents); + } + + public static void WriteAllText(string path, string contents, System.Text.Encoding? encoding = null) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot write all text to \"{path}\": failed validation"); + return; + } + System.IO.File.WriteAllText(path, contents, encoding ?? System.Text.Encoding.UTF8); + } + + public static void WriteAllLines(string path, IEnumerable contents, System.Text.Encoding? encoding = null) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot write all lines to \"{path}\": failed validation"); + return; + } + System.IO.File.WriteAllLines(path, contents, encoding ?? System.Text.Encoding.UTF8); + } + + public static byte[] ReadAllBytes(string path) + { + return System.IO.File.ReadAllBytes(path); + } + + public static string ReadAllText(string path, System.Text.Encoding? encoding = null) + { + return System.IO.File.ReadAllText(path, encoding ?? System.Text.Encoding.UTF8); + } + + public static string[] ReadAllLines(string path, System.Text.Encoding? encoding = null) + { + return System.IO.File.ReadAllLines(path, encoding ?? System.Text.Encoding.UTF8); + } + } + + public class FileStream : System.IO.Stream + { + private System.IO.FileStream innerStream; + private string fileName; + + public FileStream(string fn, System.IO.FileStream stream) + { + innerStream = stream; + fileName = fn; + } + + public override bool CanRead => innerStream.CanRead; + public override bool CanSeek => innerStream.CanSeek; + public override bool CanTimeout => innerStream.CanTimeout; + public override bool CanWrite + { + get + { + if (!Validation.CanWrite(fileName)) { return false; } + return innerStream.CanWrite; + } + } + + public override long Length => innerStream.Length; + + public override long Position + { + get + { + return innerStream.Position; + } + set + { + innerStream.Position = value; + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + return innerStream.Read(buffer, offset, count); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (Validation.CanWrite(fileName)) + { + innerStream.Write(buffer, offset, count); + } + else + { + DebugConsole.ThrowError($"Cannot write to file \"{fileName}\": failed validation"); + } + } + + public override long Seek(long offset, System.IO.SeekOrigin origin) + { + return innerStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + innerStream.SetLength(value); + } + + public override void Flush() + { + innerStream.Flush(); + } + + protected override void Dispose(bool disposing) + { + innerStream.Dispose(); + } + } + + public class DirectoryInfo + { + private System.IO.DirectoryInfo innerInfo; + + public DirectoryInfo(string path) + { + innerInfo = new System.IO.DirectoryInfo(path); + } + + private DirectoryInfo(System.IO.DirectoryInfo info) + { + innerInfo = info; + } + + public bool Exists => innerInfo.Exists; + public string Name => innerInfo.Name; + public string FullName => innerInfo.FullName; + + public System.IO.FileAttributes Attributes => innerInfo.Attributes; + + public IEnumerable GetDirectories() + { + var dirs = innerInfo.GetDirectories(); + foreach (var dir in dirs) + { + yield return new DirectoryInfo(dir); + } + } + + public IEnumerable GetFiles() + { + var files = innerInfo.GetFiles(); + foreach (var file in files) + { + yield return new FileInfo(file); + } + } + + public void Delete() + { + if (!Validation.CanWrite(innerInfo.FullName)) + { + DebugConsole.ThrowError($"Cannot delete directory \"{Name}\": failed validation"); + return; + } + innerInfo.Delete(); + } + } + + public class FileInfo + { + private System.IO.FileInfo innerInfo; + + public FileInfo(string path) + { + innerInfo = new System.IO.FileInfo(path); + } + + public FileInfo(System.IO.FileInfo info) + { + innerInfo = info; + } + + public bool Exists => innerInfo.Exists; + public string Name => innerInfo.Name; + public string FullName => innerInfo.FullName; + public long Length => innerInfo.Length; + + public bool IsReadOnly + { + get + { + return innerInfo.IsReadOnly; + } + set + { + if (!Validation.CanWrite(innerInfo.FullName)) + { + DebugConsole.ThrowError($"Cannot set read-only to {value} for \"{Name}\": failed validation"); + return; + } + innerInfo.IsReadOnly = value; + } + } + + public void CopyTo(string dest, bool overwriteExisting = false) + { + if (!Validation.CanWrite(dest)) + { + DebugConsole.ThrowError($"Cannot copy \"{Name}\" to \"{dest}\": failed validation"); + return; + } + innerInfo.CopyTo(dest, overwriteExisting); + } + + public void Delete() + { + if (!Validation.CanWrite(innerInfo.FullName)) + { + DebugConsole.ThrowError($"Cannot delete file \"{Name}\": failed validation"); + return; + } + innerInfo.Delete(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 2532b16cc..5aa2a9611 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.IO.Compression; using System.Linq; using System.Text; @@ -245,7 +245,7 @@ namespace Barotrauma // B. // Read file into byte array buffer. byte[] b; - using (FileStream f = new FileStream(temp, FileMode.Open)) + using (FileStream f = File.Open(temp, System.IO.FileMode.Open)) { b = new byte[f.Length]; f.Read(b, 0, (int)f.Length); @@ -253,7 +253,7 @@ namespace Barotrauma // C. // Use GZipStream to write compressed bytes to target file. - using (FileStream f2 = new FileStream(fileName, FileMode.Create)) + using (FileStream f2 = File.Open(fileName, System.IO.FileMode.Create)) using (GZipStream gz = new GZipStream(f2, CompressionMode.Compress, false)) { gz.Write(b, 0, b.Length); @@ -276,10 +276,10 @@ namespace Barotrauma public static void CompressDirectory(string sInDir, string sOutFile, ProgressDelegate progress) { - string[] sFiles = Directory.GetFiles(sInDir, "*.*", SearchOption.AllDirectories); + IEnumerable sFiles = Directory.GetFiles(sInDir, "*.*", System.IO.SearchOption.AllDirectories); int iDirLen = sInDir[sInDir.Length - 1] == Path.DirectorySeparatorChar ? sInDir.Length : sInDir.Length + 1; - using (FileStream outFile = new FileStream(sOutFile, FileMode.Create, FileAccess.Write, FileShare.None)) + using (FileStream outFile = File.Open(sOutFile, System.IO.FileMode.Create, System.IO.FileAccess.Write)) using (GZipStream str = new GZipStream(outFile, CompressionMode.Compress)) foreach (string sFilePath in sFiles) { @@ -290,11 +290,11 @@ namespace Barotrauma } - public static Stream DecompressFiletoStream(string fileName) + public static System.IO.Stream DecompressFiletoStream(string fileName) { - using (FileStream originalFileStream = new FileStream(fileName, FileMode.Open)) + using (FileStream originalFileStream = File.Open(fileName, System.IO.FileMode.Open)) { - MemoryStream decompressedFileStream = new MemoryStream(); + System.IO.MemoryStream decompressedFileStream = new System.IO.MemoryStream(); using (GZipStream decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress)) { @@ -347,13 +347,13 @@ namespace Barotrauma { try { - using (FileStream outFile = new FileStream(sFilePath, FileMode.Create, FileAccess.Write, FileShare.None)) + using (FileStream outFile = File.Open(sFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write)) { outFile.Write(bytes, 0, iFileLen); } break; } - catch (IOException e) + catch (System.IO.IOException e) { if (i >= maxRetries || !File.Exists(sFilePath)) { throw; } DebugConsole.NewMessage("Failed decompress file \"" + sFilePath + "\" {" + e.Message + "}, retrying in 250 ms...", Color.Red); @@ -371,13 +371,13 @@ namespace Barotrauma { try { - using (FileStream inFile = new FileStream(sCompressedFile, FileMode.Open, FileAccess.Read, FileShare.None)) + using (FileStream inFile = File.Open(sCompressedFile, System.IO.FileMode.Open, System.IO.FileAccess.Read)) using (GZipStream zipStream = new GZipStream(inFile, CompressionMode.Decompress, true)) while (DecompressFile(sDir, zipStream, progress)) { }; break; } - catch (IOException e) + catch (System.IO.IOException e) { if (i >= maxRetries || !File.Exists(sCompressedFile)) { throw; } DebugConsole.NewMessage("Failed decompress file \"" + sCompressedFile + "\" {" + e.Message + "}, retrying in 250 ms...", Color.Red); @@ -393,12 +393,12 @@ namespace Barotrauma if (!dir.Exists) { - throw new DirectoryNotFoundException( + throw new System.IO.DirectoryNotFoundException( "Source directory does not exist or could not be found: " + sourceDirName); } - DirectoryInfo[] dirs = dir.GetDirectories(); + IEnumerable dirs = dir.GetDirectories(); // If the destination directory doesn't exist, create it. if (!Directory.Exists(destDirName)) { @@ -406,7 +406,7 @@ namespace Barotrauma } // Get the files in the directory and copy them to the new location. - FileInfo[] files = dir.GetFiles(); + IEnumerable files = dir.GetFiles(); foreach (FileInfo file in files) { string tempPath = Path.Combine(destDirName, file.Name); @@ -473,7 +473,7 @@ namespace Barotrauma di.Delete(); break; } - catch (IOException) + catch (System.IO.IOException) { if (i >= maxRetries) { throw; } Thread.Sleep(250); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index e8ff1a8e9..21251f8de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; @@ -325,15 +325,12 @@ namespace Barotrauma { try { - using (StreamReader file = new StreamReader(filePath)) + lines = File.ReadAllLines(filePath).ToList(); + cachedLines.Add(filePath, lines); + if (lines.Count == 0) { - lines = File.ReadLines(filePath).ToList(); - cachedLines.Add(filePath, lines); - if (lines.Count == 0) - { - DebugConsole.ThrowError("File \"" + filePath + "\" is empty!"); - return ""; - } + DebugConsole.ThrowError("File \"" + filePath + "\" is empty!"); + return ""; } } catch (Exception e) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs index 721224dbb..dcd806d14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Security.Cryptography; using System.Xml.Linq; @@ -15,7 +15,7 @@ namespace Barotrauma { XDocument doc = new XDocument(CreateFileList()); - doc.Save(filePath); + doc.SaveSafe(filePath); } public static XElement CreateFileList() @@ -23,7 +23,7 @@ namespace Barotrauma XElement root = new XElement("filelist"); string currentDir = Directory.GetCurrentDirectory(); - string[] files = Directory.GetFiles(currentDir, "*", SearchOption.AllDirectories); + IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); foreach (string file in files) { @@ -122,7 +122,7 @@ namespace Barotrauma /// public static void InstallUpdatedFiles(string updateFileFolder) { - string[] files = Directory.GetFiles(updateFileFolder, "*", SearchOption.AllDirectories); + IEnumerable files = Directory.GetFiles(updateFileFolder, "*", System.IO.SearchOption.AllDirectories); string currentDir = Directory.GetCurrentDirectory(); @@ -166,7 +166,7 @@ namespace Barotrauma { string currentDir = Directory.GetCurrentDirectory(); - string[] files = Directory.GetFiles(currentDir, "*", SearchOption.AllDirectories); + IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); foreach (string file in files) { @@ -199,7 +199,7 @@ namespace Barotrauma { string currentDir = Directory.GetCurrentDirectory(); - string[] files = Directory.GetFiles(currentDir, "*", SearchOption.AllDirectories); + IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); foreach (string file in files) { diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index c36cc0868dbb5edf35ddc0574365e9f6178b2e7f..2b67e4ed6fe7955fd4c552f74cb7b752767b2c81 100644 GIT binary patch literal 217658 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

CQr*OLLf*}4gn&8#7i*?JSCM8dj&LR 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

^Bv6l3iRZ4iU4Q%49)I%Qia^$sJ4wjC zQajGfjS)3kW7J@_AeZ$x-*~WHI@~FDpa<+@d{bex@P@!dl)+~Wfv|SYwAGjt^#M_x zp%90ij()BZ?d-G_b$i?^y3Yy}e{2yyRte%sd=1FC)saX>CF~14g+w>BN-#eV@Z+d* zh#Hl9HB_Vb>&Q;LY6_>@zSF>;xanS)(5VQ-VmHI?KA$$f?ayRR#FliJ;0-5o0~n7d zKeQnTO{G{)YqJPw=z{w@^TM(v7UB^kt1s*a?-vTnW)1GTwkhgP;u}5l^ZIc7*RNI5 ziV+fs-Cooh&X{G>5F>o#Ca)5bEI*tB(*`IQdI~~lGnGrD_<_q)$AB2Tq9oW$l3()$ zzO9mhnR)y67C%;NFxHLox>cq2_>=j;*^3+|;au&a-OpM1qV!iJfRBS-ui&x-?C%?< z;+CZRAMn(*SBqOy_?qqHHOUupiE*iXXT;c^*kdBYB!ekB0B9t7+l_tK7`AR*-G8Q<_V zfKWFw$ByT^I@%&-^!;wlP3o-1rhsN_j>*7(C{Rt_4zS82psabnUB?`3Qe;(j56g|S z72JZq^J{ScPPE5w9_Ii`$g2x)+Bs+2SQrfq7K>_eXNaX=m?Wcq;pN<6p5hi1UrnD6 zvOq{;+rQ|El`LkKH%O{Gr8o1kO3(wbIO7qBU9WXboWl@#HyY$v)|hMNs(HU zj4uO8WsL0a#DcftEuoboby5;K8(2)>YGdaNYljQi>&2KQDBXjJ9PGgWfYd6nso|VNOLSXqNaAF$CS{nL8hVb~)V-Wx`YThx=-v#WN|0KLidE4XG6& z8#qAqA#p~5z=)M8aSC*#EnZ5Hs!h^6*m8y71(LcQm0`rs3OcH;m49DhvvH!ELBY!{ zT{!>bxh0rOdHm}4l^sWBzwZHP%!_X#Jv#8V$i29jU_cBd(%_uEPp7;+&D{2<~G zc=-w>b-!ciH@H+#G7J*=2kfmMnz9*9y1epECnQRTp+(F?7`Vm~NUc0K2AUcAw%!-V zQ4~SH<}IR&Hw4e}o*h~E6TMG<5KH?EU3h ziI;d9@2JEVT>y6dcJG!^Aekev9pF#0jfFSNN5)-D=RK34@qNe}SYfjwpv!jreItZ# z{17w^N&nbIVr8r2Xk_KU)9$9F>X7}v_OWjHcs20l=qrsUxCg@+4Zv;WSGEs;nZ};M zNJJ7St$d!Y7OG&l!;G&QV{Ki&mQ^jR6IOT101HdM2`d4E$)!HRwp2VTYho&|AR^Ri zBfC!yBvh12siXkq7XKDz)TwX!IHH6(pnL_VkM2713*>Y7mD_kDR#*m_{t^OSM@le7 zu=D5AUxx)U(-&{(@fLP<>o~MEImqMRy_oD?Bf_A5!r}!ww-jz&w|-PK%g7B4AxsX- zXIeLG;MjP!T+Yx1zl>8Dg5L%L4Ej`GU>=@^kLiBv8$bm{!uG0ujS5KTo5Jmr z64Nww{@x zMY^f)&VwknG$Drg6=W;RBc^4#?zfs36#&B;RSAT@$eDj>C)IfG7N-Q9>s8{{eai12 zz==SwPMPy-USOlM#HjVT3^Ek_Z9bP_!7jb(>Z*4#+@rZ0j`(%IX9tyL+Y@9w;s@c{ z7<<3(2f`=%fTb4+OlhbOo=ZpR6y2eLACQk2en&TZqw%GrUpkII66smdgx&xS&f)tD z9c>u^6Q-iIAE!mA4EU5jn-Ao{QDU$JD@~MPLLgj{M*Aaa+k3E7imBWczVZxkh>k0# zHd4NuSXTUZa27br5UY^nnU!*UY{@UnMQT*aTOjmjHA)IcRF}I`@k%yyZ2FdLGS}V; zwOs3O-LjGpWCT`S!3GJ|;cu^D9Uy-!Ebgl=0|Z1HV8qxFJ};0-XXIla>;;PbB#W9T zv8xCNjyBXQ0rL9goi<+kflF3iS50Em?D8ItbVv+obszd-XXUdny8j1idD&dG-WJHi+5Q zu4+IpK9l@SpBhU8J>(7I=C`^ZAge_T5suSPcwBE-;FEGDzlwhT7U^>Ucpc6t)pr+< z>Km*6_iE+lJb+$2cJe4$6y?@Jf3vUdwcMknV(!>bU$@$;lVX&EAWWhV?xX@X@uae} zXD4Y)VG4(fWp{EpsnSVfWhb+eReMI`x?ZD ze%l(E^Y^oqX`02u-;O`6z}rGM;KFf#KT3K7&j#pH!8=Qo2F*W?)fK&iq^3XqG3;n; z2{mc&fy~|B71sBl1z=A7-H{O`K>tkC&CnCvQPo2{14i^@I{$&Zc@YDC>Cy^D-ktm> zU!f;o>;e+R`#rIeV77w_-j18~7J7MM3s38g(O)<2KS}U#?-%klZz*xpz9D{mG6wS@ zyu*uaRGXZ$v^71DzIfmc%<F8^V@z&H`#Sf)nr{SG}62~5qeLA68i$9AB^VZW8i)shioTXH*-_i)I6k?O zjm}B~|5ia0BsUD{d5R`}f2*o08DLvkr|!{Q3%S5lCoUt9(7Juz3On_Iew%WTSZe56 za_R!bwBm8s1=51i4cvx|y1#;*@~3{ka*Y~!mP()9ki$8CbW zM6#dyu`(7^O%cpKtp+xHbKxjksEby2(xm!An^N`QR9Nc3Uf1{q`&Yac8eHS0{2mWS zN3Q!+fa03JyKN<+a(W!~Bfa{s4aW6ba-2qpa%#u> z?2D_>tE7ibsyKmGgr?TO(zgwb!uFjcxjYlW^VA(JJYrk|;_o3VyYTzX@Z#NGa!v!6 zLow22W26dGEfiMx-Zi-zENDO-Ta?#oP=ZIgDm(W5ih&X3$j^MWrUv(0)@|S!7V!3Y z4ZMNXbk%H4Q*owX1_(DDLF<++t9ni{3+m|-0)|=P`hg=7PYp3MT;M9y;F8~XKxSFv z2Y<*Nq@l5JRcz1_Wslk*^9gZKx0mGctGPdwoTcj9zqavh0k4=za*V?5xrB{pePjx! zQy#jIxeTe(i#D;{~GY4U6mH^B*t+5w!o9EV>xV#@r`P zl1%|lU~JD@6QCIqFeRoR{KdnF@PcMAt9I?T)adPmaZ+i^_gFIJk5Z~ogf`X!Vrb`8 zzqW8e+jd|@RneCx^0w4wN|kRIFwRasM4kP!E)Q^}8^9_Tz>^kJFq{s|pfc=(rIav6 z=zF2eP=p9jzV=jxw}h8!ItSj8L9kXdekr=%*?HfYugo(n7(Ow;&B;-Va_c$9k|H0`Pa@u6#b(&vhx3o>S1;zvZ!$&c=^{iKs^Lvj|`1fcV=NsI71xRz|THv>#)yLn%`%VM$!73b99O6NW180 zHR}fZe0=n9hYr6X$;%jLX>_0fHGS@f-t^c6KyfpMOf3ME)&fjpMxTaC(>}!rScU9* z(Ko7EdChbcy_*$6p&FFn&Saf|b9*(@zns)h0Q=6%=WPJz$gy*XNy^PZ^|jsv)J`KNhO`Vv5~KaFRDm$R*o$j-l8#ke`IJu&6e}9-tcCtuOwpel&`=ySMg4 zL9?GGKKZ7Hjmk5GkIa-db(Z}>Vsn#_Y>nSVzK^6{--0z!2PtcF?t|>iA?KMkdnrw^ zkc$FO2KAfgh?SS;$8^Dm?p^RfyOCJBA!+VN_=|uNO!`ds17Qa?ikZ8 zdrkaA*lA^!benX0kTfLE7@!5{DbHIU9x87EmpmH4DZ9McxOfCTMYH^qDjjUTzy?DA zi-N66ELQqRc1b$(D!DyGPCt zBK?pR)A)?ON zGlA(V9Lqw$?G8Q-df&&8^`Vp>#T5NVC5+ex zC1=;Vy&pm%YUGyUIxZI+5nVE!S!VGv5%kN=I8qGSkNNuBep9=YswDIxOkOGIk4NcxVmDn@=OER%@knRey zE+w%AYS2B&zZfW6csNG7?j!U4cF+qQNWHt%#rVU3ZU!zt#P0xtH%DB9E1jVg#O57& z^&|+|P*&Ckvw0tyMt5-hElC1!sq9QMfQbS|t=lbjWbRvmof%fRjyh-~`)MSh=rW$vc96Hq z&(G_Gt+?#bSg{gpCSm_w!q-U)?77R|p`A7lHZNC6eX*k$7IeL2@Tp+eL0v%NM2k}$ z6GW?HPU?5blaVNx|%;jWsazO!r)tPq?(03WCR=#lXJr#~I zfVD7*Ck4pi;hKH!3eZ8baE{7*mGy-#5ev3_xPo?HK*RMA7lzm?q9CUi&mVS+3wnFy zfI*ZKMcPTbI{&bVfvl&dBHY>Z=UiCE z2ho|C9n&uS5QZv0Ww_oG@R2(O1JOocwWl5{X8xI6Ty=^10&inT;wToMQ#a}^Ezra2 zJM9UzOfDTwyI*%L)}LFip*t0h+V@&lJm01>N?%P+DiAPrU(H^n7TvFk+#yJeoW%hh z{0Wng-?$?uO+|^mo!f-?eZ;_6SC?PDy|7Mq_5$)h#m5sU*KcVWboIvKdIZ;f@%47_ zRo9nx0#b{}^vzGn(@D_HYb2zUo2vK;IT&o6UMPZMlj%W_`mAnJ{@m03hrQH3&X*8? z2cQt4tEEekJLKB5ybVk#7MB@7t4orDEn-3|e7uL=2=RG*VR~Y$C3;5e$vFqloBEEtw+h#i8!X_#OKWjNW~W$&9}Q1MzL)H|=d_#NM>k$?Es zkKy@~D)%y3UtD1xQ4adve|@K8M)>LF)G*nX@$!yb-h=ESAo;*bRvi!~-Gc?}VHlhc zfC=q+BpMxSdlHH_{2(xb*rcMZ<-h~5A!3^=*^7LL#z*u}81t7^!td?Wtsk4_!9Kl8vK zMmaaRTAh*VEc?{mA6-YF3|9bs3~Xscs?jm2W~oaat{|(aB0f?T{*2#If?neqIMJ$R zRj7z^8^UUG`7q`rxiF&e@d-=|a>k|`Tr7O%R-1PX0`iSb45rSLxRyZ<`THgJC;KV{ zfagoeEdgs2{zW`|W8xpsl&d6JO}`nD@Km7Y1+^g&+tvgMYw(Ujkg?Qe?VOpo$+G6W z@9FBuayK6&5_~rOaa^V*5uZ^07&sCTLCAMC5XqXh6WOKw*PR~A6QNZ~x?J3a&-wTu zUri*k)=QN3N6B86Twh*7oh8)Zy-aHjT>+Wp_xlTaYBhJo$2Za!xgoB^u=!8f#>Nn3 za&a(2)G@VB|3eu$Hkx};GvlF6aO=W8&Yk+li{b0-aq`Mye=H&}Z zD|Wo4NAD&QQ?be|O|x(k6mxhJmDUr)evei8_GHNBG-AnnVq%m3xexc$r;W;JOBmeO z5>b|(K=@riDZ^2jJ6RRG>a0hGiC|`Orj1<%xnlrGW9IFnHb9AGu}Sywf_27(*0N2X z=@~{vI>9wu-~^U)tjUIhhG4@D?Q3zFy^{ybRn|!0?r;IeC!Ep}e0~2zW;tlv1t{DD zLUnObBM8t*E(X^LN&QeR6z;gA%9p-gm|v`KKm;aOEUYAHIA}(Rb;~=I1Vkkz3H+_siZ? z@Jp|T&|mv<4T$mTG=PN>r`=si(vE#`rtSLuIABXXW`3800-blkd2h&d9oZJ*2OX^u zw^~YkzRS1Q^-P3dP1}~rukHTezH@CGrk?UvA5}LmU=o;qzUl&$cBGEYAkv9i$1~>j z%kZRqs)_hCQ-zH%XeKN$sVs*) zenIZi{1HH*DsR^+1MK3a*+t4%OCs;b0G;lIfI$Q2_UE|~zDG)XeJNh4kH~9F^blXi_%}TPcU-CcxJJ8DX^Vgln*B`Q0gix&{m~Q_do+6p`Xdfuo%k^;;-IOcNz50c#(1#Z8I0eiYO2 zDa7kl=$izD_VxNKOoGGRApw`Xf|+zFR}!;fy`a~+RGAxYY7*Rgyrd}u1lMx%$+$xafMuLZ>M5ixKLFF?fh*sJJa=4LBtHJpK5zFaj`x*Wg zReNy_qi>p#FO;MUY5Y`!vh3EyVvQYw0oeO$bfX))K%=6XLp?j=G|;J^2yEB}73SQ6 ziI5p3NcrB7YoxYg$dliY{LZ6Bj}5;z(^UCtN$Ao(=nVAI&OYfMnEu&nN{%=*%h59R zp<@I^pD4O5mF4Rx_`Qynm&%Jr~=DH6En5R4sZF>X1BRLv=eFPXA+L zk})nVPy2PH`@=a;PF>-F#w8Z#CaA=wO5Z^IEeGfgxsSi|Q2@ccdE{?<1&(@?_`4j5 zT|Q!5xwJ`-CKXl5&)qN9N`n~TVo@?_5~j)#uqADavy-=)6mXK5qoZOIQuwA^ya^2l=-H2Ye$o&ba(-+o-p+4h)fEQ4b1j~@Km;h zJITZLkQG14jWTNYL641=O_=3aFK{?ufeO7fV0S(j;8DUXTr2E(oQe$19Q=aqQgiD! zz2e9_G)&$%Fe1?6myyiD4)i1IwBs3gz@YMjUcW|j|86<>nxAL_-jwUO=( z6KNYxTeS@0%ru@wDB_g%YnLw~Q$9w#^vgxYuS%XUGy4uDxOll;!RFCmtwSkH!3HjQ z688`{SM`bNI}kn%AGJ^129+5FOj-Xrb4DHKN9yrUGmQCU;`sXUo^~DT=Gyh7Fm!vab+KE}Cp;UdOI4uC$;%zrlfi=M0C#kF1Ig%t^Mq zEf~uqFxpo(TR?`|CD4TaoTW7CPu|oN+<4KEM+Py&5)ATvM%0(cxr*gkB{NgpiiS}j zBHdRN6BoeOw#`c%RaBgiCh)7?h0~Sh{7!s5-KOvyuQzO_nclcc&L_Urn$1Qatl9!uVSWeTQZ2TUewv z$VhS$rW~g@B1D)OnF6ec19RX|*w}6xq+aD2h&-9gKS8-~y32?`D;;ABj+NtUX_b%h z`q(lJ5auxM5`U%j_sejHujpP6{Y+rm-CvM~xXQKTUoi4!Dv(_pX>K@QWqp;q#|_qt zo`JMFSwb>tfxbc@@d$y-wGf=cPy@<`ElC9u~<0pECrXj)(zX`CWal-{If5xoVl&Cj;->%dH1YyVe9 z;IlNqYiU6*Rx=pk zwsZ^cO^+_89ucWBC2{S)JO3>BrP$bvC>pdsw)z9_Ug~{gNph=qPsb-Sj_dAU@5a@Nqm91}yPQioA$(YvUSDZj#m*%C`rVqswq!N& zwdnHmrgR@>Shn2YIbgn)zW@yS{2M2vjZMh!PJq73JdNvGq7~eXj>y@MhCpP>a&9AN z7#OWbw%Ww+!1-r}N%0$DL;QOBrLNw3t9JEXb_~gG9nCug;GBO?eTtg+#J+JWyGkvL zK>6azf|4>IYXgYdaatct^0!@!EC-SafJWjcX-*O_t@tU*kNkS1ovQ?%Nch{vQ_0Rg zHt@sZP(#vOkC*s7nFV)wL;irx^q@N~EY(!A)H(3Zgvf9>7#s^tm>9C2+Y?JF|U-ntGHFaLWVwz3j_qdQ&^ zRAByR6C!yL3038ra5Zz&KRX{hD2HiKyxT8yg+SjDHtugx(ug-fm+b&&1$E9sFVKQM zvcag)v}9f27IM&FtZU{~%ndZ+f+E!9HuMf$c|!Ak#b9A%;RgFzAKI|Fe&ETXV00Wl z4FS@I?~O$_Xr*ub@8S3e4g#G>Np`CQUlH*98S#U7FQyM{%Z>-x9l_*dublgJQGpph z`k)nl(D#{@8u*G-9iUy?EcYp!!SC~{b)T0HbhRk7+n3B5O~sE*|I z7kyj-WPfK4xj-j<1J?kAEc^If!U03Z8D3vDmWyUPcMfF!FpxrhT=OaA)JM@?$?0*v z^6j9?Z-Y_8f!2~tJgr)oZO}+2z zi^s(P=Nht1l6n7T`s6#A?StzDBOznY+u9LX5gyJ3M}X~px+j?Nq^U< zm8;$G#y9IEDIEhsP07jVm#*+D{Es3pzf@F_FjH>ouPRuMUxlFcTLZ&hkn^~00CexT z*{!88QQ`vwH)(nmG%=|yFXE(tl*6;iT;BFr39| zDFZEfj05+m!anemf>)q`#XOxLOLLlpEl#4e)%SZYcB$0E6IcuY3exeXC`2zAi5ZzQ z**f>BT3aLY(@HRH%27eZbY)Jqi9k;qhwO)Fj3Esx+2Mu%zJPE7^jc7aC#WVgVZ88o ze`GSYAjRHz;P-mnos4zhi@=(rf6L7Ke_yP;mlJ8ip*14Y85L#wXd7}anH92Fs^G6a ziyG|XkHE(4X~mmk75;`_rVM?QD--YecV-d9V2sznor+j(>m zcsF-ei22^VkVgotV>F_~a^;pT+jnGxBN{lMSg7Fgo~uXdx&DT-K%)b_V~7Nd@^&od zjY_2eL<2YZ0NuL#5-f@G#*16yUTv@ak4+P@#bPs-n1~`m@hZ}qm3hhMLBGk-#N&$l z-zv|@{o&J_X1=h_08#0L!9r5XTLfR8<78$w`7gu#fT`S?FkE$HIvS?}r09$LapN0t zR02;=%*QaiQhu;Mdfg7mCT8W3fZA<{gMM{6qxAaigU+BcUP%?k1`@yn`(;S#B%>o> zTd|D0tc-c)Svujiqrw%0hq}V>sg<6d;Ong=scd{mBjuvy83W%^yLK$m>-41r=Dh-@ zC=92>VR$JK?WGrCHSjA%v@3op==H+vpY;=WFeS$^nO}1vI}m?Z(5CJJ$-&!2pa_QV zjcaXQfP4Udw2p$ZZbHPudXcg3TB3?@AucVkx5V2~@|!B2H3bV+#F61XAI+W#m#C`h zBbFZMQu&f)EDM}AmQ}ZXuaV>cbU)981|W%?MufedXJQgi>u=HM{gjQgZHS^-YqYoB z7^rf}e-5BV;AOqO0d?@^B|QO@YK6BV5OcH$V4Q?a_S`bdwu6*g+0$F z)J}QN!c32M^Zk8G1-t;jpcWNKksk#F4znqdc~?|i1~jj8f-XTw*2O4Hr%@r^f@lKA zbfHva$Y+%4KP^&)pP2?Wg3}pN-I|Z4pk6Pvc2qHO07{Z+z@ZR^gddC_BiwYb_JG8m zU%58ofQk;vFD<<0lb@ae!e+C&KX@|X5rjC_Y`{4yDa3i%4*tWPAKOHj!ZOq`gd3uG zFZJP;-L4@n)YOWS=@_?}g#F;$lUn1cJxV75uc7wuO#ftb%*rRcQToVY4sQW4@vRXv z&34>RSE0#X9eUW;?{hmBfb6TjjHW*2;M`C|iapkpN1r+kf7eos2w5SQfPUNTB1AsX zo7GHTkwk+;@_SFhCGoEJC)9*kE2oFciu~UfboB-`StR+up%7(EM^d3-4Bgbth|pfXTWs z2VBmRsuWX}>&FW_Kuh)Ds(4Pgr`e!-b~@X`n9%G`&0T5AbC(wT3X;!sf0n6fk@ zSuH&b90B754?3`tT(VHP6xT8v58|*$_(h7O40jEJ7DFkJ5|i`kXy(9=^M1WvRtryD zSdz#W*rc*1{4J;q;2nI4-p7-Fw~+^BF8Sc&xl9~}8O&o|@!qQ_2+TxDrRxzhkrY6V z{iO)-Ue|w!&E~fyo>@5)dt=X6MH*jN-;(pVDixmtY|a2ZaZ`blIQ03c8`keqd7apc zazJ{$8PcTQlOKS+W;SvtD9HpVZTcFM`F42hK1+NutbGgU;2-L7`TX6~pe(JsRPa!T zGC7fYGJ=f{zt^%VZ?^XEBY*^FYxG|dcEjw%)UkwSrtnj$S>t?U@^nY3&e)+j$T^|P zTr*!dj>vHXTi^gZsQL<@uGbSqWA`%GH{ch1+lUDyDI%Fs%{p8B8OB&qSv0r_R#P_U zHse!~H14|}Z@nVIFkwWOE5Vs8*X?;Iu>43l@+x}^HYm8EFpM>vXE6y0V-N~sY$|Ef z)KBs*B;r+mzWK=pCVz*bCS5k$wmbi66>(<^n|%Xv?SrOOU^$R|Em&54^S_z^$|=j|8p0PZP?Hx@ z|LL5m9Jjt8nTVMVfmar13Yd5b2z`GqlE{bmG+(zkK*W5VQI}+gCJhZjq`|sI>zG0Q zdyIi4>lLwT?bTP#@@jm?3JB(Outpy_%C|D%h(HYaC4Kf zy{dX!z;m8DI8?}tHN+3kmJr?6=6W(BZD$y!H@7JC@VrSkwDk&Cvmr~X>ccpIEeN%(b3l@S?N+Yu{B`baQ0ul?an#zpp_%UW0_ zFWSV@e8fLMrv$M8rNr`t8J2^IQ-Vthf@!`}d75kMRK9l?g@G!0t4Nks$?X^D5C8Dx zdKot8o8(em)W=5pc-^SsZM=-CXL**H5|n-CUT92fN!NpAjo>U+DFv{j=wx(tw)X zMn-pHKS&fAh>Uj}cP+SSHW)auxIu&4*ErSv8P`X|DrN%(juSHs?yVJl5$fIy1GZ}I zZyfQ(yec_xLK)dlYO$`L(#t5!p-aHX_L+4;XT(M2hw(kD0KXfTadG}-dAhL1R@sce znV5?<-lrT;SfXqfG^>Q}C-q$14*oicxc{mZ3XI1r`90nFzFu*(I>Wsc<(q{RZMqogvvQ^e zka1PC?xtCoUnJzaaK&Ix$0b~eZvzOup}L17=r4E#ON=zL;Cy$EgD}921(YD1(dFOp z`?lfVSIFxh<{e`?;L2R+U#SfPm%MWEQB(Z~t=KO5V0vZGc}6{&sj!7dxaG6##U0=uf$-dYs9*fjlq9Y-KP^%Ugg`;S#Z`*+& zLf(>dv-j+J=0F>i+cLYrxcuuGLyv2f=hYbo;0J&}4gq~xw*9a&JRUqmhY>wpVG#=( zpt0)Uh$)s2C+s&!Fkf#Qr?2gYc-LOhufgr>nmx2NuQH&OwoFVJpGg3n^;rPkA{J8Y z3Qln3((56u`;~rvxagB7g-9#|;|@Dh`Y`YOp%2i3Vr8-n@RI>4S9*Yi5g7r6<{Y;B zOL6eYf}K)9eUFoR!N1dm8M}k8d8d^2TOBO?w5S2WVOtn`w1V(cDaJ!F5XzZ5XJ3IiBs*fgDQaE}!Ow^pUio){d=hmv^$ zn^MOI)(T1DOFcpcFob1n#ZxxE+E&??x^|Ni6-bnRsxh(^3= z0FO9*Icac5BAxa5_!y%sUDVn9nO-sEwW|9Rn`<4Hy}}z8A@PAJN#y73t&x3-0quQ1@*aNKZqrHT;BQi4VzRCUXd70I&%LyGl z=hF}dzOckc37Qf}&jvo=YN*LS=rq-BXztj&OMvS#^8rds zx?b-enu*?b7z+4zVD%j|FWeg=S=C=@B3_cA)?8^!wqgXE3`8ypWk_Zsf`-T?5&dk%GeOFB8Hv?2@%d*nN~oV z;#tGX!NQP{PvSGtJ8-R-fVBY^o1!HC9nuXjMH#W#foakO%k)G^M3(&= ziANOaYVgRR;pUR8O^TljHM9P9XP5v9t`2K)VPW2I_+ml~p8K7JY1Dj7Wv6Yax6dk= z`-cL!`=)*>O?2W$c)kqTzIpS_8{1!gu!n7;)`P1w&GLSY3l_8Mna~phO9Qo^HZJTf zA!A@yPDdM&cSAFh_=P~(q+zN4G9f)Ogr&%Pqe=*@LbrjQXTvNYL(X|0Dbe*ibpsrngF{QvIlPppj#uNtq)q=3y8+q zgN=qX9yf*nwVGN!UjN=6CahZe9ZZG^GVX+Drht{&B@BM9|w<$M@gWv2r*i~)tldQGm2ZE70i=A}xy z8*!SXG2h)QmH-)LxrTcrVLrjYU{yg14=DwGl_BK7?GT^y$)v+3{SI5)A<&gzpnUpXrsNWNS6_Tn zOx=bJfhzH*t8kv`%Y*yaMh&?W`l&}9X-#mJ1Io_X9P+NfWg=$MjH?)Q-11{3gbh;+ zM(C41nc)q=)krhBm0Bk&s}HSITZkOf$^6E2u|eTxo=1#v zG+T>=%7hH~38Cyh04J%pGAv1BPsuYT8bTpDPr8&Bq-s^=N3o}~lCt8lO2t~O3<^U* zjr-#iE^f6Ut0Z+kK@u%1ic)&$-V$;*}O-~IVv%xUkNafqiyzq+}Gr>`2 z!!k2)7<=GRpxZ5`xonk%NctXPJS@OTvn=>{lUW58YEu;a+ceAd7A>at#x-7!op;$6 z-OuV(_>ts_!Fl41q=R=-I5>ux8B%^Uk*mlrSdf0c+{{;90cD?dDke!KLiG~!M!#!P zt5aY;>~LB_47fG<5n^-^ek;#zNO<-EgZ2$Z&krgv+CUTqF9>nbbfR#P52AgU*$s59 zH}iV96+_I2)(4HOON6fFG;VQ$QMv3P1jmhCpcLrt!xO%MUA}`=iK;rn(rLX^OBEbJ z(NPAO0?q^r7;&kqynyk6g1!OBTn=GAp8OfQCijDhVAtMaXI9Uv2RK#v3%;Gytc_X~ zNbxNnj|A1MQOVa3{a_E~x)36{%4*c7iJZI9znl_93Sg%W`YN4C$|MqDqhA4IW(WM} zo_K-Pp3o3626R}b7AlvV?~ckFnP_gmMfO;=8A}y!NqFKS)9`PWOp&3Jmudt|0%Mrt zfP(_-qQua{J(wSmwZPuJvpB(e=n5*nnOr0Btlw#h%mJ7sFRNRq!A&o!F@V=I&l95< zzVzn0R9j=zKSz`J>bB6J|`=2FEyEtcKi*e%f!~k2j~> z)AsgLWMZ?=zwd)Xeemk{73SW|}<_ zBLW{edYsGKrjWN2n?GNSw)+yO;`r{mTb?1_D@Qk9BB8v#s2Y>bVXrB05I7}kcBnlr z%t@Kfe?MkS=x)~pmm-|;mZ)b+wXC2G6LJ!ya>}3zs-Ad?Y5YBUZ}D)3Mf(gFb0L(O z{4`RTHXv{{wubO<-k7!Ud8x&9)uKuKeW^9-5dyKOhp%3w6GIRrQS6!AE^6L)%b5hK zlT?NvFx%z<|iDpI08l z=+l2a15`D^4}Tg%uRo>d-v~dxh)jx`f#BE{tWkY-p)2*w{WpWYeL>PNcj^2#(qFEl|wT|5y@N`kU?3Z_7m#ZY!()4 zv0MI})bpSL04!Xkr9p6S+HaN_KXQBkZ+9gb$&Qx&K9_(KBoBaqh{!YiV42#?UIb19 zotrH#iyt(|NwK=pMcx6zktf)S(}!HBnjZru-wh9f== z&OHJ~OcM)NBe;E8nhioo(;Mx#kUwC*su{CO#ZRcez|}x)IUo0tKDQ#-vTs}*oZhXV z(Y$*1Lt&l_sKvW-mrdR7gyIfY%Ir%QwSg(qtB@&s-_WL9T83MQC@a zm|r|J10VV~!U9|`s(72zEd1(B_%(sW_?SSHXMLb5D7r7qD$8ImtZlOus#Wb}4hOHG zw{S}n?x!ohebQ40Ue2?4=-tu1bF(VZ!a!^0TqpSUjU=$4#bLu9e__T=`-2`93mUn@ zDN_n~5u5mOTg!YeZNMwtS-X%S6dfWn3(&U@WW4S`2tFISHK751; z4er@3$=%U1JEwC|@p_f;&`CKg|t*gjUB41h=zH%r1J!&~? zA!+#{^Sc&33FW$1T^sP&d`3M0C)8`6D1@UbQ3vK7jOq~6^?S`GNE9v(c$3RjG!Ijy`WvGNFe%t=3hc!VKPB2;6|r@5#+@`e9OiTc_R1w2>)81`^aGG7h<<4d<*E~1O zYa)XZ5a-?*O_|acb(GhLSB@YHuS-_e9~@j7pDW;umRohVRPO~>{Z)sp;8^T(@5G(I zpBQQK=*8|vbG~BW17YE-?uP1q2xFvY_&O;;D8HelQ$j7dr2fwGrIlzFT>e-@mnLP#=b0`xpS#wj8AlV9s7R9*X&I2fB#{ z@@r-f-(CQ;n#|F|jGGak7w4;|Uae6Ab0i@`*RXQJ>)Tk;aQ zF`XAznFECR$iio}`bw&&pKuig8O@l(qV(r_e)=2pH$Dbz6@tw*q2EPLZ)ZOh#l@%f z-GPmr_G~bNNG!E{LXi5>olejopIUk^6GYeC`xo;SpuH|0Gu8HjDm+=jpc1NZ#pL9| zKFhq?;n4#Ao6#iH-g1@%r@R4Yajn0Jwo14NI0^((uD*KUc82= zc_i{AI6H^PYyyFqrdr33C3VO%vvQkD4D7Ay^DayGNZ-;IH);-w4+t%7A$7+M%O0dRPuF=m_g?S06xq1${uJ-t+v znG{~7HVumy*z#iW{j^i^!LKod|4L04H#z=j(_vNGMM}-~*%-j$IV1xc>WRFGqq_tm zyyb7_5DIIZQWAEQV~bG19>YoPA45A^o=JfVnR-Ohz4G6$b06;r>EL+`i+l7*`e`!U zK~A@V_CZrX3Cr_hLKl4!xA~dh#5{V~{zZ{;kGQUR+o}0V3h$RVx8hOpiDT;hAnSU8 z$ZrJ!KMP;1;XdPJjXy@*U%duN!^>}26=-@~ELoV*)akT25)FY9rfJ9Ys^X^4V<~@- z?hxSuL~$70ehPVF4FDd>~J%o~G)V%$U@FM|`uehHqD-*?fXe8BSI@G1`M)YB3Mg-<( zlM=7*MdH5Fx-EODi?|@}%P^3;H(=yble2miP}kNF%*D_^F4@uSB`WRe^? zjiOD?L!mcB-;IjDZ|STJ)6nNP8LAEhz~Rpf|LA&>FeMj$L4BB6+TnOSe8$$pN6W^7 zXfNxaZmn$bXGGC5CJu$c91u%h?RrTuw49Q6`*EHLo?o#Mb7eMATW0j2;`h{lfgx2- z{=n=GJX;EFJ4qPb>*5C93sd~fd)K|^lbi&4;Szi}SLx=K=dW+CIZq%PhisDr8X+$v2u6UB~vt&}xh-E9;0Z5jFmnbAGYtpZY9sEA^7Rlc$D$dI2Z3`hqbwoF7vs1aAzz?2bbSPgJaz>|a~F2nR4v zObKFnACR30n1(v^gM=@~qv@qhp~5SC{^brtmlH4;XtD`LB)Ahe1A!1lWj2(YeiX)E zC|_@INPO9{yvgtZ?GyaNPw8NJVhG>6Spk$TN&FRjY$gcrU>16Z13PH3<7T_}0AGv& zV!c{^qU8IaqGq0GUU0Er1Z1{1U1faQ?A*l4;ss$`5BSS4EoO}EPxujo_(UB zo-O2O(@jKld$Y*1JA`~pfNHPwd)@dGrONTSG=lI}$=`b#dfv>(FuMK?7u6X`A-sAV zkB}}{Y!5xMYuA*bw3Q;c7|8Tj5642yyJth1aeI}7y=bt}m?zl-E<^!WSq8^Vo*Wm-^@Wd&j8fkE^*v8pD~r*}2}+tKS}M5L+j z+6DRWuYsd-$0Fv3RENmY!*r9Rr7z0EWKmw)LT$Z!5I`#`;qk*5-Lwf20j+bB0!L0< z>fn|IaOPmigKBo3AgHALfEj(tPNuwa0Yxaq+JFP;3C%<5rq{(dV|uc-XD542y6v4- z;|1d!ql?XsBHNZC3oM7~%h~p%nD*l-6yyBH95fMi*8M$yGIB<3mn;9w;COAzGWjs# zv}Z12EgPIJgx%K+;IBwk3S-X#cfA4DU5FJcG7D+HiX_M1xDQLVVRoM>=&b`W>2ZvZ zwDt<5@I`D~+^5zDzVmgbo{(^PyEIYqz*n!enJF|bM)0pdVo=Y+z}Z8A9%WsrDbp}w z4!C9t&XZ#TJGC$5(ii^C&0ot4D{0d!jwK!?w#cgp108;GdS5W-Ow!h9Dkj6TRFdvk zd?t=MA)2208rg$-JBkx?3{maV)&4^UPYP~$NnX@*h~4j35~RqGy9dS|!;bj1oOX1qWWvZ{<_MA3DZ z_PQQzPuyg6Q)yJ#y$JCSra298ClS=bqr?PP`sOI2g4LZr?!=kW3wa-JIQsqy;3a

8OH0IZb}_6?7e-_O%EzM_ed7Kl(7g-@z6$_ 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

public static int FileCount => Internal.GetFileCount(); - /// - /// Get a list of filenames synchronized by Steam Cloud - /// - public static IEnumerable Files + public struct RemoteFile { - get + public string Filename; + public int Size; + + public bool Delete() { - int _ = 0; - for( int i=0; i + /// Get a list of filenames synchronized by Steam Cloud + ///
+ public static List Files + { + get + { + var ret = new List(); + int count = FileCount; + for( int i=0; i SubmitAsync( IProgress progress = null ) { var result = default( PublishResult ); diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs index 7393a2401..88be2106d 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs @@ -664,7 +664,7 @@ namespace FarseerPhysics.Dynamics } } - if (Enabled) + if (Enabled && World != null) { IBroadPhase broadPhase = World.ContactManager.BroadPhase; fixture.DestroyProxies(broadPhase); @@ -677,7 +677,7 @@ namespace FarseerPhysics.Dynamics ((PolygonShape)fixture.Shape).Vertices.AttachedToBody = false; #endif - if (World.FixtureRemoved != null) + if (World?.FixtureRemoved != null) World.FixtureRemoved(World, this, fixture); ResetMassData(); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsDevice.DirectX.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsDevice.DirectX.cs index c5fef7f3d..0812ff629 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsDevice.DirectX.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsDevice.DirectX.cs @@ -1382,13 +1382,13 @@ namespace Microsoft.Xna.Framework.Graphics private void PlatformDrawIndexedPrimitives(PrimitiveType primitiveType, int baseVertex, int startIndex, int primitiveCount) { + var indexCount = GetElementCountArray(primitiveType, primitiveCount); + lock (_d3dContext) { ApplyState(true); _d3dContext.InputAssembler.PrimitiveTopology = ToPrimitiveTopology(primitiveType); - - var indexCount = GetElementCountArray(primitiveType, primitiveCount); _d3dContext.DrawIndexed(indexCount, startIndex, baseVertex); } } diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SamplerStateCollection.DirectX.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SamplerStateCollection.DirectX.cs index 868b9cf77..bf869dfb2 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SamplerStateCollection.DirectX.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SamplerStateCollection.DirectX.cs @@ -8,8 +8,18 @@ namespace Microsoft.Xna.Framework.Graphics { public sealed partial class SamplerStateCollection { + private int _d3dMaxDirty; private int _d3dDirty; + partial void CalculateMaxDirty() + { + _d3dMaxDirty = 0; + for (var i = 0; i < _actualSamplers.Length; i++) + { + _d3dMaxDirty |= 1 << i; + } + } + private void PlatformSetSamplerState(int index) { _d3dDirty |= 1 << index; @@ -17,12 +27,12 @@ namespace Microsoft.Xna.Framework.Graphics private void PlatformClear() { - _d3dDirty = int.MaxValue; + _d3dDirty = _d3dMaxDirty; } private void PlatformDirty() { - _d3dDirty = int.MaxValue; + _d3dDirty = _d3dMaxDirty; } internal void PlatformSetSamplers(GraphicsDevice device) @@ -60,7 +70,8 @@ namespace Microsoft.Xna.Framework.Graphics break; } - _d3dDirty = 0; + if (_d3dDirty != 0) { throw new System.Exception($"SamplerStateCollection still dirty ({_d3dDirty})"); } + //_d3dDirty = 0; } } } diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SamplerStateCollection.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SamplerStateCollection.cs index 20826622a..d90a5ab91 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SamplerStateCollection.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SamplerStateCollection.cs @@ -23,6 +23,8 @@ namespace Microsoft.Xna.Framework.Graphics private readonly SamplerState[] _actualSamplers; private readonly bool _applyToVertexStage; + partial void CalculateMaxDirty(); + internal SamplerStateCollection(GraphicsDevice device, int maxSamplers, bool applyToVertexStage) { _graphicsDevice = device; @@ -38,7 +40,8 @@ namespace Microsoft.Xna.Framework.Graphics _actualSamplers = new SamplerState[maxSamplers]; _applyToVertexStage = applyToVertexStage; - Clear(); + CalculateMaxDirty(); + Clear(); } public SamplerState this [int index] diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs index f32bad148..4b12f5c1f 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs @@ -1258,7 +1258,7 @@ namespace Microsoft.Xna.Framework.Graphics } } } - _batcher.Dispose(); + //_batcher.Dispose(); base.Dispose(disposing); } } diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs index 25814aece..036fd783c 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs @@ -13,8 +13,8 @@ namespace Microsoft.Xna.Framework.Graphics /// batched and will process them into short.MaxValue groups (strided by 6 for the number of vertices /// sent to the GPU). /// - internal class SpriteBatcher : IDisposable - { + internal class SpriteBatcher + { /* * Note that this class is fundamental to high performance for SpriteBatch games. Please exercise * caution when making changes to this class. @@ -41,7 +41,7 @@ namespace Microsoft.Xna.Framework.Graphics /// Index pointer to the next available SpriteBatchItem in _batchItemList. /// private int _batchItemCount; - + /// /// The target graphics device. /// @@ -54,21 +54,18 @@ namespace Microsoft.Xna.Framework.Graphics private VertexPositionColorTexture[] _vertexArray; - private VertexBuffer vertexBuffer; - private IndexBuffer indexBuffer; - - public SpriteBatcher (GraphicsDevice device) - { + public SpriteBatcher(GraphicsDevice device) + { _device = device; - _batchItemList = new SpriteBatchItem[InitialBatchSize]; + _batchItemList = new SpriteBatchItem[InitialBatchSize]; _batchItemCount = 0; for (int i = 0; i < InitialBatchSize; i++) _batchItemList[i] = new SpriteBatchItem(); EnsureArrayCapacity(InitialBatchSize); - } + } /// /// Reuse a previously allocated SpriteBatchItem from the item pool. @@ -80,11 +77,11 @@ namespace Microsoft.Xna.Framework.Graphics if (_batchItemCount >= _batchItemList.Length) { var oldSize = _batchItemList.Length; - var newSize = oldSize + oldSize/2; // grow by x1.5 + var newSize = oldSize + oldSize / 2; // grow by x1.5 newSize = (newSize + 63) & (~63); // grow in chunks of 64. Array.Resize(ref _batchItemList, newSize); - for(int i=oldSize; i /// Sorts the batch items and then groups batch drawing into maximal allowed batch sets that do not /// overflow the 16 bit array indices for vertices. @@ -154,36 +145,36 @@ namespace Microsoft.Xna.Framework.Graphics /// The type of depth sorting desired for the rendering. /// The custom effect to apply to the drawn geometry public unsafe void DrawBatch(SpriteSortMode sortMode, Effect effect) - { + { if (effect != null && effect.IsDisposed) throw new ObjectDisposedException("effect"); - // nothing to do + // nothing to do if (_batchItemCount == 0) - return; - - // sort the batch items - switch ( sortMode ) - { - case SpriteSortMode.Texture : - case SpriteSortMode.FrontToBack : - case SpriteSortMode.BackToFront : - Array.Sort(_batchItemList, 0, _batchItemCount); - break; - } + return; + + // sort the batch items + switch (sortMode) + { + case SpriteSortMode.Texture: + case SpriteSortMode.FrontToBack: + case SpriteSortMode.BackToFront: + Array.Sort(_batchItemList, 0, _batchItemCount); + break; + } // Determine how many iterations through the drawing code we need to make int batchIndex = 0; int batchCount = _batchItemCount; - + unchecked { _device._graphicsMetrics._spriteCount += batchCount; } // Iterate through the batches, doing short.MaxValue sets of vertices only. - while(batchCount > 0) + while (batchCount > 0) { // setup the vertexArray array var startIndex = 0; @@ -217,10 +208,10 @@ namespace Microsoft.Xna.Framework.Graphics } // store the SpriteBatchItem data in our vertexArray - *(vertexArrayPtr+0) = item.vertexTL; - *(vertexArrayPtr+1) = item.vertexTR; - *(vertexArrayPtr+2) = item.vertexBL; - *(vertexArrayPtr+3) = item.vertexBR; + *(vertexArrayPtr + 0) = item.vertexTL; + *(vertexArrayPtr + 1) = item.vertexTR; + *(vertexArrayPtr + 2) = item.vertexBL; + *(vertexArrayPtr + 3) = item.vertexBR; // Release the texture. item.Texture = null; @@ -234,7 +225,6 @@ namespace Microsoft.Xna.Framework.Graphics } // return items to the pool. _batchItemCount = 0; - _device.Textures[0] = null; } /// @@ -251,7 +241,6 @@ namespace Microsoft.Xna.Framework.Graphics var vertexCount = end - start; - _device.Indices = indexBuffer; // If the effect is not null, then apply each pass and render the geometry if (effect != null) { @@ -263,27 +252,31 @@ namespace Microsoft.Xna.Framework.Graphics // Whatever happens in pass.Apply, make sure the texture being drawn // ends up in Textures[0]. _device.Textures[0] = texture; - vertexBuffer.SetData(_vertexArray, start, vertexCount); - _device.SetVertexBuffer(vertexBuffer); - _device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, (vertexCount / 4) * 2); + + _device.DrawUserIndexedPrimitives( + PrimitiveType.TriangleList, + _vertexArray, + 0, + vertexCount, + _index, + 0, + (vertexCount / 4) * 2, + VertexPositionColorTexture.VertexDeclaration); } } else { // If no custom effect is defined, then simply render. - vertexBuffer.SetData(_vertexArray, start, vertexCount); - _device.SetVertexBuffer(vertexBuffer); - _device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, (vertexCount / 4) * 2); + _device.DrawUserIndexedPrimitives( + PrimitiveType.TriangleList, + _vertexArray, + 0, + vertexCount, + _index, + 0, + (vertexCount / 4) * 2, + VertexPositionColorTexture.VertexDeclaration); } - - _device.Indices = null; } - - public void Dispose() - { - indexBuffer?.Dispose(); - vertexBuffer?.Dispose(); - } - } + } } - diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/TextureCollection.DirectX.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/TextureCollection.DirectX.cs index 6cd68880c..ae0350aec 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/TextureCollection.DirectX.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/TextureCollection.DirectX.cs @@ -16,13 +16,15 @@ namespace Microsoft.Xna.Framework.Graphics return; if (_applyToVertexStage) - ClearTargets(targets, device._d3dContext.VertexShader); + ClearTargets(targets, device, device._d3dContext.VertexShader); else - ClearTargets(targets, device._d3dContext.PixelShader); + ClearTargets(targets, device, device._d3dContext.PixelShader); } - private void ClearTargets(RenderTargetBinding[] targets, SharpDX.Direct3D11.CommonShaderStage shaderStage) + private void ClearTargets(RenderTargetBinding[] targets, GraphicsDevice device, SharpDX.Direct3D11.CommonShaderStage shaderStage) { + PlatformSetTextures(device); + // NOTE: We make the assumption here that the caller has // locked the d3dContext for us to use. @@ -92,7 +94,8 @@ namespace Microsoft.Xna.Framework.Graphics break; } - _dirty = 0; + if (_dirty != 0) { throw new System.Exception($"TextureCollection still dirty ({_dirty})"); } + //_dirty = 0; } } } diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/TextureCollection.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/TextureCollection.cs index 25aeab3dc..096c432de 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/TextureCollection.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/TextureCollection.cs @@ -12,13 +12,18 @@ namespace Microsoft.Xna.Framework.Graphics private readonly Texture[] _textures; private readonly bool _applyToVertexStage; private int _dirty; + private int _dirtyMax; internal TextureCollection(GraphicsDevice graphicsDevice, int maxTextures, bool applyToVertexStage) { _graphicsDevice = graphicsDevice; _textures = new Texture[maxTextures]; _applyToVertexStage = applyToVertexStage; - _dirty = int.MaxValue; + for (int i=0;i @@ -55,7 +60,7 @@ namespace Microsoft.Xna.Framework.Graphics /// internal void Dirty() { - _dirty = int.MaxValue; + _dirty = _dirtyMax; } internal void SetTextures(GraphicsDevice device)