From 6e6c17e1003f07bd5a2bc190991724a5d0184d6c Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Tue, 22 Oct 2024 17:29:04 +0300 Subject: [PATCH] v1.6.17.0 (Unto the Breach update) --- .../Characters/AI/EnemyAIController.cs | 6 +- .../ClientSource/Characters/Character.cs | 82 +- .../ClientSource/Characters/CharacterHUD.cs | 2 +- .../ClientSource/Characters/CharacterInfo.cs | 21 +- .../Characters/CharacterNetworking.cs | 40 +- .../Characters/Health/CharacterHealth.cs | 65 +- .../ClientSource/Characters/Jobs/JobPrefab.cs | 102 +- .../ClientSource/Characters/Limb.cs | 24 +- .../CircuitBox/CircuitBoxComponent.cs | 2 +- .../ClientSource/CircuitBox/CircuitBoxUI.cs | 3 +- .../ContentPackage/ModProject.cs | 4 +- .../Transition/LegacySteamUgcTransition.cs | 2 +- .../ClientSource/DebugConsole.cs | 243 ++- .../ClientSource/Events/EventManager.cs | 27 + .../Missions/AbandonedOutpostMission.cs | 9 +- .../Events/Missions/CombatMission.cs | 100 +- .../ClientSource/GUI/DeathPrompt.cs | 2 +- .../ClientSource/GUI/FileSelection.cs | 55 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 17 +- .../ClientSource/GUI/GUIButton.cs | 80 +- .../ClientSource/GUI/GUIDropDown.cs | 48 +- .../ClientSource/GUI/GUIListBox.cs | 27 +- .../ClientSource/GUI/GUIStyle.cs | 12 +- .../ClientSource/GUI/SubmarineSelection.cs | 4 +- .../ClientSource/GUI/TabMenu.cs | 135 +- .../ClientSource/GUI/TalentMenu.cs | 189 ++- .../BarotraumaClient/ClientSource/GameMain.cs | 4 +- .../ClientSource/GameSession/CrewManager.cs | 79 +- .../GameSession/GameModes/CampaignMode.cs | 4 +- .../GameModes/MultiPlayerCampaign.cs | 4 +- .../GameModes/SinglePlayerCampaign.cs | 8 +- .../GameSession/GameModes/TestGameMode.cs | 37 +- .../GameModes/Tutorials/Tutorial.cs | 4 +- .../ClientSource/GameSession/GameSession.cs | 6 +- .../ClientSource/GameSession/HintManager.cs | 2 +- .../ClientSource/GameSession/PvPMode.cs | 84 + .../ClientSource/GameSession/RoundSummary.cs | 109 +- .../Items/Components/GeneticMaterial.cs | 31 +- .../Items/Components/Holdable/RangedWeapon.cs | 2 + .../Items/Components/Holdable/Sprayer.cs | 1 + .../Items/Components/ItemComponent.cs | 63 +- .../Items/Components/ItemContainer.cs | 76 +- .../Items/Components/ItemLabel.cs | 2 +- .../Items/Components/Machines/Controller.cs | 10 + .../Components/Machines/Deconstructor.cs | 25 +- .../Items/Components/Machines/MiniMap.cs | 169 +- .../Items/Components/Machines/Sonar.cs | 87 +- .../Items/Components/Machines/Steering.cs | 8 +- .../Items/Components/RemoteController.cs | 1 + .../Items/Components/Repairable.cs | 2 + .../ClientSource/Items/Components/Rope.cs | 6 +- .../Items/Components/Signal/ButtonTerminal.cs | 17 +- .../Items/Components/Signal/CircuitBox.cs | 20 +- .../Components/Signal/CustomInterface.cs | 146 +- .../Items/Components/Signal/Wire.cs | 2 +- .../Items/Components/StatusHUD.cs | 67 +- .../Items/Components/TriggerComponent.cs | 18 +- .../ClientSource/Items/Components/Turret.cs | 15 +- .../ClientSource/Items/Inventory.cs | 2 +- .../ClientSource/Items/Item.cs | 66 +- .../BarotraumaClient/ClientSource/Map/Gap.cs | 3 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 27 +- .../ClientSource/Map/Levels/Level.cs | 79 +- .../ClientSource/Map/Levels/LevelRenderer.cs | 15 +- .../ClientSource/Map/Levels/LevelWall.cs | 22 +- .../ClientSource/Map/Lights/ConvexHull.cs | 4 +- .../ClientSource/Map/Lights/LightManager.cs | 17 +- .../ClientSource/Map/Lights/LightSource.cs | 35 +- .../ClientSource/Map/Map/Map.cs | 24 +- .../ClientSource/Map/MapEntity.cs | 22 + .../ClientSource/Map/RoundSound.cs | 11 +- .../ClientSource/Map/Structure.cs | 18 +- .../ClientSource/Map/Submarine.cs | 3 +- .../ClientSource/Map/SubmarinePreview.cs | 5 +- .../ClientSource/Map/WayPoint.cs | 33 +- .../Networking/FileTransfer/FileReceiver.cs | 4 +- .../ClientSource/Networking/GameClient.cs | 229 ++- .../Networking/Primitives/Peers/ClientPeer.cs | 17 +- .../Primitives/Peers/LidgrenClientPeer.cs | 11 +- .../ClientSource/Networking/RespawnManager.cs | 100 +- .../Networking/ServerList/PingUtils.cs | 4 +- .../ClientSource/Networking/ServerSettings.cs | 47 +- .../Networking/ServerSettingsUI.cs | 43 +- .../Networking/Voip/VoipCapture.cs | 15 +- .../ClientSource/Networking/Voting.cs | 173 +- .../ClientSource/Particles/Particle.cs | 6 +- .../ClientSource/Particles/ParticleEmitter.cs | 19 +- .../ClientSource/Particles/ParticleManager.cs | 21 +- .../ClientSource/Particles/ParticlePrefab.cs | 16 +- .../ClientSource/Physics/PhysicsBody.cs | 89 +- .../CampaignSetupUI/CampaignSetupUI.cs | 119 +- .../MultiPlayerCampaignSetupUI.cs | 100 +- .../SinglePlayerCampaignSetupUI.cs | 50 +- .../CharacterEditor/CharacterEditorScreen.cs | 81 +- .../Screens/CharacterEditor/Wizard.cs | 2 +- .../Screens/EventEditor/EventEditorScreen.cs | 4 +- .../ClientSource/Screens/GameScreen.cs | 125 +- .../ClientSource/Screens/LevelEditorScreen.cs | 223 ++- .../Screens/MainMenuScreen/MainMenuScreen.cs | 20 +- .../ClientSource/Screens/NetLobbyScreen.cs | 1434 ++++++++++++++--- .../ServerListScreen/ServerListScreen.cs | 2 +- .../Screens/SpriteEditorScreen.cs | 78 +- .../ClientSource/Screens/SubEditorScreen.cs | 314 +++- .../ClientSource/Screens/TestScreen.cs | 7 +- .../Serialization/SerializableEntityEditor.cs | 11 +- .../ClientSource/Settings/SettingsMenu.cs | 32 +- .../ClientSource/Sounds/OggSound.cs | 2 +- .../ClientSource/Sounds/SoundChannel.cs | 2 +- .../ClientSource/Sounds/SoundManager.cs | 37 +- .../ClientSource/Sounds/SoundPlayer.cs | 21 +- .../ClientSource/Sounds/SoundPrefab.cs | 7 +- .../ClientSource/Sprite/DecorativeSprite.cs | 70 +- .../ClientSource/Sprite/Sprite.cs | 19 +- .../StatusEffects/StatusEffect.cs | 48 +- .../ClientSource/Steam/SteamManager.cs | 8 +- .../ClientSource/Steam/Workshop.cs | 2 +- .../WorkshopMenu/Mutable/InstalledTab.cs | 167 +- .../WorkshopMenu/Mutable/ModListPreset.cs | 2 +- .../Utils/LocalizationCSVtoXML.cs | 4 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Characters/Character.cs | 2 +- .../ServerSource/Characters/CharacterInfo.cs | 8 +- .../Characters/CharacterNetworking.cs | 69 +- .../ServerSource/DebugConsole.cs | 49 +- .../Events/Missions/CombatMission.cs | 239 ++- .../BarotraumaServer/ServerSource/GameMain.cs | 2 +- .../GameModes/CharacterCampaignData.cs | 1 + .../GameModes/MultiPlayerCampaign.cs | 47 +- .../Items/Components/ItemLabel.cs | 2 +- .../Items/Components/Machines/Steering.cs | 2 +- .../Items/Components/Signal/CircuitBox.cs | 10 +- .../Components/Signal/CustomInterface.cs | 73 +- .../ServerSource/Items/Item.cs | 6 +- .../ServerSource/Networking/BanList.cs | 2 +- .../ServerSource/Networking/ChatMessage.cs | 2 +- .../ServerSource/Networking/Client.cs | 10 +- .../Networking/FileTransfer/FileSender.cs | 4 +- .../ServerSource/Networking/GameServer.cs | 811 ++++++++-- .../Peers/Server/LidgrenServerPeer.cs | 21 +- .../Primitives/Peers/Server/P2PServerPeer.cs | 4 +- .../Primitives/Peers/Server/ServerPeer.cs | 13 + .../ServerSource/Networking/RespawnManager.cs | 417 ++--- .../ServerSource/Networking/ServerSettings.cs | 93 +- .../ServerSource/Networking/Voting.cs | 21 +- .../ServerSource/Screens/NetLobbyScreen.cs | 35 +- .../ServerSource/Traitors/TraitorManager.cs | 11 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../SharedSource/AchievementManager.cs | 102 +- .../SharedSource/CachedDistance.cs | 2 + .../SharedSource/Characters/AI/AITarget.cs | 2 +- .../Characters/AI/EnemyAIController.cs | 1069 +++++++----- .../Characters/AI/HumanAIController.cs | 110 +- .../Characters/AI/IndoorsSteeringManager.cs | 55 +- .../SharedSource/Characters/AI/LatchOntoAI.cs | 2 +- .../Characters/AI/Objectives/AIObjective.cs | 7 +- .../Objectives/AIObjectiveCheckStolenItems.cs | 2 +- .../AI/Objectives/AIObjectiveCleanupItem.cs | 2 +- .../AI/Objectives/AIObjectiveCleanupItems.cs | 2 + .../AI/Objectives/AIObjectiveCombat.cs | 54 +- .../AI/Objectives/AIObjectiveContainItem.cs | 3 +- .../Objectives/AIObjectiveDeconstructItem.cs | 37 +- .../Objectives/AIObjectiveDeconstructItems.cs | 4 +- .../AI/Objectives/AIObjectiveDecontainItem.cs | 2 +- .../Objectives/AIObjectiveEscapeHandcuffs.cs | 2 +- .../Objectives/AIObjectiveExtinguishFire.cs | 2 +- .../Objectives/AIObjectiveFindDivingGear.cs | 138 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 82 +- .../AI/Objectives/AIObjectiveFindThieves.cs | 2 + .../AI/Objectives/AIObjectiveFixLeak.cs | 10 +- .../AI/Objectives/AIObjectiveGetItem.cs | 3 +- .../AI/Objectives/AIObjectiveGetItems.cs | 4 +- .../AI/Objectives/AIObjectiveGoTo.cs | 83 +- .../AI/Objectives/AIObjectiveIdle.cs | 50 +- .../AI/Objectives/AIObjectiveInspectNoises.cs | 3 +- .../AI/Objectives/AIObjectiveLoadItem.cs | 2 +- .../AI/Objectives/AIObjectiveLoadItems.cs | 2 +- .../AI/Objectives/AIObjectiveLoop.cs | 2 +- .../AI/Objectives/AIObjectiveManager.cs | 6 +- .../AI/Objectives/AIObjectiveOperateItem.cs | 2 +- .../AI/Objectives/AIObjectivePrepare.cs | 2 +- .../AI/Objectives/AIObjectiveRepairItem.cs | 2 +- .../AI/Objectives/AIObjectiveRescue.cs | 21 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 19 +- .../AI/Objectives/AIObjectiveReturn.cs | 31 +- .../SharedSource/Characters/AI/Order.cs | 8 +- .../SharedSource/Characters/AI/PetBehavior.cs | 52 +- .../AI/ShipCommand/ShipIssueWorker.cs | 8 +- .../AI/ShipCommand/ShipIssueWorkerSteer.cs | 2 +- .../Characters/Animation/AnimController.cs | 359 ++++- .../Animation/FishAnimController.cs | 139 +- .../Animation/HumanoidAnimController.cs | 283 +--- .../Characters/Animation/Ragdoll.cs | 93 +- .../SharedSource/Characters/Attack.cs | 37 +- .../SharedSource/Characters/Character.cs | 408 +++-- .../Characters/CharacterEventData.cs | 33 +- .../SharedSource/Characters/CharacterInfo.cs | 104 +- .../Characters/CharacterPrefab.cs | 7 +- .../Health/Afflictions/Affliction.cs | 22 +- .../Health/Afflictions/AfflictionBleeding.cs | 2 +- .../Health/Afflictions/AfflictionHusk.cs | 72 +- .../Health/Afflictions/AfflictionPrefab.cs | 25 +- .../Characters/Health/CharacterHealth.cs | 102 +- .../Characters/Health/DamageModifier.cs | 28 +- .../SharedSource/Characters/HumanPrefab.cs | 3 + .../SharedSource/Characters/Jobs/Job.cs | 68 +- .../SharedSource/Characters/Jobs/JobPrefab.cs | 86 +- .../SharedSource/Characters/Jobs/Skill.cs | 6 +- .../Characters/Jobs/SkillPrefab.cs | 43 +- .../SharedSource/Characters/Limb.cs | 29 +- .../Params/Animation/AnimationParams.cs | 47 +- .../Characters/Params/CharacterParams.cs | 142 +- .../Params/Ragdoll/RagdollParams.cs | 18 +- .../AbilityConditionMission.cs | 23 +- .../AbilityConditionCrewMemberUnconscious.cs | 2 +- .../AbilityConditionHasSkill.cs | 11 +- .../AbilityConditionLowestLevel.cs | 2 +- .../AbilityConditionNoCrewDied.cs | 8 +- .../AbilityConditionShipFlooded.cs | 3 +- ...ilityApplyStatusEffectsToApprenticeship.cs | 2 +- .../CharacterAbilityGiveAffliction.cs | 6 +- ...haracterAbilityGiveTalentPointsToAllies.cs | 2 +- .../CharacterAbilityModifyStatToSkill.cs | 4 +- .../Abilities/CharacterAbilityModifyValue.cs | 2 + .../CharacterAbilityUpgradeSubmarine.cs | 12 +- .../CharacterAbilityByTheBook.cs | 6 +- ...erAbilityUnlockApprenticeshipTalentTree.cs | 17 +- .../CharacterAbilityWarStories.cs | 27 +- .../Characters/Talents/TalentTree.cs | 6 +- .../ContentFile/AfflictionsFile.cs | 11 +- .../ContentFile/CharacterFile.cs | 26 +- .../ContentFile/DisembarkPerkFile.cs | 17 + .../ContentPackage/ContentPackage.cs | 56 +- .../ContentManagement/ContentXElement.cs | 1 + .../SharedSource/DebugConsole.cs | 742 +++++++-- .../DisembarkPerks/DisembarkPerkPrefab.cs | 58 + .../PerkBehaviors/GiveTalentPointPerk.cs | 20 + .../DisembarkPerks/PerkBehaviors/PerkBase.cs | 84 + .../PerkBehaviors/SpawnItemPerk.cs | 210 +++ .../PerkBehaviors/SubItemSwapPerk.cs | 61 + .../PerkBehaviors/UpgradeSubmarinePerk.cs | 57 + .../BarotraumaShared/SharedSource/Enums.cs | 38 +- .../EventActions/CheckConditionalAction.cs | 1 - .../Events/EventActions/CheckDataAction.cs | 4 + .../Events/EventActions/CheckItemAction.cs | 5 +- .../Events/EventActions/ConversationAction.cs | 28 +- .../Events/EventActions/EventAction.cs | 4 + .../SharedSource/Events/EventActions/GoTo.cs | 5 +- .../Events/EventActions/MissionAction.cs | 4 +- .../Events/EventActions/MissionStateAction.cs | 9 +- .../EventActions/NPCChangeTeamAction.cs | 63 +- .../EventActions/NPCOperateItemAction.cs | 4 +- .../Events/EventActions/RemoveItemAction.cs | 2 +- .../Events/EventActions/SpawnAction.cs | 155 +- .../Events/EventActions/TagAction.cs | 57 +- .../Events/EventActions/TriggerAction.cs | 57 +- .../Events/EventActions/TriggerEventAction.cs | 14 +- .../SharedSource/Events/EventManager.cs | 79 +- .../SharedSource/Events/EventPrefab.cs | 48 +- .../SharedSource/Events/EventSet.cs | 20 +- .../Missions/AbandonedOutpostMission.cs | 55 +- .../Events/Missions/CombatMission.cs | 124 +- .../Events/Missions/EscortMission.cs | 1 - .../SharedSource/Events/Missions/Mission.cs | 19 +- .../Events/Missions/MissionPrefab.cs | 245 ++- .../Events/Missions/PirateMission.cs | 168 +- .../Events/Missions/SalvageMission.cs | 6 +- .../SharedSource/Events/MonsterEvent.cs | 29 +- .../SharedSource/Events/ScriptedEvent.cs | 90 +- .../Extensions/IEnumerableExtensions.cs | 40 +- .../SharedSource/ForbiddenWordFilter.cs | 2 +- .../GameSession/AutoItemPlacer.cs | 12 +- .../SharedSource/GameSession/CargoManager.cs | 24 +- .../SharedSource/GameSession/CrewManager.cs | 10 +- .../GameSession/GameModes/CampaignMode.cs | 25 +- .../GameSession/GameModes/CoOpMode.cs | 5 +- .../GameSession/GameModes/MissionMode.cs | 30 +- .../GameModes/MultiPlayerCampaign.cs | 25 +- .../GameSession/GameModes/PvPMode.cs | 47 +- .../GameSession/GameModes/TestGameMode.cs | 19 + .../SharedSource/GameSession/GameSession.cs | 481 ++++-- .../SharedSource/Items/CharacterInventory.cs | 60 +- .../Items/Components/DockingPort.cs | 20 +- .../SharedSource/Items/Components/Door.cs | 21 +- .../Items/Components/ElectricalDischarger.cs | 2 +- .../Components/EntitySpawnerComponent.cs | 4 +- .../Items/Components/GeneticMaterial.cs | 287 +++- .../SharedSource/Items/Components/Growable.cs | 2 +- .../Items/Components/Holdable/IdCard.cs | 8 +- .../Items/Components/Holdable/MeleeWeapon.cs | 24 +- .../Items/Components/Holdable/RangedWeapon.cs | 5 +- .../Items/Components/Holdable/RepairTool.cs | 2 +- .../Items/Components/ItemComponent.cs | 21 +- .../Items/Components/ItemContainer.cs | 281 ++-- .../SharedSource/Items/Components/Ladder.cs | 3 +- .../Items/Components/Machines/Controller.cs | 75 +- .../Components/Machines/Deconstructor.cs | 39 +- .../Items/Components/Machines/Engine.cs | 4 +- .../Items/Components/Machines/Fabricator.cs | 2 +- .../Items/Components/Machines/MiniMap.cs | 2 +- .../Components/Machines/OxygenGenerator.cs | 2 +- .../Items/Components/Machines/Pump.cs | 6 +- .../Items/Components/Machines/Reactor.cs | 6 +- .../Items/Components/Machines/Sonar.cs | 3 +- .../Components/Machines/SonarTransducer.cs | 2 +- .../Items/Components/Machines/Steering.cs | 4 +- .../Items/Components/Power/PowerContainer.cs | 2 +- .../Items/Components/Power/PowerTransfer.cs | 21 +- .../Items/Components/Power/Powered.cs | 4 + .../Items/Components/Projectile.cs | 62 +- .../Items/Components/Repairable.cs | 2 +- .../SharedSource/Items/Components/Rope.cs | 130 +- .../Items/Components/Signal/ButtonTerminal.cs | 177 +- .../Items/Components/Signal/CircuitBox.cs | 11 +- .../Items/Components/Signal/Connection.cs | 2 +- .../Signal/ConnectionSelectorComponent.cs | 122 ++ .../Components/Signal/CustomInterface.cs | 216 +-- .../Signal/DemultiplexerComponent.cs | 57 + .../Items/Components/Signal/LightComponent.cs | 5 +- .../Items/Components/Signal/MotionSensor.cs | 95 +- .../Components/Signal/MultiplexerComponent.cs | 59 + .../Items/Components/Signal/OxygenDetector.cs | 12 +- .../Items/Components/Signal/SmokeDetector.cs | 7 +- .../Items/Components/Signal/Wire.cs | 1 + .../Items/Components/TriggerComponent.cs | 208 ++- .../SharedSource/Items/Components/Turret.cs | 38 +- .../SharedSource/Items/Components/Wearable.cs | 12 +- .../SharedSource/Items/ContainerTagPrefab.cs | 19 +- .../SharedSource/Items/Inventory.cs | 7 +- .../SharedSource/Items/Item.cs | 302 +++- .../SharedSource/Items/ItemEventData.cs | 16 +- .../SharedSource/Items/ItemPrefab.cs | 29 +- .../SharedSource/Items/RelatedItem.cs | 22 +- .../Map/Creatures/BallastFloraBehavior.cs | 3 +- .../SharedSource/Map/Explosion.cs | 26 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 2 +- .../SharedSource/Map/ItemAssemblyPrefab.cs | 3 +- .../SharedSource/Map/Levels/Level.cs | 529 ++++-- .../SharedSource/Map/Levels/LevelData.cs | 36 +- .../Map/Levels/LevelGenerationParams.cs | 67 +- .../Levels/LevelObjects/LevelObjectManager.cs | 13 +- .../Map/Levels/LevelObjects/LevelTrigger.cs | 50 +- .../SharedSource/Map/Map/Location.cs | 22 +- .../SharedSource/Map/Map/LocationType.cs | 9 +- .../SharedSource/Map/Map/Map.cs | 29 +- .../Map/Map/MapGenerationParams.cs | 12 +- .../SharedSource/Map/MapEntityPrefab.cs | 2 +- .../Map/Outposts/ExtraSubmarineInfo.cs | 44 +- .../Map/Outposts/OutpostGenerationParams.cs | 80 +- .../Map/Outposts/OutpostGenerator.cs | 297 +++- .../SharedSource/Map/PriceInfo.cs | 18 +- .../SharedSource/Map/Structure.cs | 6 +- .../SharedSource/Map/Submarine.cs | 153 +- .../SharedSource/Map/SubmarineBody.cs | 16 +- .../SharedSource/Map/SubmarineInfo.cs | 31 + .../SharedSource/Map/WayPoint.cs | 52 +- .../SharedSource/Networking/ChatMessage.cs | 10 +- .../SharedSource/Networking/Client.cs | 22 +- .../SharedSource/Networking/EntitySpawner.cs | 51 +- .../SharedSource/Networking/NetworkMember.cs | 17 +- .../NetworkConnection/EosP2PConnection.cs | 2 + .../NetworkConnection/LidgrenConnection.cs | 5 + .../NetworkConnection/NetworkConnection.cs | 5 + .../NetworkConnection/PipeConnection.cs | 3 + .../NetworkConnection/SteamP2PConnection.cs | 5 +- .../SharedSource/Networking/RespawnManager.cs | 393 +++-- .../SharedSource/Networking/ServerLog.cs | 5 +- .../SharedSource/Networking/ServerSettings.cs | 196 ++- .../SharedSource/PerformanceCounter.cs | 43 +- .../SharedSource/Physics/PhysicsBody.cs | 31 +- .../Prefabs/IImplementsVariants.cs | 44 +- .../SharedSource/Screens/NetLobbyScreen.cs | 4 + .../Editable/ConditionallyEditable.cs | 2 +- .../Serialization/Editable/Editable.cs | 10 +- .../SerializableProperty.cs | 21 +- .../Serialization/XMLExtensions.cs | 78 +- .../SharedSource/Settings/GameSettings.cs | 6 +- .../SharedSource/Sprite/ConditionalSprite.cs | 9 +- .../SharedSource/Sprite/Sprite.cs | 4 - .../StatusEffects/DelayedEffect.cs | 37 +- .../StatusEffects/PropertyConditional.cs | 142 +- .../StatusEffects/StatusEffect.cs | 206 ++- .../BarotraumaShared/SharedSource/Tags.cs | 44 +- .../SharedSource/Text/TextPack.cs | 13 +- .../Traitors/TraitorEventPrefab.cs | 13 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 44 +- .../SharedSource/Utils/SafeIO.cs | 203 ++- .../SharedSource/Utils/SaveUtil.cs | 300 +++- .../SharedSource/Utils/ToolBox.cs | 87 +- Barotrauma/BarotraumaShared/changelog.txt | 271 +++- Barotrauma/BarotraumaShared/hintmanager.xml | 1 + .../ClientServer/ClientServerTests.cs | 129 ++ .../ClientServer/HeadlessNetworkClient.cs | 62 + .../BarotraumaTest/CommonnessInfoTests.cs | 6 +- .../BarotraumaTest/CoordinateSpace2DTests.cs | 5 +- .../BarotraumaTest/EndpointParseTests.cs | 13 +- Barotrauma/BarotraumaTest/EnumTests.cs | 49 +- .../FabricatorQualityRollTests.cs | 8 +- .../BarotraumaTest/GenericToolBoxTests.cs | 52 +- ...tSerializableStructImplementationChecks.cs | 7 +- .../INetSerializableStructTests.cs | 24 +- Barotrauma/BarotraumaTest/LinuxTest.csproj | 3 +- Barotrauma/BarotraumaTest/MacTest.csproj | 3 +- Barotrauma/BarotraumaTest/NetIdUtilsTests.cs | 6 +- .../PropertyConditionalTests.cs | 4 +- .../SerializableDateTimeTests.cs | 6 +- Barotrauma/BarotraumaTest/WindowsTest.csproj | 3 +- .../Extensions/ColorExtensions.cs | 9 +- .../Extensions/EnumExtensions.cs | 9 + .../Primitives/AccountId/EpicAccountId.cs | 3 + .../BarotraumaCore/Utils/Identifier.cs | 21 +- .../Generated/SteamStructFunctions.cs | 2 +- .../Networking/NetAddress.cs | 35 +- WindowsSolution.sln | 1 - 417 files changed, 17166 insertions(+), 5870 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/PvPMode.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/DisembarkPerkFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/DisembarkPerkPrefab.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/GiveTalentPointPerk.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/PerkBase.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/SpawnItemPerk.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/SubItemSwapPerk.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/UpgradeSubmarinePerk.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/TestGameMode.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionSelectorComponent.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DemultiplexerComponent.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MultiplexerComponent.cs create mode 100644 Barotrauma/BarotraumaTest/ClientServer/ClientServerTests.cs create mode 100644 Barotrauma/BarotraumaTest/ClientServer/HeadlessNetworkClient.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index 9ecd47a4e..55998217a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -48,7 +48,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); } GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity}", GUIStyle.Red, Color.Black); - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"{targetValue.FormatZeroDecimal()} (M: {SelectedTargetMemory?.Priority.FormatZeroDecimal()}, P: {SelectedTargetingParams?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"{targetValue.FormatZeroDecimal()} (M: {CurrentTargetMemory?.Priority.FormatZeroDecimal()}, P: {CurrentTargetingParams?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); } /*GUIStyle.Font.DrawString(spriteBatch, targetValue.ToString(), pos - Vector2.UnitY * 80.0f, GUIStyle.Red); @@ -73,7 +73,7 @@ namespace Barotrauma } GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 80.0f, State.ToString(), stateColor, Color.Black); - if (State == AIState.Attack && selectedTargetingParams != null && selectedTargetingParams.AttackPattern == AttackPattern.Circle) + if (State == AIState.Attack && currentTargetingParams != null && currentTargetingParams.AttackPattern == AttackPattern.Circle) { GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 100.0f, CirclePhase.ToString(), stateColor, Color.Black); } @@ -134,8 +134,8 @@ namespace Barotrauma //GUI.DrawLine(spriteBatch, pos, ConvertUnits.ToDisplayUnits(steeringManager.AvoidLookAheadPos.X, -steeringManager.AvoidLookAheadPos.Y), Color.Orange, width: 4); } } + GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Steering.X, -Steering.Y)), Color.Blue, width: 4); GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Character.AnimController.TargetMovement.X, -Character.AnimController.TargetMovement.Y)), Color.SteelBlue, width: 2); - GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Steering.X, -Steering.Y)), Color.Blue, width: 3); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index e35f32c4e..23cd60f7f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -27,7 +27,8 @@ namespace Barotrauma protected float lastRecvPositionUpdateTime; - private float hudInfoHeight = 100.0f; + private const float DefaultHudInfoHeight = 78.0f; + private float hudInfoHeight = DefaultHudInfoHeight; private List sounds; @@ -471,7 +472,7 @@ namespace Barotrauma if (!GUI.InputBlockingMenuOpen) { if (SelectedItem != null && - (SelectedItem.ActiveHUDs.Any(ic => ic.GuiFrame != null && HUD.CloseHUD(ic.GuiFrame.Rect)) || + (SelectedItem.ActiveHUDs.Any(ic => ic.GuiFrame != null && ic.CloseByClickingOutsideGUIFrame && HUD.CloseHUD(ic.GuiFrame.Rect)) || ((ViewTarget as Item)?.Prefab.FocusOnSelected ?? false) && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape))) { if (GameMain.Client != null) @@ -543,7 +544,10 @@ namespace Barotrauma { if (attackResult.Damage <= 1.0f) { return; } } - PlaySound(CharacterSound.SoundType.Damage, maxInterval: 2); + if (AIState != AIState.PlayDead) + { + PlaySound(CharacterSound.SoundType.Damage, maxInterval: 2); + } } partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log) @@ -588,7 +592,6 @@ namespace Barotrauma } } - sounds.ForEach(s => s.Sound?.Dispose()); sounds.Clear(); if (GameMain.GameSession?.CrewManager != null && @@ -814,9 +817,12 @@ namespace Barotrauma PlaySound(CharacterSound.SoundType.Idle); } break; + case AIState.PlayDead: + case AIState.Freeze: + case AIState.Hiding: + break; default: - var petBehavior = enemyAI.PetBehavior; - if (petBehavior != null && + if (enemyAI.PetBehavior is PetBehavior petBehavior && (petBehavior.Happiness < petBehavior.UnhappyThreshold || petBehavior.Hunger > petBehavior.HungryThreshold)) { PlaySound(CharacterSound.SoundType.Unhappy); @@ -948,7 +954,9 @@ namespace Barotrauma Controlled != this && Submarine != null && Controlled.Submarine == Submarine && - GameSettings.CurrentConfig.Graphics.LosMode != LosMode.None) + GameSettings.CurrentConfig.Graphics.LosMode != LosMode.None && + //less restrictions on name tag visibility in PvP mode (always show them if the character is visible) + GameMain.GameSession?.GameMode is not PvPMode) { float yPos = Controlled.AnimController.FloorY - 1.5f; @@ -965,15 +973,16 @@ namespace Barotrauma Vector2 pos = DrawPosition; pos.Y += hudInfoHeight; - if (CurrentHull != null && DrawPosition.Y > CurrentHull.WorldRect.Y - 130.0f) + float paddingBelowCeiling = 30.0f; + if (CurrentHull != null && DrawPosition.Y + DefaultHudInfoHeight > CurrentHull.WorldRect.Y - paddingBelowCeiling) { - float lowerAmount = DrawPosition.Y - (CurrentHull.WorldRect.Y - 130.0f); - hudInfoHeight = MathHelper.Lerp(hudInfoHeight, 100.0f - lowerAmount, 0.1f); + float lowerAmount = (DrawPosition.Y + DefaultHudInfoHeight) - (CurrentHull.WorldRect.Y - paddingBelowCeiling); + hudInfoHeight = MathHelper.Lerp(hudInfoHeight, DefaultHudInfoHeight - lowerAmount, 0.1f); hudInfoHeight = Math.Max(hudInfoHeight, 20.0f); } else { - hudInfoHeight = MathHelper.Lerp(hudInfoHeight, 100.0f, 0.1f); + hudInfoHeight = MathHelper.Lerp(hudInfoHeight, DefaultHudInfoHeight, 0.1f); } pos.Y = -pos.Y; @@ -1013,6 +1022,8 @@ namespace Barotrauma CampaignInteractionType == CampaignMode.InteractionType.None ? MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) : 1.0f; + //full name tag visibility in PvP mode to make it easier to tell who's an enemy + float nameTextAlpha = GameMain.GameSession?.GameMode is PvPMode ? 1.0f : hudInfoAlpha; if (!GUI.DisableCharacterNames && hudInfoVisible && (controlled == null || this != controlled.FocusedCharacter || IsPet) && cam.Zoom > 0.4f) @@ -1030,7 +1041,7 @@ namespace Barotrauma } Vector2 nameSize = GUIStyle.Font.MeasureString(name); - Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; + Vector2 namePos = new Vector2(pos.X, pos.Y - 5.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; Color nameColor = GetNameColor(); Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight); @@ -1057,7 +1068,7 @@ namespace Barotrauma } GUIStyle.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f); - GUIStyle.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); + GUIStyle.Font.DrawString(spriteBatch, name, namePos, nameColor * nameTextAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); if (GameMain.DebugDraw) { GUIStyle.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); @@ -1068,15 +1079,18 @@ namespace Barotrauma if (petBehavior != null && !IsDead && !IsUnconscious) { var petStatus = petBehavior.GetCurrentStatusIndicatorType(); - var iconStyle = GUIStyle.GetComponentStyle("PetIcon." + petStatus); - if (iconStyle != null) + if (petStatus != PetBehavior.StatusIndicatorType.None) { - Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; - Vector2 iconPos = headPos; - iconPos.Y = -iconPos.Y; - var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); - float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; - icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); + var iconStyle = GUIStyle.GetComponentStyle("PetIcon." + petStatus); + if (iconStyle != null) + { + Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; + Vector2 iconPos = headPos; + iconPos.Y = -iconPos.Y; + var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); + float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; + icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); + } } } } @@ -1100,7 +1114,7 @@ namespace Barotrauma } } - if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) + if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible && AIState != AIState.PlayDead && AIState != AIState.Hiding) { hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f)); @@ -1233,7 +1247,7 @@ namespace Barotrauma public Color GetNameColor() { CharacterTeamType team = teamID; - if (Info?.IsDisguisedAsAnother != null) + if (Info is { IsDisguisedAsAnother: true }) { var idCard = Inventory.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); if (idCard != null) @@ -1249,18 +1263,22 @@ namespace Barotrauma } } + CharacterTeamType myTeam = + Controlled?.TeamID ?? + GameMain.Client?.MyClient?.TeamID ?? + CharacterTeamType.Team1; + Color nameColor = GUIStyle.TextColorNormal; - if (Controlled != null && team != Controlled.TeamID) + if (TeamID == CharacterTeamType.FriendlyNPC) { - if (TeamID == CharacterTeamType.FriendlyNPC) - { - nameColor = UniqueNameColor ?? Color.SkyBlue; - } - else - { - nameColor = GUIStyle.Red; - } + nameColor = UniqueNameColor ?? Color.SkyBlue; } + else if (team != myTeam) + { + //opposing team is red when controlling a character + nameColor = GUIStyle.Red; + } + return nameColor; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 633630298..4068aba1a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -752,7 +752,7 @@ namespace Barotrauma } textPos.X += 10.0f * GUI.Scale; - if (!character.FocusedCharacter.IsIncapacitated && character.FocusedCharacter.IsPet) + if (!character.FocusedCharacter.IsIncapacitated && character.FocusedCharacter.IsPet && character.IsFriendly(character.FocusedCharacter)) { GUI.DrawString(spriteBatch, textPos, GetCachedHudText("PlayHint", InputType.Use), GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index e34cbb98f..1276312d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -17,7 +17,8 @@ namespace Barotrauma private static Sprite infoAreaPortraitBG; public bool LastControlled; - public int CrewListIndex { get; set; } = -1; + + public int CrewListIndex { get; set; } = int.MaxValue; //default to the bottom of the list private Sprite disguisedPortrait; private List disguisedAttachmentSprites; @@ -32,6 +33,8 @@ namespace Barotrauma private float tintHighlightThreshold; private float tintHighlightMultiplier; + public bool ShowTalentResetPopupOnOpen = true; + public static void Init() { infoAreaPortraitBG = GUIStyle.GetComponentStyle("InfoAreaPortraitBG")?.GetDefaultSprite(); @@ -208,7 +211,7 @@ namespace Barotrauma return frame; } - partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel) + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel, bool forceNotification) { if (TeamID == CharacterTeamType.FriendlyNPC) { return; } if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } @@ -226,6 +229,18 @@ namespace Barotrauma specialIncrease ? GUIStyle.Orange : GUIStyle.Green, playSound: Character == Character.Controlled, skillIdentifier, increase); } + else if (forceNotification) + { + float change = newLevel - prevLevel; + if (Math.Abs(change) > 0.01f) + { + string sign = change > 0 ? "+" : "-"; + Character?.AddMessage( + $"{sign}{Math.Round(change, 2)} {TextManager.Get("SkillName." + skillIdentifier).Value}", + specialIncrease ? GUIStyle.Orange : GUIStyle.Green, + playSound: Character == Character.Controlled); + } + } } partial void OnExperienceChanged(int prevAmount, int newAmount) @@ -586,6 +601,8 @@ namespace Barotrauma ch.ExperiencePoints = inc.ReadInt32(); ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints); ch.PermanentlyDead = inc.ReadBoolean(); + ch.TalentRefundPoints = inc.ReadInt32(); + ch.TalentResetCount = inc.ReadInt32(); return ch; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index ee0137f93..5ef1cebff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -154,6 +154,9 @@ namespace Barotrauma case TreatmentEventData _: msg.WriteBoolean(AnimController.Anim == AnimController.Animation.CPR); break; + case ConfirmRefundEventData _: + //do nothing + break; case CharacterStatusEventData _: //do nothing break; @@ -202,12 +205,16 @@ namespace Barotrauma keys[(int)InputType.Use].Held = useInput; keys[(int)InputType.Use].SetState(false, useInput); - bool crouching = msg.ReadBoolean(); if (AnimController is HumanoidAnimController) { + bool crouching = msg.ReadBoolean(); keys[(int)InputType.Crouch].Held = crouching; keys[(int)InputType.Crouch].SetState(false, crouching); } + else if (AnimController is FishAnimController fishAnim) + { + fishAnim.Reverse = msg.ReadBoolean(); + } bool attackInput = msg.ReadBoolean(); keys[(int)InputType.Attack].Held = attackInput; @@ -395,13 +402,13 @@ namespace Barotrauma ReadStatus(msg); break; case EventType.UpdateSkills: - int skillCount = msg.ReadByte(); - for (int i = 0; i < skillCount; i++) + Identifier skillIdentifier = msg.ReadIdentifier(); + if (!skillIdentifier.IsEmpty) { - Identifier skillIdentifier = msg.ReadIdentifier(); + bool forceNotification = msg.ReadBoolean(); float skillLevel = msg.ReadSingle(); - info?.SetSkillLevel(skillIdentifier, skillLevel); - } + info?.SetSkillLevel(skillIdentifier, skillLevel, forceNotification: forceNotification); + } break; case EventType.SetAttackTarget: case EventType.ExecuteAttack: @@ -512,7 +519,12 @@ namespace Barotrauma break; case EventType.UpdateExperience: int experienceAmount = msg.ReadInt32(); - info?.SetExperience(experienceAmount); + int additionalTalentPoints = msg.ReadInt32(); + if (info != null) + { + info.SetExperience(experienceAmount); + info.AdditionalTalentPoints = additionalTalentPoints; + } break; case EventType.UpdateTalents: ushort talentCount = msg.ReadUInt16(); @@ -527,6 +539,20 @@ namespace Barotrauma int moneyAmount = msg.ReadInt32(); SetMoney(moneyAmount); break; + case EventType.UpdateTalentRefundPoints: + int refundPoints = msg.ReadInt32(); + if (info != null) + { + if (refundPoints > info.TalentRefundPoints) + { + info.ShowTalentResetPopupOnOpen = true; + } + info.TalentRefundPoints = refundPoints; + } + break; + case EventType.ConfirmTalentRefund: + Info?.RefundTalents(); + break; case EventType.UpdatePermanentStats: byte savedStatValueCount = msg.ReadByte(); StatTypes statType = (StatTypes)msg.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 01a89a762..02af592fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -31,8 +31,7 @@ namespace Barotrauma } public static Sprite DamageOverlay => DamageOverlayPrefab.Prefabs.ActivePrefab.DamageOverlay; - - + private Point screenResolution; private float uiScale, inventoryScale; @@ -105,6 +104,12 @@ namespace Barotrauma private GUILayoutGroup treatmentLayout; private GUIListBox recommendedTreatmentContainer; + /// + /// Timer for updating visuals (limb tints and overlays) caused by the affliction + /// + private float updateVisualsTimer = Rand.Range(0.0f, UpdateVisualsInterval); + const float UpdateVisualsInterval = 0.5f; + private float distortTimer; // 0-1 @@ -461,15 +466,17 @@ namespace Barotrauma private void OnAttacked(Character attacker, AttackResult attackResult) { if (Math.Abs(attackResult.Damage) < 0.01f) { return; } - DamageOverlayTimer = MathHelper.Clamp(attackResult.Damage / MaxVitality, DamageOverlayTimer, 1.0f); - if (healthShadowDelay <= 0.0f) { healthShadowDelay = 1.0f; } + if (ShowDamageOverlay) + { + DamageOverlayTimer = MathHelper.Clamp(attackResult.Damage / MaxVitality, DamageOverlayTimer, 1.0f); + float additionalIntensity = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 0.1f, attackResult.Damage / MaxVitality)); + damageIntensity = MathHelper.Clamp(damageIntensity + additionalIntensity, 0, 1); + } + + if (healthShadowDelay <= 0.0f) { healthShadowDelay = 1.0f; } if (healthBarPulsateTimer <= 0.0f) { healthBarPulsatePhase = 0.0f; } healthBarPulsateTimer = 1.0f; - - float additionalIntensity = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 0.1f, attackResult.Damage / MaxVitality)); - damageIntensity = MathHelper.Clamp(damageIntensity + additionalIntensity, 0, 1); - DisplayVitalityDelay = 0.5f; } @@ -1036,6 +1043,12 @@ namespace Barotrauma var affliction = kvp.Key; affliction.Prefab.AfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f, new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y)); + + var activeEffect = affliction.GetActiveEffect(); + if (activeEffect is { ThermalOverlayRange: > 0.0f }) + { + StatusHUD.DrawThermalOverlay(spriteBatch, Character, Character, activeEffect.ThermalOverlayColor, activeEffect.ThermalOverlayRange, effectState: (float)Timing.TotalTimeUnpaused, showDeadCharacters: false); + } } float damageOverlayAlpha = DamageOverlayTimer; @@ -1380,7 +1393,7 @@ namespace Barotrauma recommendedTreatmentContainer.Content.ClearChildren(); - float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel("medical"); + float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel(Tags.MedicalSkill); //key = item identifier //float = suitability @@ -1949,7 +1962,6 @@ namespace Barotrauma } } - private bool ShouldDisplayAfflictionOnLimb(KeyValuePair kvp, LimbHealth limbHealth) { if (!kvp.Key.ShouldShowIcon(Character)) { return false; } @@ -2058,23 +2070,23 @@ namespace Barotrauma newAfflictions.Add((limbHealths[limbIndex], afflictionPrefab, afflictionStrength)); } - foreach (KeyValuePair kvp in afflictions) + foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions) { //deactivate afflictions that weren't included in the network message - if (!newAfflictions.Any(a => kvp.Key.Prefab == a.afflictionPrefab && kvp.Value == a.limb)) + if (newAfflictions.None(a => affliction.Prefab == a.afflictionPrefab && limbHealth == a.limb)) { - kvp.Key.Strength = 0.0f; + affliction.Strength = 0.0f; } } foreach (var (limb, afflictionPrefab, strength) in newAfflictions) { Affliction existingAffliction = null; - foreach (KeyValuePair kvp in afflictions) + foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions) { - if (kvp.Key.Prefab == afflictionPrefab && kvp.Value == limb) + if (affliction.Prefab == afflictionPrefab && limbHealth == limb) { - existingAffliction = kvp.Key; + existingAffliction = affliction; break; } } @@ -2123,9 +2135,8 @@ namespace Barotrauma if (!Character.Params.Health.ApplyAfflictionColors) { return; } - foreach (KeyValuePair kvp in afflictions) + foreach ((Affliction affliction, LimbHealth _) in afflictions) { - var affliction = kvp.Key; Color faceTint = affliction.GetFaceTint(); if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } Color bodyTint = affliction.GetBodyTint(); @@ -2137,17 +2148,23 @@ namespace Barotrauma { foreach (Limb limb in Character.AnimController.Limbs) { - if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { continue; } limb.BurnOverlayStrength = 0.0f; limb.DamageOverlayStrength = 0.0f; - foreach (KeyValuePair kvp in afflictions) + } + + foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions) + { + if (affliction.Prefab.BurnOverlayAlpha <= 0.0f && affliction.Prefab.DamageOverlayAlpha <= 0.0f) { continue; } + + float burnStrength = affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.BurnOverlayAlpha; + float damageOverlayStrength = affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.DamageOverlayAlpha; + foreach (Limb limb in Character.AnimController.Limbs) { - var affliction = kvp.Key; - float burnStrength = affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.BurnOverlayAlpha; - if (kvp.Value == limbHealths[limb.HealthIndex] || !affliction.Prefab.LimbSpecific) + if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { continue; } + if (limbHealth == limbHealths[limb.HealthIndex] || !affliction.Prefab.LimbSpecific) { limb.BurnOverlayStrength += burnStrength; - limb.DamageOverlayStrength += affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.DamageOverlayAlpha; + limb.DamageOverlayStrength += damageOverlayStrength; } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs index 1ac28a552..8c6d451df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs @@ -1,14 +1,13 @@ using Microsoft.Xna.Framework; -using System.Linq; using System; -using System.Xml.Linq; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { partial class JobPrefab : PrefabWithUintIdentifier { - public GUIButton CreateInfoFrame(out GUIComponent buttonContainer) + public GUIButton CreateInfoFrame(bool isPvP, out GUIComponent buttonContainer) { int width = 500, height = 400; @@ -29,57 +28,28 @@ namespace Barotrauma TextManager.Get("Skills"), font: GUIStyle.LargeFont); foreach (SkillPrefab skill in Skills) { + var levelRange = skill.GetLevelRange(isPvP); + + string levelStr = + levelRange.End > levelRange.Start ? + (int)levelRange.Start + " - " + (int)levelRange.End : + ((int)levelRange.Start).ToString(); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillContainer.RectTransform), - " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), (int)skill.LevelRange.Start + " - " + (int)skill.LevelRange.End), + " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), levelStr), font: GUIStyle.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) }) - { - Stretch = true - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), itemContainer.RectTransform), - TextManager.Get("Items", "mapentitycategory.equipment"), font: GUIStyle.LargeFont); - foreach (string identifier in itemIdentifiers.Distinct()) - { - if (!(MapEntityPrefab.Find(name: null, identifier: identifier) is ItemPrefab itemPrefab)) { continue; } - int count = itemIdentifiers.Count(i => i == identifier); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), itemContainer.RectTransform), - " - " + (count == 1 ? itemPrefab.Name : itemPrefab.Name + " x" + count), - font: GUIStyle.SmallFont); - }*/ - + return frameHolder; } - - public class OutfitPreview + + public IEnumerable GetJobOutfitSprites(CharacterTeamType team, bool isPvPMode) { - public readonly List<(Sprite sprite, Vector2 drawOffset)> Sprites; - - public Vector2 Dimensions; - - public OutfitPreview() - { - Sprites = new List<(Sprite sprite, Vector2 drawOffset)>(); - Dimensions = Vector2.One; - } - - public void AddSprite(Sprite sprite, Vector2 drawOffset) - { - Sprites.Add((sprite, drawOffset)); - } - } - - public List GetJobOutfitSprites(CharacterInfoPrefab charInfoPrefab, bool useInventoryIcon, out Vector2 maxDimensions) - { - List outfitPreviews = new List(); - maxDimensions = Vector2.One; - - var equipIdentifiers = Element.GetChildElements("ItemSet").Elements().Where(e => e.GetAttributeBool("outfit", false)).Select(e => e.GetAttributeIdentifier("identifier", "")); + var equipIdentifiers = JobItems + .SelectMany(kvp => kvp.Value) + .Where(j => j.Outfit) + .Select(j => j.GetItemIdentifier(team, isPvPMode)); List outfitPrefabs = new List(); foreach (var equipIdentifier in equipIdentifiers) @@ -88,45 +58,9 @@ namespace Barotrauma if (itemPrefab != null) { outfitPrefabs.Add(itemPrefab); } } - if (!outfitPrefabs.Any()) { return null; } + if (!outfitPrefabs.Any()) { return Enumerable.Empty(); } - for (int i = 0; i < outfitPrefabs.Count; i++) - { - var outfitPreview = new OutfitPreview(); - - if (!ItemSets.TryGetValue(i, out var itemSetElement)) { continue; } - var previewElement = itemSetElement.GetChildElement("PreviewSprites"); - if (previewElement == null || useInventoryIcon) - { - if (outfitPrefabs[i] is ItemPrefab prefab && prefab.InventoryIcon != null) - { - outfitPreview.AddSprite(prefab.InventoryIcon, Vector2.Zero); - outfitPreview.Dimensions = prefab.InventoryIcon.SourceRect.Size.ToVector2(); - maxDimensions.X = MathHelper.Max(maxDimensions.X, outfitPreview.Dimensions.X); - maxDimensions.Y = MathHelper.Max(maxDimensions.Y, outfitPreview.Dimensions.Y); - } - outfitPreviews.Add(outfitPreview); - continue; - } - - var children = previewElement.Elements().ToList(); - for (int n = 0; n < children.Count; n++) - { - var spriteElement = children[n]; - string spriteTexture = charInfoPrefab.ReplaceVars(spriteElement.GetAttributeString("texture", ""), charInfoPrefab.Heads.First()); - var sprite = new Sprite(spriteElement, file: spriteTexture); - sprite.size = new Vector2(sprite.SourceRect.Width, sprite.SourceRect.Height); - outfitPreview.AddSprite(sprite, children[n].GetAttributeVector2("offset", Vector2.Zero)); - } - - outfitPreview.Dimensions = previewElement.GetAttributeVector2("dims", Vector2.One); - maxDimensions.X = MathHelper.Max(maxDimensions.X, outfitPreview.Dimensions.X); - maxDimensions.Y = MathHelper.Max(maxDimensions.Y, outfitPreview.Dimensions.Y); - - outfitPreviews.Add(outfitPreview); - } - - return outfitPreviews; + return outfitPrefabs.Select(p => p.InventoryIcon ?? p.Sprite); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 04fb57758..2542cdb11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -184,12 +184,6 @@ 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>(); @@ -198,6 +192,7 @@ namespace Barotrauma { public float RotationState; public float OffsetState; + public float ScaleState; public Vector2 RandomOffsetMultiplier = new Vector2(Rand.Range(-1.0f, 1.0f), Rand.Range(-1.0f, 1.0f)); public float RandomRotationFactor = Rand.Range(0.0f, 1.0f); public float RandomScaleFactor = Rand.Range(0.0f, 1.0f); @@ -301,7 +296,16 @@ namespace Barotrauma DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams, ref _damagedTexturePath), sourceRectScale: sourceRectScale); break; case "conditionalsprite": - var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), file: GetSpritePath(subElement, null, ref _texturePath), sourceRectScale: sourceRectScale); + string conditionalSpritePath = string.Empty; + GetSpritePath(subElement.GetChildElement("sprite") ?? subElement.GetChildElement("deformablesprite") ?? subElement, null, ref conditionalSpritePath); + if (conditionalSpritePath.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"Failed to find a sprite path in the conditional sprite defined in {character.SpeciesName}, limb {type}.", + contentPackage: subElement.ContentPackage); + } + var conditionalSprite = new ConditionalSprite(subElement, GetConditionalTarget(), + file: conditionalSpritePath, + sourceRectScale: sourceRectScale); ConditionalSprites.Add(conditionalSprite); if (conditionalSprite.DeformableSprite != null) { @@ -475,7 +479,7 @@ namespace Barotrauma private string _damagedTexturePath; private string GetSpritePath(ContentXElement element, SpriteParams spriteParams, ref string path) { - if (path == null) + if (path.IsNullOrEmpty()) { if (spriteParams != null) { @@ -952,8 +956,8 @@ namespace Barotrauma var ca = (float)Math.Cos(-body.Rotation); var sa = (float)Math.Sin(-body.Rotation); Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, - -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect, + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, decorativeSprite.Sprite.Origin, + -body.Rotation + rotation, decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect, depth: activeSprite.Depth - depthStep); depthStep += step; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs index bdfc98c6e..d3de65b36 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs @@ -86,7 +86,7 @@ namespace Barotrauma var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame: !isEditor, showName: false, titleFont: GUIStyle.SubHeadingFont) { - Readonly = CircuitBox.Locked + Readonly = CircuitBox.IsLocked() }; fieldCount += componentEditor.Fields.Count; diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs index 17f1e9215..f0cf5ef70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs @@ -35,7 +35,7 @@ namespace Barotrauma public List VirtualWires = new(); - public bool Locked => CircuitBox.Locked; + public bool Locked => CircuitBox.IsLocked(); public CircuitBoxUI(CircuitBox box) { @@ -786,6 +786,7 @@ namespace Barotrauma if (wireOption.TryUnwrap(out var wire)) { CircuitBox.RemoveWires(wire.IsSelected ? wireSelection : ImmutableArray.Create(wire)); + return; } switch (nodeOption) diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs index 4c883b07d..8cd18148c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -169,9 +169,9 @@ namespace Barotrauma return doc; } - public void Save(string path) + public void Save(string path, bool catchUnauthorizedAccessExceptions = true) { - Directory.CreateDirectory(Path.GetDirectoryName(path)!); + Directory.CreateDirectory(Path.GetDirectoryName(path)!, catchUnauthorizedAccessExceptions); ToXDocument().SaveSafe(path); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs index 5f45b7d56..036e488a4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/LegacySteamUgcTransition.cs @@ -265,7 +265,7 @@ namespace Barotrauma.Transition subs = getFiles(oldSubsPath, "*.sub"); itemAssemblies = getFiles(oldItemAssembliesPath, "*.xml"); - string[] allOldMods = Directory.GetDirectories(oldModsPath, "*", System.IO.SearchOption.TopDirectoryOnly); + string[] allOldMods = Directory.GetDirectories(oldModsPath, "*"); var publishedItems = await SteamManager.Workshop.GetPublishedItems(); foreach (var modDir in allOldMods) diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index d6400ca91..002104b6a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -587,6 +587,39 @@ namespace Barotrauma } GameMain.CharacterEditorScreen.Select(); })); + + commands.Add(new Command("settainted", "settainted [true/false]: Sets tainted effect on hovered genetic material.", + onExecute: (string[] args) => + { + + if (Character.Controlled == null) + { + NewMessage("No controlled character!", Color.Red); + return; + } + + Item focusedItem = Character.Controlled?.FocusedItem ?? Inventory.SelectedSlot?.Item; + + if (focusedItem == null) + { + NewMessage("No focused item, hover on something!", Color.Red); + return; + } + + var geneticMaterial = focusedItem.GetComponent(); + + if (geneticMaterial == null) + { + NewMessage("Not hovering on a genetic material!", Color.Red); + return; + } + else + { + bool newValue = args.None(arg => string.Equals(arg, "false", StringComparison.InvariantCultureIgnoreCase)); + geneticMaterial.SetTainted(newValue); + NewMessage($"Set tainted to {newValue} for {focusedItem.Name}", Color.Yellow); + } + }, isCheat: true)); commands.Add(new Command("quickstart", "Starts a singleplayer sandbox", (string[] args) => { @@ -625,6 +658,113 @@ namespace Barotrauma GameMain.MainMenuScreen.QuickStart(fixedSeed: false, subName, difficulty, levelGenerationParams); }, getValidArgs: () => new[] { SubmarineInfo.SavedSubmarines.Select(s => s.Name).Distinct().OrderBy(s => s).ToArray() })); + + commands.Add(new Command("forcewreck", "forcewreck [wreckname] (optional, ThalamusSpawn)[Random/Forced/Disabled]: When generating levels, ensures a specific wreck is generated. Second optional parameter to control thalamus spawning.", (string[] args) => + { + if (args.Length > 0) + { + var submarineFile = GetSubmarineFile(args[0]); + + if (submarineFile != null) + { + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(i => i.FilePath == submarineFile.Path.Value); + if (matchingSub != null) + { + NewMessage($"Setting ForceWreck to: {matchingSub.Name}, {submarineFile.Path}", color: Color.Yellow); + + LevelData.ConsoleForceWreck = matchingSub; + } + } + else + { + NewMessage($"Can't find: {args[0]}", color: Color.Red); + } + } + + if (args.Length > 1) + { + string forceThalamusArg = args[1]; + if (Enum.TryParse(forceThalamusArg, out LevelData.ThalamusSpawn result)) + { + NewMessage($"Setting ThalamusSpawn to: {result}", color: Color.Yellow); + LevelData.ForceThalamus = result; + } + else + { + NewMessage($"Can't parse argument: {forceThalamusArg}", color: Color.Red); + } + } + else + { + NewMessage($"Setting ThalamusSpawn to: {LevelData.ThalamusSpawn.Random}", color: Color.Yellow); + LevelData.ForceThalamus = LevelData.ThalamusSpawn.Random; + } + }, + () => + { + return new string[][] + { + ListSubmarineFileNames(), + new string[] { LevelData.ThalamusSpawn.Random.ToString(), LevelData.ThalamusSpawn.Forced.ToString(), LevelData.ThalamusSpawn.Disabled.ToString() } + }; + }, isCheat: true)); + + commands.Add(new Command("forcebeaconstation|forcebeacon", "forcebeaconstation [station name]: When generating levels, ensures a specific beacon station is generated.", (string[] args) => + { + if (args.Length > 0) + { + var submarineFile = GetSubmarineFile(args[0]); + + if (submarineFile != null) + { + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(i => i.FilePath == submarineFile.Path.Value); + if (matchingSub != null) + { + NewMessage($"Setting ForceBeaconStation to: {matchingSub.Name}, {submarineFile.Path}", color: Color.Yellow); + + LevelData.ConsoleForceBeaconStation = matchingSub; + } + } + else + { + NewMessage($"Can't find: {args[0]}", color: Color.Red); + } + } + }, + () => + { + return new string[][] + { + ListSubmarineFileNames() + }; + }, isCheat: true)); + + commands.Add(new Command("reloadcontentfile", "reloadcontentfile [filepath]: Reloads a specific content xml file during runtime.", (string[] args) => + { + if (args.Length > 0) + { + string pathArgument = args[0]; + var contentFile = GetContentFile(pathArgument); + + if (contentFile != null) + { + NewMessage($"Reloading content file: {pathArgument}", Color.Yellow); + contentFile.UnloadFile(); + contentFile.LoadFile(); + } + else + { + NewMessage($"Can't find {args[0]} to reload", color:Color.Red); + } + } + }, + () => + { + return new string[][] + { + ListContentFilePaths() + }; + }, isCheat: true)); commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks networking debug logging.", (string[] args) => { @@ -783,6 +923,7 @@ namespace Barotrauma AssignRelayToServer("spreadsheetexport", false); #if DEBUG AssignRelayToServer("listspamfilters", false); + AssignRelayToServer("showitemxml", false); AssignRelayToServer("crash", false); AssignRelayToServer("showballastflorasprite", false); AssignRelayToServer("simulatedlatency", false); @@ -1924,7 +2065,7 @@ namespace Barotrauma addIfMissing($"missionname.{missionId}".ToIdentifier(), language); } - if (missionPrefab.Type == MissionType.Combat) + if (missionPrefab.Type == Tags.MissionTypeCombat) { addIfMissing($"MissionDescriptionNeutral.{missionId}".ToIdentifier(), language); addIfMissing($"MissionDescription1.{missionId}".ToIdentifier(), language); @@ -2360,6 +2501,34 @@ namespace Barotrauma GameMain.SubEditorScreen.LoadSub(wreckedSubmarineInfo); })); + commands.Add(new Command("showitemxml", "showitemxml [item]: Shows the XML configuration of an item in the console and copies it to the clipboard. Useful for debugging variants that partially override the XML of the base item for example.", (string[] args) => + { + if (args.Length == 0) + { + ThrowError("Please specify the name or identifier of the item."); + return; + } + + string itemNameOrId = args[0].ToLowerInvariant(); + ItemPrefab itemPrefab = + (MapEntityPrefab.FindByName(itemNameOrId) ?? + MapEntityPrefab.FindByIdentifier(itemNameOrId.ToIdentifier())) as ItemPrefab; + if (itemPrefab == null) + { + ThrowError("Item \"{itemNameOrId}\" not found!"); + return; + } + string xmlStr = itemPrefab.ConfigElement.Element.ToString(); + NewMessage(xmlStr); + Clipboard.SetText(xmlStr); + }, getValidArgs: () => + { + return new string[][] + { + GetItemNameOrIdParams().ToArray() + }; + })); + #if DEBUG commands.Add(new Command("deathprompt", "Shows the death prompt for testing purposes.", (string[] args) => { @@ -2665,7 +2834,7 @@ namespace Barotrauma string[] lines; try { - lines = File.ReadAllLines(sourcePath); + lines = File.ReadAllLines(sourcePath, catchUnauthorizedAccessExceptions: false); } catch (Exception e) { @@ -2757,7 +2926,6 @@ namespace Barotrauma foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs()) { - if (eventPrefab is not TraitorEventPrefab) { continue; } if (eventPrefab.Identifier.IsEmpty) { continue; @@ -2812,7 +2980,7 @@ namespace Barotrauma } string textId = $"EventText.{parentName}"; - if (!string.IsNullOrEmpty(text) && !text.Contains("EventText.", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(text) && !text.StartsWith("EventText.", StringComparison.OrdinalIgnoreCase) && !text.StartsWith("Tutorial.", StringComparison.OrdinalIgnoreCase)) { if (existingTexts.TryGetValue(text, out string existingTextId)) { @@ -2982,9 +3150,18 @@ namespace Barotrauma })); #if DEBUG + commands.Add(new Command("playovervc", "Plays a sound over voice chat.", (args) => + { + VoipCapture.Instance?.SetOverrideSound(args.Length > 0 ? args[0] : null); + })); + commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) => { - if (args.Length != 1) { return; } + if (args.Length != 1) + { + ThrowError("Please specify a language to check."); + return; + } TextManager.CheckForDuplicates(args[0].ToIdentifier().ToLanguageIdentifier()); })); @@ -3443,9 +3620,7 @@ namespace Barotrauma } RagdollParams ragdollParams = character.AnimController.RagdollParams; ragdollParams.LimbScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE); - var pos = character.WorldPosition; - character.AnimController.Recreate(); - character.TeleportTo(pos); + character.AnimController.RecreateAndRespawn(); }, isCheat: true)); commands.Add(new Command("jointscale", "Define the jointscale for the controlled character. Provide id or name if you want to target another character. Note: the changes are not saved!", (string[] args) => @@ -3468,9 +3643,7 @@ namespace Barotrauma } RagdollParams ragdollParams = character.AnimController.RagdollParams; ragdollParams.JointScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE); - var pos = character.WorldPosition; - character.AnimController.Recreate(); - character.TeleportTo(pos); + character.AnimController.RecreateAndRespawn(); }, isCheat: true)); commands.Add(new Command("ragdollscale", "Rescale the ragdoll of the controlled character. Provide id or name if you want to target another character. Note: the changes are not saved!", (string[] args) => @@ -3494,9 +3667,7 @@ namespace Barotrauma RagdollParams ragdollParams = character.AnimController.RagdollParams; ragdollParams.LimbScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE); ragdollParams.JointScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE); - var pos = character.WorldPosition; - character.AnimController.Recreate(); - character.TeleportTo(pos); + character.AnimController.RecreateAndRespawn(); }, isCheat: true)); commands.Add(new Command("recreateragdoll", "Recreate the ragdoll of the controlled character. Provide id or name if you want to target another character.", (string[] args) => @@ -3507,21 +3678,43 @@ namespace Barotrauma ThrowError("Not controlling any character!"); return; } - var pos = character.WorldPosition; - character.AnimController.Recreate(); - character.TeleportTo(pos); - }, isCheat: true)); + character.AnimController.RecreateAndRespawn(); + }, isCheat: true, + getValidArgs: () => new[] { GetSpawnedSpeciesNames() })); - commands.Add(new Command("resetragdoll", "Reset the ragdoll of the controlled character. Provide id or name if you want to target another character.", (string[] args) => + commands.Add(new Command("resetragdoll", "Reset the ragdoll of the controlled character (and all of the same species). Provide species name if you want to target another character.", (string[] args) => { - var character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, true); - if (character == null) + IEnumerable characters; + if (args.Length == 0) { - ThrowError("Not controlling any character!"); + if (Character.Controlled == null) + { + ThrowError("Invalid species name! Press [TAB] to get valid options in this context."); + return; + } + // Reset all characters of the same species, because the same params affect them too. + characters = FindMatchingSpecies(Character.Controlled.SpeciesName.ToString()); + } + else + { + characters = FindMatchingSpecies(args); + } + if (characters.None()) + { + ThrowError("Invalid species name!"); return; } - character.AnimController.ResetRagdoll(forceReload: true); - }, isCheat: true)); + characters.ForEach(c => c.AnimController.ResetRagdoll()); + foreach (Character character in characters) + { + // Variant scale multiplier doesn't work without recreating the ragdoll. + if (!character.VariantOf.IsEmpty) + { + character.AnimController.RecreateAndRespawn(); + } + } + }, isCheat: true, + getValidArgs: () => new[] { GetSpawnedSpeciesNames() })); commands.Add(new Command("loadanimation", "Loads an animation variation by name for the controlled character. The animation file has to be in the correct animations folder. Note: the changes are not saved!", (string[] args) => { @@ -3634,7 +3827,7 @@ namespace Barotrauma ThrowError("Cannot use the flipx command while playing online."); return; } - if (Submarine.MainSub.SubBody != null) { Submarine.MainSub?.FlipX(); } + if (Submarine.MainSub?.SubBody != null) { Submarine.MainSub.FlipX(); } }, isCheat: true)); commands.Add(new Command("head", "Load the head sprite and the wearables (hair etc). Required argument: head id. Optional arguments: hair index, beard index, moustache index, face attachment index.", args => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index d5687b38a..e5540cfa8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -549,6 +549,33 @@ namespace Barotrauma } public void ClientRead(IReadMessage msg) + { + if (GameMain.GameSession.IsRunning && !GameMain.Instance.LoadingScreenOpen) + { + ClientApplyNetworkMessage(msg); + } + else + { + //if the game session is not currently running (round still loading), + //we need to wait because the entities the status effect / conversation / etc targets may not exist yet + CoroutineManager.StartCoroutine(ApplyNetworkMessageWhenRoundLoaded(msg)); + } + } + + public IEnumerable ApplyNetworkMessageWhenRoundLoaded(IReadMessage msg) + { + while (GameMain.GameSession is { IsRunning: false } || GameMain.Instance.LoadingScreenOpen) + { + yield return new WaitForSeconds(1.0f); + } + if (GameMain.GameSession != null && GameMain.Client != null) + { + ClientApplyNetworkMessage(msg); + } + yield return CoroutineStatus.Success; + } + + public void ClientApplyNetworkMessage(IReadMessage msg) { NetworkEventType eventType = (NetworkEventType)msg.ReadByte(); switch (eventType) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs index fa15748e0..0ac3d1fd6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -59,10 +59,13 @@ namespace Barotrauma if (character.Submarine != null && character.AIController is EnemyAIController enemyAi) { enemyAi.UnattackableSubmarines.Add(character.Submarine); - enemyAi.UnattackableSubmarines.Add(Submarine.MainSub); - foreach (Submarine sub in Submarine.MainSub.DockedTo) + if (Submarine.MainSub != null) { - enemyAi.UnattackableSubmarines.Add(sub); + enemyAi.UnattackableSubmarines.Add(Submarine.MainSub); + foreach (Submarine sub in Submarine.MainSub.DockedTo) + { + enemyAi.UnattackableSubmarines.Add(sub); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs index 291366b9b..6c92275b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs @@ -1,7 +1,17 @@ -namespace Barotrauma +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System.Collections.Generic; + +namespace Barotrauma { partial class CombatMission : Mission { + private readonly Dictionary clientKills = new Dictionary(); + private readonly Dictionary clientDeaths = new Dictionary(); + + private readonly Dictionary botKills = new Dictionary(); + private readonly Dictionary botDeaths = new Dictionary(); + public override LocalizedString Description { get @@ -21,5 +31,93 @@ public override bool DisplayAsCompleted => false; public override bool DisplayAsFailed => false; + + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels + { + get + { + if (targetSubmarine == null) + { + yield break; + } + else + { + yield return (targetSubmarineSonarLabel is { Loaded: true } ? targetSubmarineSonarLabel : targetSubmarine.Info.DisplayName, targetSubmarine.WorldPosition); + } + } + } + + public static Color GetTeamColor(CharacterTeamType teamID) + { + if (teamID == CharacterTeamType.Team1) + { + return GUIStyle.GetComponentStyle("CoalitionIcon")?.Color ?? GUIStyle.Blue; + } + else if (teamID == CharacterTeamType.Team2) + { + return GUIStyle.GetComponentStyle("SeparatistIcon")?.Color ?? GUIStyle.Orange; + } + return Color.White; + } + + public int GetClientKillCount(Client client) + { + if (clientKills.TryGetValue(client.SessionId, out int kills)) + { + return kills; + } + return 0; + } + + public int GetClientDeathCount(Client client) + { + if (clientDeaths.TryGetValue(client.SessionId, out int deaths)) + { + return deaths; + } + return 0; + } + + public int GetBotKillCount(CharacterInfo botInfo) + { + if (botKills.TryGetValue(botInfo.ID, out int kills)) + { + return kills; + } + return 0; + } + + public int GetBotDeathCount(CharacterInfo botInfo) + { + if (botDeaths.TryGetValue(botInfo.ID, out int deaths)) + { + return deaths; + } + return 0; + } + + public override void ClientRead(IReadMessage msg) + { + base.ClientRead(msg); + Scores[0] = msg.ReadUInt16(); + Scores[1] = msg.ReadUInt16(); + + uint clientCount = msg.ReadVariableUInt32(); + for (int i = 0; i < clientCount; i++) + { + byte clientId = msg.ReadByte(); + clientDeaths[clientId] = (int)msg.ReadVariableUInt32(); + clientKills[clientId] = (int)msg.ReadVariableUInt32(); + } + + uint botCount = msg.ReadVariableUInt32(); + for (int i = 0; i < botCount; i++) + { + ushort botId = msg.ReadUInt16(); + botDeaths[botId] = (int)msg.ReadVariableUInt32(); + botKills[botId] = (int)msg.ReadVariableUInt32(); + } + + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/DeathPrompt.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/DeathPrompt.cs index f1cc3b397..817fc30fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/DeathPrompt.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/DeathPrompt.cs @@ -58,7 +58,7 @@ internal class DeathPrompt const float FadeInDuration = 1.0f; bool permadeath = GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath }; - bool ironman = GameMain.NetworkMember is { ServerSettings: { RespawnMode: RespawnMode.Permadeath, IronmanMode: true } }; + bool ironman = GameMain.NetworkMember is { ServerSettings.IronmanModeActive: true }; var background = new GUICustomComponent(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), onDraw: DrawBackground) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index d0b10b1e3..c53cc34d1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Barotrauma.IO; using System.Linq; -using System.Text; using Barotrauma.Extensions; namespace Barotrauma @@ -84,16 +83,31 @@ namespace Barotrauma { currentDirectory += "/"; } - fileSystemWatcher?.Dispose(); - fileSystemWatcher = new System.IO.FileSystemWatcher(currentDirectory) + try { - Filter = "*", - NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName - }; - fileSystemWatcher.Created += OnFileSystemChanges; - fileSystemWatcher.Deleted += OnFileSystemChanges; - fileSystemWatcher.Renamed += OnFileSystemChanges; - fileSystemWatcher.EnableRaisingEvents = true; + fileSystemWatcher?.Dispose(); + fileSystemWatcher = new System.IO.FileSystemWatcher(currentDirectory) + { + Filter = "*", + NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName + }; + fileSystemWatcher.Created += OnFileSystemChanges; + fileSystemWatcher.Deleted += OnFileSystemChanges; + fileSystemWatcher.Renamed += OnFileSystemChanges; + fileSystemWatcher.EnableRaisingEvents = true; + } + catch (System.IO.FileNotFoundException exception) + { + DebugConsole.ThrowError("Failed to set the current directory, possibly due to insufficient access permissions.", exception); + } + catch (ArgumentException exception) + { + DebugConsole.ThrowError("Failed to set the current directory, possibly because it was deleted.", exception); + } + catch (Exception exception) + { + DebugConsole.ThrowError("Failed to set the current directory for an unknown reason.", exception); + } RefreshFileList(); } } @@ -218,6 +232,14 @@ namespace Barotrauma { if (Directory.Exists(txt)) { + var attributes = System.IO.File.GetAttributes(txt); + if (attributes.HasAnyFlag(System.IO.FileAttributes.System) || attributes.HasAnyFlag(System.IO.FileAttributes.Hidden)) + { + // System and hidden folders should be filtered out when populating the options, but the user can still write or copy-paste the path in the text field, + // which will throw a file not found exception when the file system watcher starts. Therefore, this extra check. + tb.Text = CurrentDirectory; + return false; + } CurrentDirectory = txt; return true; } @@ -354,20 +376,19 @@ namespace Barotrauma var directories = Directory.EnumerateDirectories(currentDirectory, "*" + filterBox!.Text + "*"); foreach (var directory in directories) { - string txt = directory; - if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); } - if (!txt.EndsWith("/")) { txt += "/"; } - //get directory info - DirectoryInfo dirInfo = new DirectoryInfo(directory); try { - //this will throw an exception if the directory can't be opened - Directory.GetDirectories(directory); + //this will intentionally throw an exception if the directory can't be opened + System.IO.Directory.GetDirectories(directory); } catch (UnauthorizedAccessException) { + // Skip the folders that can't be accessed. continue; } + string txt = directory; + if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); } + if (!txt.EndsWith("/")) { txt += "/"; } var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt) { UserData = ItemIsDirectory.Yes diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index e03511a3e..99db34df3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -469,7 +469,7 @@ namespace Barotrauma "Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); soundTextY += yStep; - for (int i = 0; i < SoundManager.SOURCE_COUNT; i++) + for (int i = 0; i < SoundManager.SourceCount; i++) { Color clr = Color.White; string soundStr = i + ": "; @@ -1546,7 +1546,7 @@ namespace Barotrauma private static readonly VertexPositionColorTexture[] donutVerts = new VertexPositionColorTexture[DonutSegments * 4]; public static void DrawDonutSection( - SpriteBatch sb, Vector2 center, Range radii, float sectionRad, Color clr, float depth = 0.0f) + SpriteBatch sb, Vector2 center, Range radii, float sectionRad, Color clr, float depth = 0.0f, float rotationRad = 0.0f) { float getRadius(int vertexIndex) => (vertexIndex % 4) switch @@ -1589,7 +1589,7 @@ namespace Barotrauma for (int vertexIndex = 0; vertexIndex < maxDirectionIndex * 4; vertexIndex++) { donutVerts[vertexIndex].Color = clr; - donutVerts[vertexIndex].Position = new Vector3(center + getDirection(vertexIndex) * getRadius(vertexIndex), 0.0f); + donutVerts[vertexIndex].Position = new Vector3(center + Vector2.Transform(getDirection(vertexIndex) * getRadius(vertexIndex), Matrix.CreateRotationZ(rotationRad)), 0.0f); } sb.Draw(solidWhiteTexture, donutVerts, depth, count: maxDirectionIndex); } @@ -1856,9 +1856,16 @@ namespace Barotrauma Vector2 pos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) - new Vector2(HUDLayoutSettings.Padding) - 2 * Scale * sheet.FrameSize.ToVector2(); sheet.Draw(spriteBatch, (int)Math.Floor(savingIndicatorSpriteIndex), pos, savingIndicatorColor, origin: Vector2.Zero, rotate: 0.0f, scale: new Vector2(Scale)); } -#endregion -#region Element creation + public static void DrawCapsule(SpriteBatch sb, Vector2 origin, float length, float radius, float rotation, Color clr, float depth = 0, float thickness = 1) + { + DrawDonutSection(sb, origin + Vector2.Transform(-new Vector2(length / 2, 0), Matrix.CreateRotationZ(rotation)), new Range(radius - thickness / 2, radius + thickness / 2), MathHelper.Pi, clr, depth, rotation - MathHelper.Pi); + DrawRectangle(sb, origin, new Vector2(length, radius * 2), new Vector2(length / 2, radius), rotation, clr, depth, thickness); + DrawDonutSection(sb, origin + Vector2.Transform(new Vector2(length / 2, 0), Matrix.CreateRotationZ(rotation)), new Range(radius - thickness / 2, radius + thickness / 2), MathHelper.Pi, clr, depth, rotation); + } + #endregion + + #region Element creation public static Texture2D CreateCircle(int radius, bool filled = false) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index c5e1a2b81..da33ae1cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -159,6 +159,34 @@ namespace Barotrauma } } + private GUIComponent holdOverlay; + + private bool requireHold; + public bool RequireHold + { + get => requireHold; + set + { + requireHold = value; + if (value) + { + holdOverlay ??= new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), Frame.RectTransform, Anchor.CenterLeft), style: null) + { + Color = GUIStyle.Yellow * 0.33f, + CanBeFocused = false, + IgnoreLayoutGroups = true, + Visible = true + }; + } + else if (holdOverlay != null) + { + holdOverlay.Visible = false; + } + } + } + public float HoldDurationSeconds { get; set; } = 5f; + private float holdTimer; + public bool Pulse { get; set; } private float pulseTimer; private float pulseExpand; @@ -220,7 +248,7 @@ namespace Barotrauma Rectangle expandRect = Rect; float expand = (pulseExpand * 20.0f) * GUI.Scale; expandRect.Inflate(expand, expand); - + GUIStyle.EndRoundButtonPulse.Draw(spriteBatch, expandRect, ToolBox.GradientLerp(pulseExpand, Color.White, Color.White, Color.Transparent)); } } @@ -240,6 +268,11 @@ namespace Barotrauma } if (PlayerInput.PrimaryMouseButtonHeld()) { + if (RequireHold) + { + holdTimer += deltaTime; + } + if (OnPressed != null) { if (OnPressed()) @@ -254,25 +287,34 @@ namespace Barotrauma } else if (PlayerInput.PrimaryMouseButtonClicked()) { - if (PlaySoundOnSelect) + if (!RequireHold || holdTimer > HoldDurationSeconds) { - SoundPlayer.PlayUISound(ClickSound); - } - if (OnClicked != null) - { - if (OnClicked(this, UserData)) + if (PlaySoundOnSelect) { - State = ComponentState.Selected; + SoundPlayer.PlayUISound(ClickSound); + } + + if (OnClicked != null) + { + if (OnClicked(this, UserData)) + { + State = ComponentState.Selected; + } + } + else + { + Selected = !Selected; } } - else - { - Selected = !Selected; - } + } + else + { + holdTimer = 0.0f; } } else { + holdTimer = 0.0f; if (!ExternalHighlight) { State = Selected ? ComponentState.Selected : ComponentState.None; @@ -283,6 +325,20 @@ namespace Barotrauma } } + if (RequireHold) + { + float width = MathHelper.Clamp(holdTimer / HoldDurationSeconds, 0f, 1f); + if (!MathUtils.NearlyEqual(width, holdOverlay.RectTransform.RelativeSize.X)) + { + holdOverlay.RectTransform.RelativeSize = new Vector2(width, 1f); + } + + holdOverlay.Color = + holdTimer >= HoldDurationSeconds + ? Color.Green * 0.33f + : Color.Red * 0.33f; + } + foreach (GUIComponent child in Children) { child.State = State; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 5185b7b63..3116cfd81 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -9,8 +9,21 @@ namespace Barotrauma { public class GUIDropDown : GUIComponent, IKeyboardSubscriber { + /// The component that was selected from the dropdown. + /// of the component selected from the dropdown. public delegate bool OnSelectedHandler(GUIComponent selected, object obj = null); + /// + /// Triggers when some item is cliecked from the dropdown. + /// Note that is not set yet when this callback triggers, and returning false from the callback disallows selecting it. + /// If you want to access the new value, use the obj argument. + /// public OnSelectedHandler OnSelected; + + /// + /// Triggers after an item has been selected from the dropdown, all validation has been done and the new value has been set. + /// + public OnSelectedHandler AfterSelected; + public OnSelectedHandler OnDropped; private readonly GUIButton button; @@ -166,7 +179,7 @@ namespace Barotrauma public Vector4 Padding => button.TextBlock.Padding; - public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false, Alignment textAlignment = Alignment.CenterLeft) : base(style, rectT) + public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false, Alignment textAlignment = Alignment.CenterLeft, float listBoxScale = 1) : base(style, rectT) { text ??= LocalizedString.EmptyString; @@ -185,13 +198,21 @@ namespace Barotrauma Anchor listAnchor = dropAbove ? Anchor.TopCenter : Anchor.BottomCenter; Pivot listPivot = dropAbove ? Pivot.BottomCenter : Pivot.TopCenter; - listBox = new GUIListBox(new RectTransform(new Point(Rect.Width, Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, listAnchor, listPivot) + listBox = new GUIListBox(new RectTransform(new Point((int)(Rect.Width * listBoxScale), Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, listAnchor, listPivot) { IsFixedSize = false }, style: null) { Enabled = !selectMultiple, PlaySoundOnSelect = true, }; - if (!selectMultiple) { listBox.OnSelected = SelectItem; } + if (!selectMultiple) + { + listBox.AfterSelected = (component, obj) => + { + SelectItem(component, obj); + AfterSelected?.Invoke(component, obj); + return true; + }; + } GUIStyle.Apply(listBox, "GUIListBox", this); GUIStyle.Apply(listBox.ContentBackground, "GUIListBox", this); @@ -199,6 +220,8 @@ namespace Barotrauma { icon = new GUIImage(new RectTransform(new Vector2(0.6f, 0.6f), button.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5, 0) }, null, scaleToFit: true); icon.ApplyStyle(button.Style.ChildStyles["dropdownicon".ToIdentifier()]); + //move the text away from the icon + button.TextBlock.Padding += new Vector4(0, 0, icon.Rect.Width, 0); } currentHighestParent = FindHighestParent(); @@ -249,12 +272,12 @@ namespace Barotrauma toolTip ??= ""; if (selectMultiple) { - var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, style: "ListBoxElement", color: color) + var frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, style: "ListBoxElement", color: color) { UserData = userData, ToolTip = toolTip }; - new GUITickBox(new RectTransform(new Vector2(1.0f, 0.8f), frame.RectTransform, anchor: Anchor.CenterLeft) { MaxSize = new Point(int.MaxValue, (int)(button.Rect.Height * 0.8f)) }, text) + var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.8f), frame.RectTransform, anchor: Anchor.CenterLeft) { MaxSize = new Point(int.MaxValue, (int)(button.Rect.Height * 0.8f)) }, text) { UserData = userData, ToolTip = toolTip, @@ -266,6 +289,11 @@ namespace Barotrauma return false; } + if (OnSelected != null && !OnSelected.Invoke(tb.Parent, tb.Parent.UserData)) + { + return false; + } + List texts = new List(); selectedDataMultiple.Clear(); selectedIndexMultiple.Clear(); @@ -282,8 +310,7 @@ namespace Barotrauma i++; } button.Text = LocalizedString.Join(", ", texts); - // TODO: The callback is called at least twice, remove this? - OnSelected?.Invoke(tb.Parent, tb.Parent.UserData); + AfterSelected?.Invoke(tb.Parent, SelectedData); return true; } }; @@ -291,7 +318,7 @@ namespace Barotrauma } else { - return new GUITextBlock(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, text, style: "ListBoxElement", color: color, textColor: textColor) + return new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, text, style: "ListBoxElement", color: color, textColor: textColor) { UserData = userData, ToolTip = toolTip @@ -328,9 +355,8 @@ namespace Barotrauma } button.Text = textBlock?.Text ?? ""; } + OnSelected?.Invoke(component, obj); Dropped = false; - // TODO: OnSelected can be called multiple times and when it shouldn't be called -> turn into an event so that nobody else can call it. - OnSelected?.Invoke(component, component.UserData); return true; } @@ -344,6 +370,7 @@ namespace Barotrauma { listBox.Select(userData); } + AfterSelected?.Invoke(SelectedComponent, SelectedData); } public void Select(int index) @@ -360,6 +387,7 @@ namespace Barotrauma { listBox.Select(index); } + AfterSelected?.Invoke(this, SelectedData); } private bool wasOpened; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index aa1a6a882..9a944ff5f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -14,8 +14,17 @@ namespace Barotrauma protected List selected; public delegate bool OnSelectedHandler(GUIComponent component, object obj); + /// + /// Triggers when some element is clicked on the listbox. + /// Note that is not set yet when this callback triggers, and returning false from the callback disallows selecting it. + /// public OnSelectedHandler OnSelected; + /// + /// Triggers after some element has been selected from the listbox. + /// + public OnSelectedHandler AfterSelected; + public delegate object CheckSelectedHandler(); public CheckSelectedHandler CheckSelected; @@ -1151,6 +1160,8 @@ namespace Barotrauma { SoundPlayer.PlayUISound(GUISoundType.Select); } + + AfterSelected?.Invoke(child, SelectedData); } public void Select(IEnumerable children) @@ -1160,8 +1171,9 @@ namespace Barotrauma selected.Clear(); selected.AddRange(children.Where(c => Content.Children.Contains(c))); foreach (var child in selected) { OnSelected?.Invoke(child, child.UserData); } + AfterSelected?.Invoke(children.FirstOrDefault(), SelectedData); } - + public void Deselect() { Selected = false; @@ -1172,6 +1184,15 @@ namespace Barotrauma selected.Clear(); } + public void DeselectElement(GUIComponent child) + { + if (child == null) { return; } + if (selected.Contains(child)) + { + selected.Remove(child); + } + } + public void UpdateScrollBarSize() { scrollBarNeedsRecalculation = false; @@ -1268,7 +1289,7 @@ namespace Barotrauma ContentBackground.DrawManually(spriteBatch, alsoChildren: false); Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; - if (HideChildrenOutsideFrame) + if (HideChildrenOutsideFrame && Content.CountChildren > 0) { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, Content.Rect); @@ -1306,7 +1327,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, drawRect, Color.White * 0.5f, thickness: 2f); } - if (HideChildrenOutsideFrame) + if (HideChildrenOutsideFrame && Content.CountChildren > 0) { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index ffb851a97..019548557 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -51,6 +51,16 @@ namespace Barotrauma public readonly static GUISprite InteractionLabelBackground = new GUISprite("InteractionLabelBackground"); public readonly static GUISprite BrokenIcon = new GUISprite("BrokenIcon"); public readonly static GUISprite YouAreHereCircle = new GUISprite("YouAreHereCircle"); + + public readonly static GUISprite SubLocationIcon = new GUISprite("SubLocationIcon"); + public readonly static GUISprite ShuttleIcon = new GUISprite("ShuttleIcon"); + public readonly static GUISprite WreckIcon = new GUISprite("WreckIcon"); + public readonly static GUISprite CaveIcon = new GUISprite("CaveIcon"); + public readonly static GUISprite OutpostIcon = new GUISprite("OutpostIcon"); + public readonly static GUISprite RuinIcon = new GUISprite("RuinIcon"); + public readonly static GUISprite EnemyIcon = new GUISprite("EnemyIcon"); + public readonly static GUISprite CorpseIcon = new GUISprite("CorpseIcon"); + public readonly static GUISprite BeaconIcon = new GUISprite("BeaconIcon"); public readonly static GUISprite Radiation = new GUISprite("Radiation"); public readonly static GUISpriteSheet RadiationAnimSpriteSheet = new GUISpriteSheet("RadiationAnimSpriteSheet"); @@ -71,7 +81,7 @@ namespace Barotrauma public readonly static GUISprite EndRoundButtonPulse = new GUISprite("EndRoundButtonPulse"); public readonly static GUISpriteSheet FocusIndicator = new GUISpriteSheet("FocusIndicator"); - + public readonly static GUISprite IconOverflowIndicator = new GUISprite("IconOverflowIndicator"); /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 6fc380b99..dac7ac2d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -742,8 +742,8 @@ namespace Barotrauma private (LocalizedString header, LocalizedString body) GetItemTransferWarningText() { - var header = TextManager.Get("itemtransferheader").Fallback("lowfuelheader", useDefaultLanguageIfFound: false); - var body = TextManager.Get("itemtransferwarning").Fallback("lowfuelwarning", useDefaultLanguageIfFound: false); + var header = TextManager.Get("itemtransferheader").Fallback(TextManager.Get("lowfuelheader"), useDefaultLanguageIfFound: false); + var body = TextManager.Get("itemtransferwarning").Fallback(TextManager.Get("lowfuelwarning"), useDefaultLanguageIfFound: false); return (header, body); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index b71854188..8d7625d3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -416,7 +416,10 @@ namespace Barotrauma => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)MathF.Round(value)}"); } - var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine"); + if (Submarine.MainSub != null) + { + createTabButton(InfoFrameTab.Submarine, "submarine"); + } var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.character"); talentsButton.OnAddedToGUIUpdateList += (component) => @@ -463,12 +466,14 @@ namespace Barotrauma } } - private const float jobColumnWidthPercentage = 0.138f, - characterColumnWidthPercentage = 0.45f, - pingColumnWidthPercentage = 0.206f, - walletColumnWidthPercentage = 0.206f; + private const float JobColumnWidthPercentage = 0.138f, + CharacterColumnWidthPercentage = 0.45f, + KillColumnWidthPercentage = 0.1f, + DeathColumnWidthPercentage = 0.1f, + PingColumnWidthPercentage = 0.15f, + WalletColumnWidthPercentage = 0.206f; - private int jobColumnWidth, characterColumnWidth, pingColumnWidth, walletColumnWidth; + private int jobColumnWidth, characterColumnWidth, pingColumnWidth, walletColumnWidth, deathColumnWidth, killColumnWidth; private void CreateCrewListFrame(GUIFrame crewFrame) { @@ -496,11 +501,21 @@ namespace Barotrauma { if (teamIDs.Count > 1) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: i == 0 ? GUIStyle.Green : GUIStyle.Orange) { ForceUpperCase = ForceUpperCase.Yes }; + var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: CombatMission.GetTeamColor(teamIDs[i])) + { + ForceUpperCase = ForceUpperCase.Yes + }; + var teamIcon = new GUIImage(new RectTransform(Vector2.One, nameText.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight), + style: teamIDs[i] == CharacterTeamType.Team2 ? "SeparatistIcon" : "CoalitionIcon") + { + Color = nameText.TextColor + }; + nameText.Padding = new Vector4(teamIcon.Rect.Width + nameText.Padding.X, nameText.Padding.Y, nameText.Padding.Z, nameText.Padding.W); } headerFrames[i] = new GUILayoutGroup(new RectTransform(Vector2.Zero, content.RectTransform, Anchor.TopLeft, Pivot.BottomLeft) { AbsoluteOffset = new Point(2, -1) }, isHorizontal: true) { + Stretch = true, AbsoluteSpacing = 2, UserData = i }; @@ -587,8 +602,8 @@ namespace Barotrauma sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width; - jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f); - characterButton.RectTransform.RelativeSize = new Vector2((1f - jobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f); + jobButton.RectTransform.RelativeSize = new Vector2(JobColumnWidthPercentage * sizeMultiplier, 1f); + characterButton.RectTransform.RelativeSize = new Vector2((1f - JobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f); jobButton.TextBlock.Font = characterButton.TextBlock.Font = GUIStyle.HotkeyFont; jobButton.CanBeFocused = characterButton.CanBeFocused = false; @@ -626,7 +641,8 @@ namespace Barotrauma var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) { - AbsoluteSpacing = 2 + AbsoluteSpacing = 2, + Stretch = true }; new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect)) @@ -639,21 +655,29 @@ namespace Barotrauma GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); + paddedFrame.Recalculate(); + linkedGUIList.Add(new LinkedGUI(character, frame, textBlock: null)); } private void CreateMultiPlayerListContentHolder(GUILayoutGroup headerFrame) { bool isCampaign = GameMain.GameSession?.Campaign is MultiPlayerCampaign; - GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale"); - GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale"); - GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale"); + GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(JobColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale"); + GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(CharacterColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale"); + + if (GameMain.GameSession?.GameMode is PvPMode) + { + var killButton = new GUIButton(new RectTransform(new Vector2(KillColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("killcount"), style: "GUIButtonSmallFreeScale"); + killColumnWidth = killButton.Rect.Width; + var deathButton = new GUIButton(new RectTransform(new Vector2(DeathColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("deathcount"), style: "GUIButtonSmallFreeScale"); + deathColumnWidth = deathButton.Rect.Width; + } + + GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(PingColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale"); if (isCampaign) { - GUIButton walletButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform) - { - RelativeSize = new Vector2(walletColumnWidthPercentage * sizeMultiplier, 1f) - }, TextManager.Get("crewwallet.wallet"), style: "GUIButtonSmallFreeScale") + GUIButton walletButton = new GUIButton(new RectTransform(new Vector2(WalletColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("crewwallet.wallet"), style: "GUIButtonSmallFreeScale") { TextBlock = { Font = GUIStyle.HotkeyFont }, CanBeFocused = false, @@ -662,15 +686,12 @@ namespace Barotrauma walletColumnWidth = walletButton.Rect.Width; } - sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width; - - jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f); - characterButton.RectTransform.RelativeSize = new Vector2((characterColumnWidthPercentage + (isCampaign ? 0 : walletColumnWidthPercentage)) * sizeMultiplier, 1f); - pingButton.RectTransform.RelativeSize = new Vector2(pingColumnWidthPercentage * sizeMultiplier, 1f); - - jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUIStyle.HotkeyFont; - jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = false; - jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = ForceUpperCase.Yes; + foreach (var btn in headerFrame.GetAllChildren()) + { + btn.TextBlock.Font = GUIStyle.HotkeyFont; + btn.CanBeFocused = false; + btn.ForceUpperCase = ForceUpperCase.Yes; + } jobColumnWidth = jobButton.Rect.Width; characterColumnWidth = characterButton.Rect.Width; @@ -692,7 +713,7 @@ namespace Barotrauma { foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i])) { - if (!(character is AICharacter) && connectedClients.Any(c => c.Character == null && c.Name == character.Name)) { continue; } + if (character is not AICharacter && connectedClients.Any(c => c.Character == null && c.Name == character.Name)) { continue; } CreateMultiPlayerCharacterElement(character, GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.Character == character), i); } } @@ -723,7 +744,8 @@ namespace Barotrauma var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) { - AbsoluteSpacing = 2 + AbsoluteSpacing = 2, + Stretch = true }; new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect)) @@ -736,6 +758,19 @@ namespace Barotrauma if (client != null) { CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon); + + if (GameMain.GameSession?.GameMode is PvPMode) + { + new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center) + { + TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientKillCount(client) ?? 0).ToString() + }; + new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center) + { + TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientDeathCount(client) ?? 0).ToString() + }; + } + linkedGUIList.Add(new LinkedGUI(client, frame, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center), permissionIcon)); @@ -745,6 +780,18 @@ namespace Barotrauma GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); + if (GameMain.GameSession?.GameMode is PvPMode) + { + new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center) + { + TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetBotKillCount(character.Info) ?? 0).ToString() + }; + new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center) + { + TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetBotDeathCount(character.Info) ?? 0).ToString() + }; + } + if (character is AICharacter) { linkedGUIList.Add(new LinkedGUI(character, frame, @@ -764,6 +811,8 @@ namespace Barotrauma } CreateWalletCrewFrame(character, paddedFrame); + + paddedFrame.Recalculate(); } private void CreateMultiPlayerClientElement(Client client) @@ -785,7 +834,8 @@ namespace Barotrauma var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) { - AbsoluteSpacing = 2 + AbsoluteSpacing = 2, + Stretch = true }; new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), @@ -797,6 +847,19 @@ namespace Barotrauma }; CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon); + + if (GameMain.GameSession?.GameMode is PvPMode) + { + new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center) + { + TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientKillCount(client) ?? 0).ToString() + }; + new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center) + { + TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientDeathCount(client) ?? 0).ToString() + }; + } + linkedGUIList.Add(new LinkedGUI(client, frame, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center), permissionIcon)); @@ -805,6 +868,8 @@ namespace Barotrauma { CreateWalletCrewFrame(character, paddedFrame); } + + paddedFrame.Recalculate(); } private int GetTeamIndex(Client client) @@ -842,7 +907,7 @@ namespace Barotrauma private void CreateWalletCrewFrame(Character character, GUILayoutGroup paddedFrame) { - if (!(GameMain.GameSession?.Campaign is MultiPlayerCampaign)) { return; } + if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign) { return; } GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(new Point(walletColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.Center) { @@ -947,7 +1012,6 @@ namespace Barotrauma float iconWidth = iconSize.X / (float)characterColumnWidth; int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUIStyle.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width); permissionIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 1f), paddedFrame.RectTransform) { AbsoluteOffset = new Point(xOffset + 2, 0) }, permissionIconSprite) { IgnoreLayoutGroups = true }; - if (client.Character != null && client.Character.IsDead) { @@ -977,10 +1041,7 @@ namespace Barotrauma } else if (client.Character != null && client.Character.IsDead) { - if (client.Character.Info != null) - { - client.Character.Info.DrawJobIcon(spriteBatch, area); - } + client.Character.Info?.DrawJobIcon(spriteBatch, area); } else { @@ -1663,6 +1724,8 @@ namespace Barotrauma private static void CreateSubmarineInfo(GUIFrame infoFrame, Submarine sub) { + if (sub == null) { return; } + GUIFrame subInfoFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); GUIFrame paddedFrame = new GUIFrame(new RectTransform(Vector2.One * 0.97f, subInfoFrame.RectTransform, Anchor.Center), style: null); @@ -1765,7 +1828,7 @@ namespace Barotrauma { parent.Content.ClearChildren(); List skillNames = new List(); - foreach (Skill skill in info.Job.GetSkills()) + foreach (Skill skill in info.Job.GetSkills().OrderByDescending(static s => s.Level)) { GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.0f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = true }; var skillName = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}").Fallback(skill.Identifier.Value)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index 1b6fad62f..b7b910965 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -7,6 +7,7 @@ using System.Linq; using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using static Barotrauma.TalentTree; using static Barotrauma.TalentTree.TalentStages; @@ -83,6 +84,10 @@ namespace Barotrauma private GUIButton? talentApplyButton, talentResetButton; + private delegate void StartAnimation(RectangleF start, RectangleF end, float duration); + private StartAnimation? startAnimation; + private GUIComponent? talentMainArea; + public void CreateGUI(GUIFrame parent, Character? targetCharacter) { parent.ClearChildren(); @@ -136,7 +141,7 @@ namespace Barotrauma GUILayoutGroup playerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), containerFrame.RectTransform, Anchor.TopCenter)); GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); - if (!GameMain.NetLobbyScreen.PermadeathMode) + if (!GameMain.NetLobbyScreen.PermadeathMode && GameMain.GameSession?.GameMode is not PvPMode) { GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall") @@ -341,7 +346,15 @@ namespace Barotrauma private void CreateTalentMenu(GUIComponent parent, CharacterInfo info, TalentTree tree) { - GUIListBox mainList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), parent.RectTransform, anchor: Anchor.TopCenter)); + talentMainArea = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), parent.RectTransform, Anchor.TopCenter), style: null ); + + GUIListBox mainList = new GUIListBox(new RectTransform(Vector2.One, talentMainArea.RectTransform)); + startAnimation = CreatePopupAnimationHandler(talentMainArea); + + if (info is { TalentRefundPoints: > 0, ShowTalentResetPopupOnOpen: true }) + { + CreateTalentResetPopup(talentMainArea); + } selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet(); @@ -425,6 +438,124 @@ namespace Barotrauma } } + private void CreateTalentResetPopup(GUIComponent parent) + { + bool hasResetTalentsBefore = character?.Info.TalentResetCount > 0; + var bgBlocker = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, anchor: Anchor.Center), style: "GUIBackgroundBlocker") + { + IgnoreLayoutGroups = true + }; + + var popup = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.8f), bgBlocker.RectTransform, Anchor.Center)); + + var popupLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(popup.RectTransform, 0.95f), popup.RectTransform, Anchor.Center), isHorizontal: false); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), popupLayout.RectTransform), TextManager.Get("talentresetheader"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Vector2(1.0f, hasResetTalentsBefore ? 0.25f : 0.5f), popupLayout.RectTransform), TextManager.Get("talentresetprompt"), wrap: true); + + if (hasResetTalentsBefore) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), popupLayout.RectTransform), TextManager.Get("talentresetpromptwarning"), wrap: true) + { + TextColor = GUIStyle.Red + }; + } + + var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), popupLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true); + + var confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), TextManager.Get("holdtoconfirm")) + { + RequireHold = true, + HoldDurationSeconds = 1.5f, + OnClicked = (button, o) => + { + if (character is null || characterInfo is null) { return false; } + + characterInfo.RefundTalents(); + selectedTalents.Clear(); + UpdateTalentInfo(); + bgBlocker.Visible = false; + return true; + } + }; + var denyButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), TextManager.Get("decidelater")) + { + RequireHold = false, + OnClicked = (button, userData) => + { + if (talentResetButton is not { } resetButton) { return false; } + startAnimation?.Invoke(popup.Rect, resetButton.Rect, 0.25f); + resetButton.Flash(GUIStyle.Green); + bgBlocker.Visible = false; + if (characterInfo != null) + { + characterInfo.ShowTalentResetPopupOnOpen = false; + } + return true; + } + }; + } + + private static StartAnimation CreatePopupAnimationHandler(GUIComponent parent) + { + bool drawAnimation = false; + + float animDur = 1f, + animTimer = 0f; + + RectangleF drawRect = RectangleF.Empty, + animStartRect = RectangleF.Empty, + animEndRect = RectangleF.Empty; + + void StartAnimation(RectangleF start, RectangleF end, float duration) + { + animStartRect = start; + animEndRect = end; + animTimer = 0; + animDur = duration; + drawRect = start; + drawAnimation = true; + } + + void OnDraw(SpriteBatch batch, GUICustomComponent component) + { + if (!drawAnimation) { return; } + + GUIComponentStyle style = GUIStyle.GetComponentStyle("GUIFrame"); + + style.Sprites[GUIComponent.ComponentState.None][0].Draw(batch, drawRect, Color.White); + } + + void OnUpdate(float f, GUICustomComponent component) + { + if (!drawAnimation) { return; } + + animTimer += f; + if (animTimer > animDur) + { + drawRect = animEndRect; + drawAnimation = false; + return; + } + + float lerp = animTimer / animDur; + + drawRect = new RectangleF( + MathHelper.Lerp(animStartRect.X, animEndRect.X, lerp), + MathHelper.Lerp(animStartRect.Y, animEndRect.Y, lerp), + MathHelper.Lerp(animStartRect.Width, animEndRect.Width, lerp), + MathHelper.Lerp(animStartRect.Height, animEndRect.Height, lerp)); + } + + new GUICustomComponent(new RectTransform(Vector2.One, parent.RectTransform), onDraw: OnDraw, onUpdate: OnUpdate) + { + IgnoreLayoutGroups = true, + CanBeFocused = false + }; + + return StartAnimation; + } + private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree, int index, TalentOption talentOption, CharacterInfo info, int specializationCount) { int elementPadding = GUI.IntScale(8); @@ -679,6 +810,15 @@ namespace Barotrauma private bool ResetTalentSelection(GUIButton guiButton, object userData) { if (characterInfo is null) { return false; } + + int newTalentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); + // if we don't have talents selected, and we have points to refund, show the refund popup + if (characterInfo.TalentRefundPoints > 0 && newTalentCount == 0) + { + CreateTalentResetPopup(talentMainArea!); + return true; + } + selectedTalents = characterInfo.GetUnlockedTalentsInTree().ToHashSet(); UpdateTalentInfo(); return true; @@ -844,12 +984,31 @@ namespace Barotrauma } } + private static readonly LocalizedString refundText = TextManager.Get("refund"), + resetText = TextManager.Get("reset"); + public void Update() { if (characterInfo is null || talentResetButton is null || talentApplyButton is null) { return; } int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); - talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; + talentApplyButton.Enabled = talentCount > 0; + talentResetButton.Enabled = talentCount > 0 || characterInfo.TalentRefundPoints > 0; + + if (talentCount == 0 && characterInfo.TalentRefundPoints > 0) + { + if (talentResetButton.FlashTimer <= 0.0f) + { + talentResetButton.Flash(GUIStyle.Orange); + } + + talentResetButton.Text = refundText; + } + else + { + talentResetButton.Text = resetText; + } + if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f) { talentApplyButton.Flash(GUIStyle.Orange); @@ -893,6 +1052,22 @@ namespace Barotrauma return info.GetIdentifierUsingOriginalName() == ownCharacterInfo.GetIdentifierUsingOriginalName(); } + private static bool IsOnSameTeam(CharacterInfo? info) + { + if (info is null) { return false; } + + CharacterTeamType? ownCharacterTeam = Character.Controlled?.TeamID ?? GameMain.Client?.MyClient?.TeamID; + if (ownCharacterTeam is null) { return false; } + + return info.TeamID == ownCharacterTeam; + } + + private static bool IsSpectatingInMultiplayer() + { + if (GameMain.Client?.MyClient is not { } myClient) { return false; } + return myClient.Spectating; + } + public static bool CanManageTalents(CharacterInfo targetInfo) { // in singleplayer we can do whatever we want @@ -901,10 +1076,16 @@ namespace Barotrauma // always allow managing talents for own character if (IsOwnCharacter(targetInfo)) { return true; } + // disallow managing talents while spectating + if (IsSpectatingInMultiplayer()) { return false; } + // don't allow controlling non-bot characters if (targetInfo.Character is not { IsBot: true }) { return false; } - // lastly check if we have the permission to do this + // only allow managing talents for bots on the same team + if (!IsOnSameTeam(targetInfo)) { return false; } + + // lastly, check if we have the permission to do this return GameMain.Client is { } client && client.HasPermission(ClientPermissions.ManageBotTalents); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index fbfaca6a9..23b6ff533 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -779,7 +779,7 @@ namespace Barotrauma { try { - SaveUtil.LoadGame(saveFiles.OrderBy(file => file.SaveTime).Last().FilePath); + SaveUtil.LoadGame(CampaignDataPath.CreateRegular(saveFiles.OrderBy(file => file.SaveTime).Last().FilePath)); } catch (Exception e) { @@ -1145,7 +1145,7 @@ namespace Barotrauma } GameSession.Campaign?.End(); - SaveUtil.SaveGame(GameSession.SavePath); + SaveUtil.SaveGame(GameSession.DataPath); } if (Client != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 5a6ae2c90..cbfbba80e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -28,7 +28,7 @@ namespace Barotrauma public GUIComponent ReportButtonFrame { get; set; } private GUIFrame guiFrame; - private GUIFrame crewArea; + private GUILayoutGroup crewArea; private GUIListBox crewList; private float crewListOpenState; private bool _isCrewMenuOpen = true; @@ -93,12 +93,30 @@ namespace Barotrauma #region Crew Area - crewArea = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.CrewArea, guiFrame.RectTransform), style: null, color: Color.Transparent) + crewArea = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.CrewArea, guiFrame.RectTransform), childAnchor: Anchor.TopCenter) { - CanBeFocused = false + Stretch = true }; crewArea.RectTransform.NonScaledSize = HUDLayoutSettings.CrewArea.Size; + for (int i = 0; i < 2; i++) + { + CharacterTeamType teamId = i == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2; + var nameText = new GUITextBlock(new RectTransform(new Point(crewArea.Rect.Width - GUI.IntScale(10), GUI.IntScale(30)), crewArea.RectTransform), CombatMission.GetTeamName(teamId), textColor: CombatMission.GetTeamColor(teamId)) + { + ForceUpperCase = ForceUpperCase.Yes, + TextGetter = () => CombatMission.GetTeamName(teamId), + Visible = false, + IgnoreLayoutGroups = true, + UserData = teamId + }; + var teamIcon = new GUIImage(new RectTransform(Vector2.One, nameText.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight), style: i == 0 ? "CoalitionIcon" : "SeparatistIcon") + { + Color = nameText.TextColor + }; + nameText.Padding = new Vector4(teamIcon.Rect.Width + nameText.Padding.X, nameText.Padding.Y, nameText.Padding.Z, nameText.Padding.W); + } + // AbsoluteOffset is set in UpdateProjectSpecific based on crewListOpenState crewList = new GUIListBox(new RectTransform(Vector2.One, crewArea.RectTransform), style: null, isScrollBarOnDefaultSide: false) { @@ -562,9 +580,19 @@ namespace Barotrauma public bool CharacterClicked(GUIComponent component, object selection) { if (!AllowCharacterSwitch) { return false; } - if (!(selection is Character character) || character.IsDead || character.IsUnconscious) { return false; } + if (selection is not Character character || character.IsDead || character.IsUnconscious) { return false; } if (!character.IsOnPlayerTeam) { return false; } + if (GameMain.IsMultiplayer) + { + if (Character.Controlled == null) + { + Camera cam = Screen.Selected.Cam; + cam.Position = character.DrawPosition; + } + return true; + } + SelectCharacter(character); if (GUI.KeyboardDispatcher.Subscriber == crewList) { GUI.KeyboardDispatcher.Subscriber = null; } return true; @@ -631,10 +659,9 @@ namespace Barotrauma { if (crewList != this.crewList) { return; } if (draggedElementData is not Character) { return; } - if (!IsSinglePlayer) { return; } if (crewList.HasDraggedElementIndexChanged) { - UpdateCrewListIndices(); + if (IsSinglePlayer) { UpdateCrewListIndices(); } } else { @@ -645,10 +672,13 @@ namespace Barotrauma private void ResetCrewListIndex(Character c) { if (c?.Info == null) { return; } - c.Info.CrewListIndex = -1; - UpdateCrewListIndices(); + //default to the bottom of the list + c.Info.CrewListIndex = int.MaxValue; } + /// + /// Refresh the of the characters based on their order in the crew list + /// private void UpdateCrewListIndices() { if (crewList == null) { return; } @@ -661,20 +691,23 @@ namespace Barotrauma } } + /// + /// Order the crew list according to the characters' + /// private void SortCrewList() { if (crewList == null) { return; } crewList.Content.RectTransform.SortChildren((x, y) => { - var infoX = (x.GUIComponent.UserData as Character)?.Info?.CrewListIndex; - var infoY = (y.GUIComponent.UserData as Character)?.Info?.CrewListIndex; - if (infoX.HasValue) + int? index1 = (x.GUIComponent.UserData as Character)?.Info?.CrewListIndex; + int? index2 = (y.GUIComponent.UserData as Character)?.Info?.CrewListIndex; + if (index1.HasValue) { - return infoY.HasValue ? infoX.Value.CompareTo(infoY.Value) : -1; + return index2.HasValue ? index1.Value.CompareTo(index2.Value) : -1; } else { - return infoY.HasValue ? 1 : 0; + return index2.HasValue ? 1 : 0; } }); UpdateCrewListIndices(); @@ -1635,6 +1668,18 @@ namespace Barotrauma { crewArea.Visible = characters.Count > 0 && CharacterHealth.OpenHealthWindow == null; + var myTeam = Character.Controlled?.TeamID ?? GameMain.Client?.MyClient?.TeamID; + if (GameMain.GameSession?.GameMode is PvPMode) + { + var team1Text = crewArea.GetChildByUserData(CharacterTeamType.Team1); + team1Text.Visible = myTeam == CharacterTeamType.Team1; + team1Text.IgnoreLayoutGroups = !team1Text.Visible; + + var team2Text = crewArea.GetChildByUserData(CharacterTeamType.Team2); + team2Text.Visible = myTeam == CharacterTeamType.Team2; + team2Text.IgnoreLayoutGroups = !team2Text.Visible; + } + foreach (GUIComponent characterComponent in crewList.Content.Children) { if (characterComponent.UserData is Character character) @@ -1645,7 +1690,7 @@ namespace Barotrauma continue; } - characterComponent.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; + characterComponent.Visible = Character.Controlled == null || myTeam == character.TeamID; if (character.TeamID == CharacterTeamType.FriendlyNPC && Character.Controlled != null && (character.CurrentHull == Character.Controlled.CurrentHull || Vector2.DistanceSquared(Character.Controlled.WorldPosition, character.WorldPosition) < 500.0f * 500.0f)) { @@ -2098,7 +2143,7 @@ namespace Barotrauma CreateNodeConnectors(); if (Character.Controlled != null) { - Character.Controlled.dontFollowCursor = true; + Character.Controlled.FollowCursor = false; } HintManager.OnShowCommandInterface(); @@ -2242,7 +2287,7 @@ namespace Barotrauma returnNodeHotkey = expandNodeHotkey = Keys.None; if (Character.Controlled != null) { - Character.Controlled.dontFollowCursor = false; + Character.Controlled.FollowCursor = true; } } @@ -2511,7 +2556,7 @@ namespace Barotrauma // --> Create shortcut node for Steer order if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["steer"]) && subItems.Find(i => i.HasTag(Tags.NavTerminal) && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedItem == nav) && - nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) + nav.GetComponent() is Steering { HasPower: true } steering) { var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering); AddOrderNode(order); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index e34b44a4c..b08178d56 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -395,13 +395,15 @@ namespace Barotrauma protected void TryEndRoundWithFuelCheck(Action onConfirm, Action onReturnToMapScreen) { + if (Submarine.MainSub == null) { return; } + Submarine.MainSub.CheckFuel(); bool lowFuel = Submarine.MainSub.Info.LowFuel; if (PendingSubmarineSwitch != null) { lowFuel = TransferItemsOnSubSwitch ? (lowFuel && PendingSubmarineSwitch.LowFuel) : PendingSubmarineSwitch.LowFuel; } - if (Level.IsLoadedFriendlyOutpost && lowFuel && CargoManager.PurchasedItems.None(i => i.Value.Any(pi => pi.ItemPrefab.Tags.Contains("reactorfuel")))) + if (Level.IsLoadedFriendlyOutpost && lowFuel && CargoManager.PurchasedItems.None(i => i.Value.Any(pi => pi.ItemPrefab.Tags.Contains(Tags.ReactorFuel)))) { var extraConfirmationBox = new GUIMessageBox(TextManager.Get("lowfuelheader"), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 7e6bc711f..67f363350 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -541,7 +541,7 @@ namespace Barotrauma { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer); - GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty, mapSeed); + GameMain.GameSession = new GameSession(null, Option.None, CampaignDataPath.CreateRegular(savePath), GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty, mapSeed); campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; GameMain.NetLobbyScreen.ToggleCampaignMode(true); @@ -1044,7 +1044,7 @@ namespace Barotrauma return false; } - public override void Save(XElement element) + public override void Save(XElement element, bool isSavingOnLoading) { //do nothing, the clients get the save files from the server } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index f16ba4ad8..aa378710f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -240,7 +240,7 @@ namespace Barotrauma if (!savedOnStart) { GUI.SetSavingIndicatorState(true); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + SaveUtil.SaveGame(GameMain.GameSession.DataPath, isSavingOnLoading: true); savedOnStart = true; } @@ -448,7 +448,7 @@ namespace Barotrauma if (success) { GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + SaveUtil.SaveGame(GameMain.GameSession.DataPath); } else { @@ -479,7 +479,7 @@ namespace Barotrauma protected override void EndCampaignProjSpecific() { GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + SaveUtil.SaveGame(GameMain.GameSession.DataPath); GameMain.CampaignEndScreen.Select(); GUI.DisableHUD = false; GameMain.CampaignEndScreen.OnFinished = () => @@ -672,7 +672,7 @@ namespace Barotrauma } } - public override void Save(XElement element) + public override void Save(XElement element, bool isSavingOnLoading) { XElement modeElement = new XElement("SinglePlayerCampaign", new XAttribute("purchasedlostshuttles", PurchasedLostShuttles), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs index c945b6103..9dc938aa5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs @@ -7,7 +7,7 @@ using Barotrauma.Items.Components; namespace Barotrauma { - class TestGameMode : GameMode + partial class TestGameMode : GameMode { public Action OnRoundEnd; @@ -22,18 +22,6 @@ namespace Barotrauma private GUIButton createEventButton; - public TestGameMode(GameModePreset preset) : base(preset) - { - foreach (JobPrefab jobPrefab in JobPrefab.Prefabs.OrderBy(p => p.Identifier)) - { - for (int i = 0; i < jobPrefab.InitialCount; i++) - { - var variant = Rand.Range(0, jobPrefab.Variants); - CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant)); - } - } - } - public override void Start() { base.Start(); @@ -42,17 +30,24 @@ namespace Barotrauma foreach (Submarine submarine in Submarine.Loaded) { submarine.NeutralizeBallast(); - //normally the body would be made static during level generation, - //but in the test mode we load the outpost/wreck/beacon as if it was a normal sub and need to do this manually - if (submarine.Info.Type == SubmarineType.Outpost || - submarine.Info.Type == SubmarineType.OutpostModule || - submarine.Info.Type == SubmarineType.Wreck || - submarine.Info.Type == SubmarineType.BeaconStation) + switch (submarine.Info.Type) { - submarine.PhysicsBody.BodyType = FarseerPhysics.BodyType.Static; + case SubmarineType.Outpost: + case SubmarineType.OutpostModule: + case SubmarineType.Wreck: + case SubmarineType.BeaconStation: + //normally the body would be made static during level generation, + //but in the test mode we load the outpost/wreck/beacon as if it was a normal sub and need to do this manually + submarine.PhysicsBody.BodyType = FarseerPhysics.BodyType.Static; + if (submarine.Info.ShouldBeRuin) + { + submarine.Info.Type = SubmarineType.Ruin; + } + submarine.TeamID = submarine.Info.IsOutpost ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None; + break; } } - + if (SpawnOutpost) { GenerateOutpost(Submarine.MainSub); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 66efaee87..3b6ac9488 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -86,7 +86,7 @@ namespace Barotrauma.Tutorials yield return CoroutineStatus.Running; - GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefabs: null); + GameMain.GameSession = new GameSession(subInfo, Option.None, GameModePreset.Tutorial, missionPrefabs: null); (GameMain.GameSession.GameMode as TutorialMode).Tutorial = this; if (generationParams is not null) @@ -138,7 +138,7 @@ namespace Barotrauma.Tutorials character = Character.Create(charInfo, wayPoint.WorldPosition, "", isRemotePlayer: false, hasAi: false); character.TeamID = CharacterTeamType.Team1; Character.Controlled = character; - character.GiveJobItems(null); + character.GiveJobItems(isPvPMode: false, null); var idCard = character.Inventory.FindItemByTag("identitycard".ToIdentifier()); if (idCard == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 728c8a7c7..e09ff197b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -282,10 +282,10 @@ namespace Barotrauma public void SetRespawnInfo(string text, Color textColor, bool waitForNextRoundRespawn, bool hideButtons = false) { if (topLeftButtonGroup == null) { return; } - + bool permadeathMode = GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath }; - bool ironmanMode = GameMain.NetworkMember is { ServerSettings: { RespawnMode: RespawnMode.Permadeath, IronmanMode: true } }; - + bool ironmanMode = GameMain.NetworkMember?.ServerSettings is { IronmanModeActive: true }; + bool hasRespawnOptions; if (permadeathMode) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index d859a5af5..6cddda566 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -205,7 +205,7 @@ namespace Barotrauma if (Character.Controlled.SelectedItem.GetComponent() is Reactor reactor && reactor.PowerOn && Character.Controlled.SelectedItem.OwnInventory?.AllItems is IEnumerable containedItems && - containedItems.Count(i => i.HasTag(Tags.Fuel)) > 1) + containedItems.Count(i => i.HasTag(Tags.ReactorFuel)) > 1) { if (DisplayHint("onisinteracting.reactorwithextrarods".ToIdentifier())) { return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/PvPMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/PvPMode.cs new file mode 100644 index 000000000..80a1449ee --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/PvPMode.cs @@ -0,0 +1,84 @@ +using Microsoft.Xna.Framework; +namespace Barotrauma +{ + partial class PvPMode : MissionMode + { + private GUIComponent scoreContainer; + private readonly GUITextBlock[] scoreTexts = new GUITextBlock[2]; + private readonly GUITextBlock[] scoreTextShadows = new GUITextBlock[2]; + private readonly int[] prevScores = new int[2]; + + private void InitUI() + { + scoreContainer = new GUILayoutGroup(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), childAnchor: Anchor.TopRight) + { + CanBeFocused = false + }; + for (int i = 0; i < 2; i++) + { + var frame = new GUIFrame(new RectTransform(new Point(scoreContainer.Rect.Width, GUI.IntScale(80)), scoreContainer.RectTransform), style: null) + { + CanBeFocused = false + }; + new GUIImage(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight), style: i == 0 ? "CoalitionIcon" : "SeparatistIcon") + { + CanBeFocused = false + }; + scoreTextShadows[i] = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(GUI.IntScale(38), GUI.IntScale(2)) }, + string.Empty, textColor: GUIStyle.TextColorDark, textAlignment: Alignment.CenterRight, font: GUIStyle.SubHeadingFont) + { + CanBeFocused = false + }; + scoreTexts[i] = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(GUI.IntScale(40), 0) }, + string.Empty, textAlignment: Alignment.CenterRight, font: GUIStyle.SubHeadingFont) + { + CanBeFocused = false + }; + } + } + + public override void AddToGUIUpdateList() + { + base.AddToGUIUpdateList(); + + if (scoreContainer == null) { InitUI(); } + + scoreContainer.Visible = false; + foreach (var mission in Missions) + { + if (mission is CombatMission combatMission && combatMission.HasWinScore) + { + for (int i = 0; i < 2; i++) + { + var scoreText = scoreTexts[i]; + //one team very close to the win score, start flashing the score + if (combatMission.Scores[i] > combatMission.WinScore * 0.9f || + combatMission.Scores[i] == combatMission.WinScore - 1) + { + if (scoreText.Parent.FlashTimer <= 0.0f) + { + scoreText.Parent.Flash(GUIStyle.Orange); + scoreText.Pulsate(Vector2.One, Vector2.One * 1.2f, scoreText.Parent.FlashTimer); + } + } + if (prevScores[i] != combatMission.Scores[i] || scoreText.Text.IsNullOrEmpty()) + { + scoreText.Text = scoreTextShadows[i].Text = $"{combatMission.Scores[i]}/{combatMission.WinScore}"; + scoreText.Parent.Flash(GUIStyle.Green); + scoreText.Parent.GetAnyChild().Pulsate(Vector2.One, Vector2.One * 1.2f, scoreText.Parent.FlashTimer); + SoundPlayer.PlayUISound(GUISoundType.UIMessage); + } + scoreText.Parent.RectTransform.NonScaledSize = + new Point( + (int)(scoreText.TextSize.X + scoreText.Padding.X + scoreText.Padding.X) + scoreText.Parent.GetChild().Rect.Width + GUI.IntScale(10), + scoreText.Parent.Rect.Height); + scoreText.Parent.ForceLayoutRecalculation(); + prevScores[i] = combatMission.Scores[i]; + } + scoreContainer.Visible = true; + } + } + scoreContainer.AddToGUIUpdateList(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 8ab663787..0ab745b7b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; @@ -13,11 +14,13 @@ namespace Barotrauma private float crewListAnimDelay = 0.25f; private float missionIconAnimDelay; - private const float jobColumnWidthPercentage = 0.11f; - private const float characterColumnWidthPercentage = 0.44f; - private const float statusColumnWidthPercentage = 0.45f; + private const float JobColumnWidthPercentage = 0.1f; + private const float CharacterColumnWidthPercentage = 0.4f; + private const float StatusColumnWidthPercentage = 0.12f; + private const float KillColumnWidthPercentage = 0.1f; + private const float DeathColumnWidthPercentage = 0.1f; - private int jobColumnWidth, characterColumnWidth, statusColumnWidth; + private int jobColumnWidth, characterColumnWidth, statusColumnWidth, killColumnWidth, deathColumnWidth; private readonly List selectedMissions; private readonly Location startLocation, endLocation; @@ -109,6 +112,16 @@ namespace Barotrauma CombatMission.GetTeamName(CharacterTeamType.Team2), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); crewHeader2.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader2.Rect.Height * 2.0f)); CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == CharacterTeamType.Team2), traitorResults); + + if (CombatMission.Winner != CharacterTeamType.None) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewHeader.RectTransform), + TextManager.Get(CombatMission.Winner == CharacterTeamType.Team1 ? "pvpmode.victory" : "pvpmode.defeat"), textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont, + textColor: CombatMission.Winner == CharacterTeamType.Team1 ? GUIStyle.Green : GUIStyle.Red); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewHeader2.RectTransform), + TextManager.Get(CombatMission.Winner == CharacterTeamType.Team2 ? "pvpmode.victory" : "pvpmode.defeat"), textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont, + textColor: CombatMission.Winner == CharacterTeamType.Team2 ? GUIStyle.Green : GUIStyle.Red); + } } //header ------------------------------------------------------------------------------- @@ -242,7 +255,7 @@ namespace Barotrauma if (selectedMissions.Contains(mission)) { - UpdateMissionStateIcon(mission.Completed, missionIcon, animDelay); + UpdateMissionStateIcon(mission is CombatMission combatMission ? CombatMission.IsInWinningTeam(GameMain.Client?.Character) : mission.Completed, missionIcon, animDelay); animDelay += 0.25f; } } @@ -567,7 +580,11 @@ namespace Barotrauma LocalizedString locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.DisplayName : startLocation?.DisplayName; string textTag; - if (gameOver) + if (gameMode is PvPMode) + { + textTag = "RoundSummaryRoundHasEnded"; + } + else if (gameOver) { textTag = "RoundSummaryGameOver"; } @@ -596,7 +613,14 @@ namespace Barotrauma textTag = "RoundSummaryReturnToEmptyLocation"; break; default: - textTag = Submarine.MainSub.AtEndExit ? "RoundSummaryProgress" : "RoundSummaryReturn"; + if (Submarine.MainSub == null) + { + textTag = "RoundSummaryRoundHasEnded"; + } + else + { + textTag = Submarine.MainSub.AtEndExit ? "RoundSummaryProgress" : "RoundSummaryReturn"; + } break; } } @@ -628,22 +652,28 @@ namespace Barotrauma { var headerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform, Anchor.TopCenter, minSize: new Point(0, (int)(30 * GUI.Scale))) { }, isHorizontal: true) { - AbsoluteSpacing = 2 + AbsoluteSpacing = 2, + Stretch = true }; - GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale"); - GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale"); - GUIButton statusButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("label.statuslabel"), style: "GUIButtonSmallFreeScale"); + GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(JobColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale"); + GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(CharacterColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale"); - float sizeMultiplier = 1.0f; - //sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width; + if (gameMode is PvPMode) + { + var killButton = new GUIButton(new RectTransform(new Vector2(KillColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("killcount"), style: "GUIButtonSmallFreeScale"); + killColumnWidth = killButton.Rect.Width; + var deathButton = new GUIButton(new RectTransform(new Vector2(DeathColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("deathcount"), style: "GUIButtonSmallFreeScale"); + deathColumnWidth = deathButton.Rect.Width; + } - jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f); - characterButton.RectTransform.RelativeSize = new Vector2(characterColumnWidthPercentage * sizeMultiplier, 1f); - statusButton.RectTransform.RelativeSize = new Vector2(statusColumnWidthPercentage * sizeMultiplier, 1f); + GUIButton statusButton = new GUIButton(new RectTransform(new Vector2(StatusColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("label.statuslabel"), style: "GUIButtonSmallFreeScale"); - jobButton.TextBlock.Font = characterButton.TextBlock.Font = statusButton.TextBlock.Font = GUIStyle.HotkeyFont; - jobButton.CanBeFocused = characterButton.CanBeFocused = statusButton.CanBeFocused = false; - jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = statusButton.ForceUpperCase = ForceUpperCase.Yes; + foreach (var btn in headerFrame.GetAllChildren()) + { + btn.TextBlock.Font = GUIStyle.HotkeyFont; + btn.ForceUpperCase = ForceUpperCase.Yes; + btn.CanBeFocused = false; + } jobColumnWidth = jobButton.Rect.Width; characterColumnWidth = characterButton.Rect.Width; @@ -658,8 +688,28 @@ namespace Barotrauma headerFrame.RectTransform.RelativeSize -= new Vector2(crewList.ScrollBar.RectTransform.RelativeSize.X, 0.0f); + killCounts.Clear(); + if (GameMain.NetworkMember != null) + { + foreach (CharacterInfo characterInfo in characterInfos) + { + if (characterInfo == null) { continue; } + Character character = characterInfo.Character; + Client ownerClient = GameMain.NetworkMember.ConnectedClients.FirstOrDefault(c => c.Character == character); + int killCount = 0, deathCount = 0; + foreach (var mission in selectedMissions) + { + if (mission is not CombatMission combatMission) { continue; } + killCount += ownerClient == null ? combatMission.GetBotKillCount(characterInfo) : combatMission.GetClientKillCount(ownerClient); + deathCount += ownerClient == null ? combatMission.GetBotDeathCount(characterInfo) : combatMission.GetClientDeathCount(ownerClient); + } + killCounts[characterInfo] = killCount; + deathCounts[characterInfo] = deathCount; + } + } + float delay = crewListAnimDelay; - foreach (CharacterInfo characterInfo in characterInfos) + foreach (CharacterInfo characterInfo in characterInfos.OrderByDescending(ci => killCounts.GetValueOrDefault(ci))) { if (characterInfo == null) { continue; } CreateCharacterElement(characterInfo, crewList, traitorResults, delay); @@ -670,6 +720,10 @@ namespace Barotrauma return crewList; } + private readonly Dictionary killCounts = new(); + private readonly Dictionary deathCounts = new(); + + private void CreateCharacterElement(CharacterInfo characterInfo, GUIListBox listBox, TraitorManager.TraitorResults? traitorResults, float animDelay) { GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, GUI.IntScale(45)), listBox.Content.RectTransform), style: "ListBoxElement") @@ -741,8 +795,19 @@ namespace Barotrauma } } + if (gameMode is PvPMode pvpMode) + { + new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), + killCounts.GetValueOrDefault(characterInfo).ToString(), textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), + deathCounts.GetValueOrDefault(characterInfo).ToString(), textAlignment: Alignment.Center); + } + GUITextBlock statusBlock = new GUITextBlock(new RectTransform(new Point(statusColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(statusText.Value, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: statusColor); + ToolBox.LimitString(statusText.Value, GUIStyle.Font, statusColumnWidth), textAlignment: Alignment.Center, textColor: statusColor, font: GUIStyle.SmallFont) + { + ToolTip = statusText.Value + }; frame.FadeIn(animDelay, 0.15f); foreach (var child in frame.GetAllChildren()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs index 7d5981608..d078480d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs @@ -48,25 +48,46 @@ namespace Barotrauma.Items.Components description += '\n' + containedDescription; } } + + if (GameMain.DevMode && Tainted && selectedTaintedEffect != null) + { + description = $"{description}\n{selectedTaintedEffect.Name}: {selectedTaintedEffect.GetDescription(0f, AfflictionPrefab.Description.TargetType.OtherCharacter)}"; + } } public void ModifyDeconstructInfo(Deconstructor deconstructor, ref LocalizedString buttonText, ref LocalizedString infoText) { if (deconstructor.InputContainer.Inventory.AllItems.Count() == 2) { - var otherGeneticMaterial = - deconstructor.InputContainer.Inventory.AllItems.FirstOrDefault(it => it != item && it.Prefab == item.Prefab)?.GetComponent(); + var otherItem = deconstructor.InputContainer.Inventory.AllItems.FirstOrDefault(it => it != item); + if (otherItem == null) + { + return; + } + + var otherGeneticMaterial = otherItem.GetComponent(); if (otherGeneticMaterial == null) { - buttonText = TextManager.Get("researchstation.combine"); - infoText = TextManager.Get("researchstation.combine.infotext"); + return; } - else + + var combineRefineResult = GetCombineRefineResult(otherGeneticMaterial); + + if (combineRefineResult == CombineResult.None) + { + infoText = TextManager.Get("researchstation.novalidcombination"); + } + else if (combineRefineResult == CombineResult.Refined) { buttonText = TextManager.Get("researchstation.refine"); int taintedProbability = (int)(GetTaintedProbabilityOnRefine(otherGeneticMaterial, Character.Controlled) * 100); infoText = TextManager.GetWithVariable("researchstation.refine.infotext", "[taintedprobability]", taintedProbability.ToString()); } + else + { + buttonText = TextManager.Get("researchstation.combine"); + infoText = TextManager.Get("researchstation.combine.infotext"); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 352ba2c30..fcd5bc39a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -177,6 +177,8 @@ namespace Barotrauma.Items.Components //don't draw the crosshair if the item is in some other type of equip slot than hands (e.g. assault rifle in the bag slot) if (!character.HeldItems.Contains(item)) { return; } + base.DrawHUD(spriteBatch, character); + GUI.HideCursor = (crosshairSprite != null || crosshairPointerSprite != null) && GUI.MouseOn == null && !Inventory.IsMouseOnInventory && !GameMain.Instance.Paused; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index 35f3edba3..cb72f3474 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -216,6 +216,7 @@ namespace Barotrauma.Items.Components public override void DrawHUD(SpriteBatch spriteBatch, Character character) { if (character == null || !character.IsKeyDown(InputType.Aim)) { return; } + base.DrawHUD(spriteBatch, character); GUI.HideCursor = targetSections.Count > 0; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index e9f8847e5..e57e69ee4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -150,6 +150,17 @@ namespace Barotrauma.Items.Components public GUIFrame GuiFrame { get; set; } + /// + /// Overlay (just a non-interactable sprite) drawn when the item is selected, equipped or focused to via Controllers (e.g. when operating a turret via a periscope or a camera via a monitor). + /// + public Sprite HUDOverlay { get; set; } + + public float HUDOverlayAnimSpeed + { + get; + set; + } + private GUIDragHandle guiFrameDragHandle; private bool guiFrameUpdatePending; @@ -161,6 +172,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, IsPropertySaveable.No)] + public bool CloseByClickingOutsideGUIFrame + { + get; + set; + } + private ItemComponent linkToUIComponent; [Serialize("", IsPropertySaveable.No)] public string LinkUIToComponent @@ -261,7 +279,7 @@ namespace Barotrauma.Items.Components if (GameMain.Client?.MidRoundSyncing ?? false) { return; } //above the top boundary of the level (in an inactive respawn shuttle?) - if (item.Submarine != null && Level.Loaded != null && item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) + if (item.Submarine != null && item.Submarine.IsAboveLevel) { return; } @@ -340,7 +358,7 @@ namespace Barotrauma.Items.Components } else if (soundSelectionMode == SoundSelectionMode.Manual) { - index = Math.Clamp(ManuallySelectedSound, 0, matchingSounds.Count); + index = Math.Clamp(ManuallySelectedSound, 0, matchingSounds.Count - 1); } else { @@ -416,12 +434,23 @@ namespace Barotrauma.Items.Components if (sound == null) { return 0.0f; } if (sound.VolumeProperty == "") { return sound.VolumeMultiplier; } - if (SerializableProperties.TryGetValue(sound.VolumeProperty, out SerializableProperty property)) + SerializableProperty property = null; + ISerializableEntity targetEntity = null; + if (SerializableProperties.TryGetValue(sound.VolumeProperty, out property)) + { + targetEntity = this; + } + else if (Item.SerializableProperties.TryGetValue(sound.VolumeProperty, out property)) + { + targetEntity = Item; + } + + if (property != null) { float newVolume; try { - newVolume = property.GetFloatValue(this); + newVolume = property.GetFloatValue(targetEntity); } catch { @@ -470,7 +499,24 @@ namespace Barotrauma.Items.Components return linkToUIComponent; } - public virtual void DrawHUD(SpriteBatch spriteBatch, Character character) { } + public virtual void DrawHUD(SpriteBatch spriteBatch, Character character) + { + if (HUDOverlay != null) + { + Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + if (HUDOverlay is SpriteSheet spriteSheet) + { + spriteSheet.Draw(spriteBatch, + spriteIndex: (int)(Math.Floor(Timing.TotalTimeUnpaused * HUDOverlayAnimSpeed) % spriteSheet.FrameCount), + pos: screenSize / 2, color: Color.White, origin: HUDOverlay.Origin, rotate: 0, scale: screenSize / spriteSheet.FrameSize.ToVector2()); + } + else + { + HUDOverlay.Draw(spriteBatch, + pos: screenSize / 2, color: Color.White, origin: HUDOverlay.Origin, rotate: 0, scale: screenSize / HUDOverlay.size); + } + } + } public virtual void AddToGUIUpdateList(int order = 0) { @@ -513,6 +559,13 @@ namespace Barotrauma.Items.Components GuiFrameSource = subElement; ReloadGuiFrame(); break; + case "hudoverlayanimated": + HUDOverlay = new SpriteSheet(subElement); + HUDOverlayAnimSpeed = subElement.GetAttributeFloat("animspeed", 1.0f); + break; + case "hudoverlay": + HUDOverlay = new Sprite(subElement); + break; case "alternativelayout": AlternativeLayout = GUILayoutSettings.Load(subElement); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index e1658c4ef..5bef2df86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -458,54 +458,14 @@ namespace Barotrauma.Items.Components public void DrawContainedItems(SpriteBatch spriteBatch, float itemDepth, Color? overrideColor = null) { - Vector2 transformedItemPos = ItemPos * item.Scale; - Vector2 transformedItemInterval = ItemInterval * item.Scale; - Vector2 transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f); - Vector2 transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y); + var rootBody = item.RootContainer?.body ?? item.body; - if (item.body == null) - { - if (item.FlippedX) - { - transformedItemPos.X = -transformedItemPos.X; - transformedItemPos.X += item.Rect.Width; - transformedItemInterval.X = -transformedItemInterval.X; - transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; - } - if (item.FlippedY) - { - transformedItemPos.Y = -transformedItemPos.Y; - transformedItemPos.Y -= item.Rect.Height; - transformedItemInterval.Y = -transformedItemInterval.Y; - transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y; - } - transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y); - if (item.Submarine != null) { transformedItemPos += item.Submarine.DrawPosition; } - - if (Math.Abs(item.RotationRad) > 0.01f) - { - Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); - transformedItemPos = Vector2.Transform(transformedItemPos - item.DrawPosition, transform) + item.DrawPosition; - transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); - transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); - transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform); - } - } - else - { - Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); - if (item.body.Dir == -1.0f) - { - transformedItemPos.X = -transformedItemPos.X; - transformedItemInterval.X = -transformedItemInterval.X; - transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; - } - - transformedItemPos = Vector2.Transform(transformedItemPos, transform); - transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); - transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); - transformedItemPos += item.body.DrawPosition; - } + Vector2 transformedItemPos = GetContainedPosition( + drawPosition: true, + out Vector2 transformedItemIntervalHorizontal, + out Vector2 transformedItemIntervalVertical, + out bool flippedX, + out bool flippedY); Vector2 currentItemPos = transformedItemPos; @@ -525,19 +485,19 @@ namespace Barotrauma.Items.Components if (item.body != null) { Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); - pos.X *= item.body.Dir; + pos.X *= rootBody.Dir; itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition; } else { itemPos = pos; // This code is aped based on above. Not tested. - if (item.FlippedX) + if (flippedX) { itemPos.X = -itemPos.X; itemPos.X += item.Rect.Width; } - if (item.FlippedY) + if (flippedY) { itemPos.Y = -itemPos.Y; itemPos.Y -= item.Rect.Height; @@ -555,15 +515,15 @@ namespace Barotrauma.Items.Components } } - if (AutoInteractWithContained) + if (CanAutoInteractWithContained(contained.Item) && Screen.Selected is not { IsEditor: true }) { contained.Item.IsHighlighted = item.IsHighlighted; item.IsHighlighted = false; } Vector2 origin = contained.Item.Sprite.Origin; - if (item.FlippedX) { origin.X = contained.Item.Sprite.SourceRect.Width - origin.X; } - if (item.FlippedY) { origin.Y = contained.Item.Sprite.SourceRect.Height - origin.Y; } + if (flippedX) { origin.X = contained.Item.Sprite.SourceRect.Width - origin.X; } + if (flippedY) { origin.Y = contained.Item.Sprite.SourceRect.Height - origin.Y; } float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? contained.Item.Sprite.Depth : ContainedSpriteDepth; if (i < containedSpriteDepths.Length) @@ -578,12 +538,13 @@ namespace Barotrauma.Items.Components { spriteRotation = contained.Rotation; } - bool flipX = (item.body != null && item.body.Dir == -1) || item.FlippedX; + + bool flipX = rootBody is { Dir: -1 } || flippedX; if (flipX) { spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally; } - bool flipY = item.FlippedY; + bool flipY = flippedY; if (flipY) { spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; @@ -598,7 +559,8 @@ namespace Barotrauma.Items.Components contained.Item.Scale, spriteEffects, depth: containedSpriteDepth); - contained.Item.DrawDecorativeSprites(spriteBatch, itemPos, flipX,flipY, (contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), + + contained.Item.DrawDecorativeSprites(spriteBatch, itemPos, flipX, flipY, (contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), containedSpriteDepth, overrideColor); foreach (ItemContainer ic in contained.Item.GetComponents()) @@ -620,7 +582,7 @@ namespace Barotrauma.Items.Components } else { - currentItemPos += transformedItemInterval; + currentItemPos += transformedItemIntervalHorizontal + transformedItemIntervalVertical; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 61cf70993..f73d34554 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -41,7 +41,7 @@ namespace Barotrauma.Items.Components } private string text; - [Serialize("", IsPropertySaveable.Yes, translationTextTag: "Label.", description: "The text displayed in the label.", alwaysUseInstanceValues: true), Editable(100)] + [Serialize("", IsPropertySaveable.Yes, translationTextTag: "Label.", description: "The text displayed in the label.", alwaysUseInstanceValues: true), Editable(MaxLength = 100)] public string Text { get { return text; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index df20deb01..a01e81ab4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -11,6 +11,7 @@ namespace Barotrauma.Items.Components public override void DrawHUD(SpriteBatch spriteBatch, Character character) { + base.DrawHUD(spriteBatch, character); if (focusTarget != null && character.ViewTarget == focusTarget) { foreach (ItemComponent ic in focusTarget.Components) @@ -23,6 +24,15 @@ namespace Barotrauma.Items.Components } } + public override void AddToGUIUpdateList(int order = 0) + { + base.AddToGUIUpdateList(order); + if (focusTarget != null && Character.Controlled.ViewTarget == focusTarget) + { + focusTarget.AddToGUIUpdateList(order); + } + } + partial void HideHUDs(bool value) { if (isHUDsHidden == value) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 0fbbf4c00..24b0ec94b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -154,12 +154,14 @@ namespace Barotrauma.Items.Components { infoArea.Text = TextManager.Get(InfoText).Fallback(InfoText); } + if (IsActive) { activateButton.Text = TextManager.Get("DeconstructorCancel"); infoArea.Text = string.Empty; return; } + bool outputsFound = false; foreach (var (inputItem, deconstructItem) in GetAvailableOutputs(checkRequiredOtherItems: true)) { @@ -174,27 +176,34 @@ namespace Barotrauma.Items.Components } inputItem.GetComponent()?.ModifyDeconstructInfo(this, ref buttonText, ref infoText); activateButton.Text = buttonText; - if (infoArea != null) - { - infoArea.Text = infoText; - } + infoArea.Text = infoText; + return; } } + + LocalizedString activateButtonText = TextManager.Get(ActivateButtonText); + activateButton.Enabled = outputsFound || !InputContainer.Inventory.IsEmpty(); + activateButton.Text = activateButtonText; + //no valid outputs found: check if we're missing some required items from the input slots and display a message about it if possible if (!outputsFound && infoArea != null) { foreach (var (inputItem, deconstructItem) in GetAvailableOutputs(checkRequiredOtherItems: false)) { + LocalizedString infoText = string.Empty; if (deconstructItem.RequiredOtherItem.Any() && !string.IsNullOrEmpty(deconstructItem.InfoTextOnOtherItemMissing)) { LocalizedString missingItemName = TextManager.Get("entityname." + deconstructItem.RequiredOtherItem.First()); - infoArea.Text = TextManager.GetWithVariable(deconstructItem.InfoTextOnOtherItemMissing, "[itemname]", missingItemName); + infoText = TextManager.GetWithVariable(deconstructItem.InfoTextOnOtherItemMissing, "[itemname]", missingItemName); } + + inputItem.GetComponent()?.ModifyDeconstructInfo(this, ref activateButtonText, ref infoText); + + activateButton.Text = activateButtonText; + infoArea.Text = infoText; } } - activateButton.Enabled = outputsFound || !InputContainer.Inventory.IsEmpty(); - activateButton.Text = TextManager.Get(ActivateButtonText); }; } @@ -415,7 +424,7 @@ namespace Barotrauma.Items.Components public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { - inSufficientPowerWarning.Visible = IsActive && !hasPower; + inSufficientPowerWarning.Visible = IsActive && !HasPower; } private bool OnActivateButtonClicked(GUIButton button, object obj) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 1a1f2780e..fafa2ee3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -10,45 +10,27 @@ using Microsoft.Xna.Framework.Input; namespace Barotrauma.Items.Components { - internal readonly struct MiniMapGUIComponent + internal readonly record struct MiniMapGUIComponent(GUIComponent RectComponent, GUIComponent BorderComponent) { - public readonly GUIComponent RectComponent; - public readonly GUIComponent BorderComponent; - - public MiniMapGUIComponent(GUIComponent rectComponent) + public MiniMapGUIComponent(GUIComponent rectComponent) : this(rectComponent, rectComponent) { - RectComponent = rectComponent; - BorderComponent = rectComponent; } - - public MiniMapGUIComponent(GUIComponent frame, GUIComponent linkedHullComponent) - { - RectComponent = frame; - BorderComponent = linkedHullComponent; - } - + public void Deconstruct(out GUIComponent component, out GUIComponent borderComponent) { component = RectComponent; borderComponent = BorderComponent; } } - - internal readonly struct MiniMapSprite + + internal readonly record struct MiniMapSprite(Sprite? Sprite, Color Color) { - public readonly Sprite? Sprite; - public readonly Color Color; - - public MiniMapSprite(JobPrefab prefab) + public MiniMapSprite(JobPrefab prefab) : this(prefab.IconSmall, prefab.UIColor) { - Sprite = prefab.IconSmall; - Color = prefab.UIColor; } - - public MiniMapSprite(Order order) + + public MiniMapSprite(Order order) : this(order.SymbolSprite, order.Color) { - Sprite = order.SymbolSprite; - Color = order.Color; } } @@ -223,7 +205,27 @@ namespace Barotrauma.Items.Components NoPowerColor = MiniMapBaseColor * 0.1f, ElectricalBaseColor = GUIStyle.Orange, NoPowerElectricalColor = ElectricalBaseColor * 0.1f; - + + // If this is portable, only allow displaying data in the player sub (not enemy subs, ruins, wrecks or other unknown places) + private bool IsPortableItemAllowed + { + get + { + if (IsUsableOutsidePlayerSub) { return true; } + if (item.Submarine == null) { return false; } + if (item.GetComponent() is not Pickable handheldItem) { return true; } + // This will effectively make sure wherever we are, it belongs to the player + return handheldItem.Picker?.TeamID == item.Submarine.TeamID; + } + } + + [Serialize(false, IsPropertySaveable.No, description: "If this item is portable, should it be usable outside the player submarine?")] + public bool IsUsableOutsidePlayerSub + { + get; + set; + } + partial void InitProjSpecific() { hullDatas = new Dictionary(); @@ -425,22 +427,25 @@ namespace Barotrauma.Items.Components return false; } - private bool VisibleOnItemFinder(Item it) + private bool VisibleOnItemFinder(Item targetItem) { - if (it?.Submarine == null) { return false; } - if (item.Submarine == null || !item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; } - if (it.NonInteractable || it.IsHidden) { return false; } - if (it.GetComponent() == null) { return false; } + if (targetItem?.Submarine == null || item.Submarine == null) { return false; } - var holdable = it.GetComponent(); + if (!IsPortableItemAllowed) { return false; } + + if (!item.Submarine.IsEntityFoundOnThisSub(targetItem, includingConnectedSubs: true)) { return false; } + if (targetItem.NonInteractable || targetItem.IsHidden) { return false; } + if (targetItem.GetComponent() == null) { return false; } + + var holdable = targetItem.GetComponent(); if (holdable != null && holdable.Attached) { return false; } - var wire = it.GetComponent(); + var wire = targetItem.GetComponent(); if (wire != null && wire.Connections.Any(c => c != null)) { return false; } - if (it.Container?.GetComponent() is { DrawInventory: false } or { AllowAccess: false }) { return false; } + if (targetItem.Container?.GetComponent() is { DrawInventory: false } or { AllowAccess: false }) { return false; } - if (it.HasTag(Tags.TraitorMissionItem)) { return false; } + if (targetItem.HasTag(Tags.TraitorMissionItem)) { return false; } return true; } @@ -454,18 +459,24 @@ namespace Barotrauma.Items.Components searchAutoComplete?.AddToGUIUpdateList(order: order + 1); } } - - private void CreateHUD() + + private void ClearHUD() { subEntities.Clear(); - prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); submarineContainer.ClearChildren(); + displayedSubs.Clear(); + } - if (item.Submarine is null) + private void RefreshHUD() + { + ClearHUD(); + + if (item.Submarine is null || !IsPortableItemAllowed) { - displayedSubs.Clear(); return; } + + prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center)); miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; @@ -574,18 +585,25 @@ namespace Barotrauma.Items.Components public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { - //recreate HUD if the subs we should display have changed - if (item.Submarine == null && displayedSubs.Count > 0 || // item not inside a sub anymore, but display is still showing subs + // Refresh HUD (including possibly just clearing it away) if the subs we should display have changed + if (item.Submarine == null && displayedSubs.Count > 0 || // item not inside a sub anymore, but display is still showing subs item.Submarine is { } itemSub && ( - !displayedSubs.Contains(itemSub) || // current sub not displayed - itemSub.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID).Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || // some of the docked subs not displayed - displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed + // current sub not displayed + !displayedSubs.Contains(itemSub) || + // some of the docked subs not displayed + itemSub.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID).Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || + // displaying a sub that shouldn't be displayed + displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) ) || - prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || // resolution changed - !submarineContainer.Children.Any()) // We lack a GUI + // If this item is portable and not in a player sub and using it otherwise is disallowed + !IsPortableItemAllowed || + // resolution changed + prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || + // We lack a GUI + !submarineContainer.Children.Any()) { - CreateHUD(); + RefreshHUD(); } //reset data if we haven't received anything in a while @@ -737,7 +755,7 @@ namespace Barotrauma.Items.Components return; } - if (Voltage < MinVoltage) + if (!HasPower) { Vector2 textSize = GUIStyle.Font.MeasureString(noPowerTip); Vector2 textPos = GuiFrame.Rect.Center.ToVector2(); @@ -747,7 +765,7 @@ namespace Barotrauma.Items.Components return; } - if (currentMode == MiniMapMode.HullStatus && item.Submarine != null) + if (currentMode == MiniMapMode.HullStatus && item.Submarine != null && IsPortableItemAllowed) { Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; spriteBatch.End(); @@ -958,19 +976,19 @@ namespace Barotrauma.Items.Components MiniMapBlips = positions.ToImmutableHashSet(); - if (searchAutoComplete is null) { return; } - searchAutoComplete.Visible = false; + HideGUIComponent(searchAutoComplete); } private void UpdateHUDBack() { - if (item.Submarine == null) { return; } - - if (hullInfoFrame != null) { hullInfoFrame.Visible = false; } - reportFrame.Visible = false; - searchBarFrame.Visible = false; - electricalFrame.Visible = false; - miniMapFrame.Visible = false; + // Clear up mode-specific elements before checking if drawing should continue, so they'll be gone if not + HideModeSpecificFrames(); + + if (item.Submarine == null || !IsPortableItemAllowed) + { + ClearHUD(); + return; + } switch (currentMode) { @@ -988,7 +1006,24 @@ namespace Barotrauma.Items.Components break; } } - + + private void HideModeSpecificFrames() + { + HideGUIComponent(hullInfoFrame); + HideGUIComponent(reportFrame); + HideGUIComponent(searchBarFrame); + HideGUIComponent(electricalFrame); + HideGUIComponent(miniMapFrame); + } + + private static void HideGUIComponent(GUIComponent? component) + { + if (component != null) + { + component.Visible = false; + } + } + private void UpdateHullStatus() { bool canHoverOverHull = true; @@ -1007,7 +1042,7 @@ namespace Barotrauma.Items.Components child.Color = child.OutlineColor = NoPowerDoorColor; } - if (Voltage < MinVoltage) { continue; } + if (!HasPower) { continue; } child.Color = child.OutlineColor = DoorIndicatorColor; if (GUI.MouseOn == child) @@ -1037,7 +1072,7 @@ namespace Barotrauma.Items.Components } } - if (Voltage < MinVoltage) { continue; } + if (!HasPower) { continue; } hullDatas.TryGetValue(hull, out HullData? hullData); if (hullData is null) @@ -1187,7 +1222,7 @@ namespace Barotrauma.Items.Components component.Color = component.OutlineColor = NoPowerElectricalColor; } - if (Voltage < MinVoltage || !miniMapGuiComponent.RectComponent.Visible) { continue; } + if (!HasPower || !miniMapGuiComponent.RectComponent.Visible) { continue; } int durability = (int)(it.Condition / (it.MaxCondition / it.MaxRepairConditionMultiplier) * 100f); Color color = ToolBox.GradientLerp(durability / 100f, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green, GUIStyle.Green); @@ -1229,11 +1264,11 @@ namespace Barotrauma.Items.Components private void DrawHUDBack(SpriteBatch spriteBatch, GUICustomComponent container) { - if (item.Submarine == null) { return; } + if (item.Submarine == null || !IsPortableItemAllowed) { return; } - DrawSubmarine(spriteBatch); + DrawSubmarine(spriteBatch); - if (Voltage < MinVoltage) { return; } + if (!HasPower) { return; } Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index cb5115422..d5b8c5224 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -144,6 +144,7 @@ namespace Barotrauma.Items.Components private bool isConnectedToSteering; private static LocalizedString caveLabel; + private static LocalizedString enemyLabel; [Serialize(false, IsPropertySaveable.Yes)] @@ -164,6 +165,8 @@ namespace Barotrauma.Items.Components TextManager.Get("cave").Fallback( TextManager.Get("missiontype.nest")); + enemyLabel = TextManager.Get("enemysubmarine"); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -1019,6 +1022,38 @@ namespace Barotrauma.Items.Components cave.StartPos.ToVector2(), transducerCenter, DisplayScale, center, DisplayRadius); } + + if (GameMain.NetworkMember is { } networkMember && GameMain.GameSession?.GameMode is PvPMode) + { + if (networkMember.ServerSettings.TrackOpponentInPvP + && Submarine.MainSubs[0] is { } coalitionSub + && Submarine.MainSubs[1] is { } separatistSub + && Character.Controlled is { } player) + { + Submarine whichSubToDraw = player.TeamID switch + { + CharacterTeamType.Team1 => separatistSub, + CharacterTeamType.Team2 => coalitionSub, + _ => null + }; + + if (whichSubToDraw != null) + { + DrawOffsetMarker(spriteBatch, + enemyLabel.Value, + Tags.Submarine, + Tags.Enemy, + whichSubToDraw.WorldPosition, + transducerCenter, + distanceThresholds: new Range(start: MetersToUnits(150), end: MetersToUnits(1600)), + offset: new Range(start: MetersToUnits(100), end: MetersToUnits(400)), + minOffset: MetersToUnits(10)); + + static float MetersToUnits(float m) + => m / Physics.DisplayToRealWorldRatio; + } + } + } } int missionIndex = 0; @@ -1042,7 +1077,8 @@ namespace Barotrauma.Items.Components } if (HasMineralScanner && UseMineralScanner && CurrentMode == Mode.Active && MineralClusters != null && - (item.CurrentHull == null || !DetectSubmarineWalls)) + (item.CurrentHull == null || !DetectSubmarineWalls) && + HasPower) { foreach (var c in MineralClusters) { @@ -1076,7 +1112,7 @@ namespace Barotrauma.Items.Components { if (!sub.ShowSonarMarker) { continue; } if (connectedSubs.Contains(sub)) { continue; } - if (Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (sub.IsAboveLevel) { continue; } if (item.Submarine != null || Character.Controlled != null) { @@ -1185,7 +1221,7 @@ namespace Barotrauma.Items.Components foreach (DockingPort dockingPort in DockingPort.List) { - if (Level.Loaded != null && dockingPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (dockingPort.Item.Submarine.IsAboveLevel) { continue; } if (dockingPort.Item.IsHidden) { continue; } if (dockingPort.Item.Submarine == null) { continue; } if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } @@ -1198,8 +1234,8 @@ namespace Barotrauma.Items.Components //don't show the docking ports of the opposing team on the sonar if (item.Submarine != null && - item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && - dockingPort.Item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && + !item.Submarine.IsRespawnShuttle && + !dockingPort.Item.Submarine.IsRespawnShuttle && !dockingPort.Item.Submarine.Info.IsOutpost && !dockingPort.Item.Submarine.Info.IsBeacon) { @@ -1792,8 +1828,47 @@ namespace Barotrauma.Items.Components sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f * blip.Alpha, sonarBlip.Origin, 0, scale, SpriteEffects.None, 0); } + /// + /// Used in DrawOffsetMarker to cache the randomized location of the marker + /// + private readonly Dictionary cachedLocations = new Dictionary(); + + private void DrawOffsetMarker(SpriteBatch spriteBatch, string label, Identifier iconIdentifier, Identifier targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, Range distanceThresholds, Range offset, float minOffset) + { + Vector2 pos; + + if (!cachedLocations.TryGetValue(targetIdentifier, out CachedLocation cachedLocation)) + { + cachedLocation = CreateCachedLocation(); + cachedLocations.Add(targetIdentifier, cachedLocation); + pos = cachedLocation.Location; + } + else + { + if (Timing.TotalTime > cachedLocation.RecalculationTime) + { + cachedLocation = CreateCachedLocation(); + cachedLocations[targetIdentifier] = cachedLocation; + } + + pos = cachedLocation.Location; + } + + DrawMarker(spriteBatch, label, iconIdentifier, targetIdentifier, pos, transducerPosition, DisplayScale, center, DisplayRadius); + + CachedLocation CreateCachedLocation() + { + float distance = Vector2.Distance(worldPosition, transducerPosition); + + float maxOffset = MathHelper.Lerp(offset.Start, offset.End, MathHelper.Clamp((distance - distanceThresholds.Start) / (distanceThresholds.End - distanceThresholds.Start), 0.0f, 1.0f)); + + Vector2 randomPos = Rand.Vector(Rand.Range(minOffset, maxOffset)); + return new CachedLocation(worldPosition + randomPos, Timing.TotalTime + Rand.Range(10.0f, 30.0f)); + } + } + private void DrawMarker(SpriteBatch spriteBatch, string label, Identifier iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius, - bool onlyShowTextOnMouseOver = false) + bool onlyShowTextOnMouseOver = false) { float linearDist = Vector2.Distance(worldPosition, transducerPosition); float dist = linearDist; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 4d9aabff1..eef3e3b4c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -554,7 +554,7 @@ namespace Barotrauma.Items.Components int x = rect.X; int y = rect.Y; - if (Voltage < MinVoltage) { return; } + if (!HasPower) { return; } Rectangle velRect = new Rectangle(x + 20, y + 20, width - 40, height - 40); Vector2 steeringOrigin = steerArea.Rect.Center.ToVector2(); @@ -759,7 +759,7 @@ namespace Barotrauma.Items.Components dockingButton.Text = dockText; } - if (Voltage < MinVoltage) + if (!HasPower) { tipContainer.Visible = true; tipContainer.Text = noPowerTip; @@ -829,7 +829,7 @@ namespace Barotrauma.Items.Components } if (!AutoPilot && Character.DisableControls && GUI.KeyboardDispatcher.Subscriber == null) { - steeringAdjustSpeed = character == null ? DefaultSteeringAdjustSpeed : MathHelper.Lerp(0.2f, 1.0f, character.GetSkillLevel("helm") / 100.0f); + steeringAdjustSpeed = character == null ? DefaultSteeringAdjustSpeed : MathHelper.Lerp(0.2f, 1.0f, character.GetSkillLevel(Tags.HelmSkill) / 100.0f); Vector2 input = Vector2.Zero; if (PlayerInput.KeyDown(InputType.Left)) { input -= Vector2.UnitX; } if (PlayerInput.KeyDown(InputType.Right)) { input += Vector2.UnitX; } @@ -909,7 +909,7 @@ namespace Barotrauma.Items.Components if (targetPort.Docked || targetPort.Item.Submarine == null) { continue; } if (targetPort.Item.Submarine == controlledSub || targetPort.IsHorizontal != sourcePort.IsHorizontal) { continue; } if (targetPort.Item.Submarine.DockedTo?.Contains(sourcePort.Item.Submarine) ?? false) { continue; } - if (Level.Loaded != null && targetPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (targetPort.Item.Submarine.IsAboveLevel) { continue; } if (sourceDir == targetPort.GetDir()) { continue; } float dist = Vector2.DistanceSquared(sourcePort.Item.WorldPosition, targetPort.Item.WorldPosition); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs index 31f951899..f573503f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs @@ -6,6 +6,7 @@ namespace Barotrauma.Items.Components { public override void DrawHUD(SpriteBatch spriteBatch, Character character) { + base.DrawHUD(spriteBatch, character); currentTarget?.DrawHUD(spriteBatch, Screen.Selected.Cam, character); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 5c790d576..e86923ef7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -299,6 +299,8 @@ namespace Barotrauma.Items.Components public override void DrawHUD(SpriteBatch spriteBatch, Character character) { + base.DrawHUD(spriteBatch, character); + IsActive = true; float defaultMaxCondition = (item.MaxCondition / item.MaxRepairConditionMultiplier); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index c6b5417c9..6c2a2c682 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -52,9 +52,11 @@ namespace Barotrauma.Items.Components Vector2 sourcePos = GetSourcePos(); + //need to double the size because this is essentially just the radius, we need the diameter + // + some extra margin to be on the safe side return new Vector2( Math.Abs(target.DrawPosition.X - sourcePos.X), - Math.Abs(target.DrawPosition.Y - sourcePos.Y)) * 1.5f; + Math.Abs(target.DrawPosition.Y - sourcePos.Y)) * 2.2f; } } @@ -122,7 +124,7 @@ namespace Barotrauma.Items.Components { if (turret.BarrelSprite != null) { - startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; + startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * turret.Item.Scale * BarrelLengthMultiplier; } startPos -= turret.GetRecoilOffset(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs index 80fbd44db..bea101f3b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs @@ -3,7 +3,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -16,7 +15,7 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(ContentXElement element) { - terminalButtonStyles = new string[RequiredSignalCount]; + terminalButtonStyles = new string[requiredSignalCount]; int i = 0; foreach (var childElement in element.GetChildElements("TerminalButton")) { @@ -38,11 +37,11 @@ namespace Barotrauma.Items.Components }; paddedFrame.OnAddedToGUIUpdateList += (component) => { - bool buttonsEnabled = AllowUsingButtons; - foreach (var child in component.Children) + bool buttonsEnabled = IsActivated; + foreach (GUIComponent child in component.Children) { - if (!(child is GUIButton)) { continue; } - if (!(child.UserData is int)) { continue; } + if (child is not GUIButton) { continue; } + if (child.UserData is not int) { continue; } child.Enabled = buttonsEnabled; child.Children.ForEach(c => c.Enabled = buttonsEnabled); } @@ -59,7 +58,7 @@ namespace Barotrauma.Items.Components containerIndicator.OverrideState = itemsContained ? GUIComponent.ComponentState.Selected : GUIComponent.ComponentState.None; }; - float x = 1.0f / (1 + RequiredSignalCount); + float x = 1.0f / (1 + requiredSignalCount); float y = Math.Min((x * paddedFrame.Rect.Width) / paddedFrame.Rect.Height, 0.5f); Vector2 relativeSize = new Vector2(x, y); @@ -69,7 +68,7 @@ namespace Barotrauma.Items.Components containerIndicator = new GUIImage(new RectTransform(new Vector2(0.5f, 0.5f * (1.0f - y)), containerSection.RectTransform, anchor: Anchor.BottomCenter), style: "IndicatorLightRed", scaleToFit: true); - for (int i = 0; i < RequiredSignalCount; i++) + for (int i = 0; i < requiredSignalCount; i++) { var button = new GUIButton(new RectTransform(relativeSize, paddedFrame.RectTransform), style: null) { @@ -111,7 +110,7 @@ namespace Barotrauma.Items.Components public void ClientEventRead(IReadMessage msg, float sendingTime) { - SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), sender: null, isServerMessage: true); + SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), sender: null, ignoreState: true); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs index 329545f2d..9d8ba4214 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; @@ -129,7 +129,7 @@ namespace Barotrauma.Items.Components public void RemoveComponents(IReadOnlyCollection node) { - if (Locked) { return; } + if (IsLocked()) { return; } var ids = node.Select(static n => n.ID).ToImmutableArray(); if (GameMain.NetworkMember is null) @@ -146,7 +146,7 @@ namespace Barotrauma.Items.Components public void AddWire(CircuitBoxConnection one, CircuitBoxConnection two) { - if (Locked) { return; } + if (IsLocked()) { return; } if (GameMain.NetworkMember is null) { Connect(one, two, static delegate { }, CircuitBoxWire.SelectedWirePrefab); @@ -160,7 +160,7 @@ namespace Barotrauma.Items.Components public void RemoveWires(IReadOnlyCollection wires) { - if (Locked) { return; } + if (IsLocked()) { return; } var ids = wires.Select(static w => w.ID).ToImmutableArray(); if (GameMain.NetworkMember is null) { @@ -230,7 +230,7 @@ namespace Barotrauma.Items.Components public void MoveComponent(Vector2 moveAmount, IReadOnlyCollection moveables) { - if (Locked) { return; } + if (IsLocked()) { return; } var ids = ImmutableArray.CreateBuilder(); var ios = ImmutableArray.CreateBuilder(); var labelIds = ImmutableArray.CreateBuilder(); @@ -265,7 +265,7 @@ namespace Barotrauma.Items.Components public void AddComponent(ItemPrefab prefab, Vector2 pos) { - if (Locked) { return; } + if (IsLocked()) { return; } if (GameMain.NetworkMember is null) { ItemPrefab resource; @@ -292,7 +292,7 @@ namespace Barotrauma.Items.Components public void RenameLabel(CircuitBoxLabelNode label, Color color, NetLimitedString header, NetLimitedString body) { - if (Locked) { return; } + if (IsLocked()) { return; } if (GameMain.NetworkMember is null) { label.EditText(header, body); @@ -316,7 +316,7 @@ namespace Barotrauma.Items.Components public void ResizeNode(CircuitBoxNode node, CircuitBoxResizeDirection dir, Vector2 amount) { - if (Locked) { return; } + if (IsLocked()) { return; } var resize = node.ResizeBy(dir, amount); if (GameMain.NetworkMember is null) { @@ -341,7 +341,7 @@ namespace Barotrauma.Items.Components public void AddLabel(Vector2 pos) { - if (Locked) { return; } + if (IsLocked()) { return; } if (GameMain.NetworkMember is null) { AddLabelInternal(ICircuitBoxIdentifiable.FindFreeID(Labels), GUIStyle.Blue, pos, CircuitBoxLabelNode.DefaultHeaderText, NetLimitedString.Empty); @@ -353,7 +353,7 @@ namespace Barotrauma.Items.Components public void RemoveLabel(IReadOnlyCollection labels) { - if (Locked) { return; } + if (IsLocked()) { return; } if (!labels.Any()) { return; } var ids = labels.Select(static n => n.ID).ToImmutableArray(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index dc5b84592..de54014d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -14,6 +14,8 @@ namespace Barotrauma.Items.Components private bool readingNetworkEvent; + private GUIComponent insufficientPowerWarning; + private Point ElementMaxSize => new Point(uiElementContainer.Rect.Width, (int)(65 * GUI.yScale)); public override bool RecreateGUIOnResolutionChange => true; @@ -40,7 +42,7 @@ namespace Barotrauma.Items.Components float elementSize = Math.Min(1.0f / visibleElements.Count(), 1); foreach (CustomInterfaceElement ciElement in visibleElements) { - if (ciElement.HasPropertyName) + if (ciElement.InputType is CustomInterfaceElement.InputTypeOption.Number or CustomInterfaceElement.InputTypeOption.Text) { var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform), isHorizontal: true) { @@ -49,7 +51,7 @@ namespace Barotrauma.Items.Components }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), TextManager.Get(ciElement.Label).Fallback(ciElement.Label)); - if (!ciElement.IsNumberInput) + if (ciElement.InputType is CustomInterfaceElement.InputTypeOption.Text) { var textBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), ciElement.Signal, style: "GUITextBoxNoIcon") { @@ -149,7 +151,7 @@ namespace Barotrauma.Items.Components } } } - else if (ciElement.ContinuousSignal) + else if (ciElement.InputType is CustomInterfaceElement.InputTypeOption.TickBox) { var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform) { @@ -175,7 +177,7 @@ namespace Barotrauma.Items.Components tickBox.RectTransform.MaxSize = new Point(int.MaxValue, int.MaxValue); uiElements.Add(tickBox); } - else + else if (ciElement.InputType is CustomInterfaceElement.InputTypeOption.Button) { var btn = new GUIButton(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform), TextManager.Get(ciElement.Label).Fallback(ciElement.Label), style: "DeviceButton") @@ -203,6 +205,16 @@ namespace Barotrauma.Items.Components uiElements.Add(btn); } } + + if (ShowInsufficientPowerWarning) + { + insufficientPowerWarning = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), GuiFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { MinSize = new Point(0, GUI.IntScale(30)) }, + TextManager.Get("SteeringNoPowerTip"), font: GUIStyle.Font, wrap: true, style: "GUIToolTip", textAlignment: Alignment.Center) + { + AutoScaleHorizontal = true, + Visible = false + }; + } } public override void CreateEditingHUD(SerializableEntityEditor editor) @@ -253,7 +265,20 @@ namespace Barotrauma.Items.Components { if (uiElement.UserData is not CustomInterfaceElement element) { continue; } bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Count > 0); - if (visible) { visibleElementCount++; } + if (visible) + { + visibleElementCount++; + if (element.GetValueInterval > 0.0f) + { + element.GetValueTimer -= deltaTime; + if (element.GetValueTimer <= 0.0f) + { + SetSignalToPropertyValue(element); + UpdateSignalProjSpecific(uiElement); + element.GetValueTimer = element.GetValueInterval; + } + } + } if (uiElement.Visible != visible) { uiElement.Visible = visible; @@ -274,6 +299,11 @@ namespace Barotrauma.Items.Components GuiFrame.Visible = visibleElementCount > 0; uiElementContainer.Recalculate(); } + + if (insufficientPowerWarning != null) + { + insufficientPowerWarning.Visible = item.GetComponents().Any(p => p.PowerConsumption > 0.0f && p.Voltage < p.MinVoltage); + } } partial void UpdateLabelsProjSpecific() @@ -336,22 +366,32 @@ namespace Barotrauma.Items.Components if (signals == null) { return; } for (int i = 0; i < signals.Length && i < uiElements.Count; i++) { - string signal = customInterfaceElementList[i].Signal; - if (uiElements[i] is GUITextBox tb) + UpdateSignalProjSpecific(uiElements[i]); + } + } + + private void UpdateSignalProjSpecific(GUIComponent uiElement) + { + if (uiElement.UserData is not CustomInterfaceElement element) { return; } + string signal = element.Signal; + if (uiElement is GUITextBox tb) + { + tb.Text = Screen.Selected is { IsEditor: true } ? + signal : + TextManager.Get(signal).Fallback(signal).Value; + } + else if (uiElement is GUINumberInput ni) + { + if (ni.InputType == NumberType.Int) { - tb.Text = Screen.Selected is { IsEditor: true } ? - signal : - TextManager.Get(signal).Fallback(signal).Value; - } - else if (uiElements[i] is GUINumberInput ni) - { - if (ni.InputType == NumberType.Int) - { - int.TryParse(signal, out int value); - ni.IntValue = value; - } + int.TryParse(signal, out int value); + ni.IntValue = value; } } + else if (uiElement is GUITickBox tickBox) + { + tickBox.Selected = signal.Equals("true", StringComparison.OrdinalIgnoreCase); + } } public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) @@ -360,14 +400,9 @@ namespace Barotrauma.Items.Components for (int i = 0; i < customInterfaceElementList.Count; i++) { var element = customInterfaceElementList[i]; - if (element.HasPropertyName) + switch (element.InputType) { - if (!element.IsNumberInput) - { - msg.WriteString(((GUITextBox)uiElements[i]).Text); - } - else - { + case CustomInterfaceElement.InputTypeOption.Number: switch (element.NumberType) { case NumberType.Float: @@ -378,15 +413,16 @@ namespace Barotrauma.Items.Components msg.WriteString(((GUINumberInput)uiElements[i]).IntValue.ToString()); break; } - } - } - else if (element.ContinuousSignal) - { - msg.WriteBoolean(((GUITickBox)uiElements[i]).Selected); - } - else - { - msg.WriteBoolean(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); + break; + case CustomInterfaceElement.InputTypeOption.Text: + msg.WriteString(((GUITextBox)uiElements[i]).Text); + break; + case CustomInterfaceElement.InputTypeOption.TickBox: + msg.WriteBoolean(((GUITickBox)uiElements[i]).Selected); + break; + case CustomInterfaceElement.InputTypeOption.Button: + msg.WriteBoolean(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); + break; } } } @@ -399,15 +435,10 @@ namespace Barotrauma.Items.Components for (int i = 0; i < customInterfaceElementList.Count; i++) { var element = customInterfaceElementList[i]; - if (element.HasPropertyName) + switch (element.InputType) { - string newValue = msg.ReadString(); - if (!element.IsNumberInput) - { - TextChanged(element, newValue); - } - else - { + case CustomInterfaceElement.InputTypeOption.Number: + string newValue = msg.ReadString(); switch (element.NumberType) { case NumberType.Int when int.TryParse(newValue, out int value): @@ -417,20 +448,23 @@ namespace Barotrauma.Items.Components ValueChanged(element, value); break; } - } - } - else - { - bool elementState = msg.ReadBoolean(); - if (element.ContinuousSignal) - { - ((GUITickBox)uiElements[i]).Selected = elementState; - TickBoxToggled(element, elementState); - } - else if (elementState) - { - ButtonClicked(element); - } + break; + case CustomInterfaceElement.InputTypeOption.Text: + string newTextValue = msg.ReadString(); + TextChanged(element, newTextValue); + break; + case CustomInterfaceElement.InputTypeOption.TickBox: + bool tickBoxState = msg.ReadBoolean(); + ((GUITickBox)uiElements[i]).Selected = tickBoxState; + TickBoxToggled(element, tickBoxState); + break; + case CustomInterfaceElement.InputTypeOption.Button: + bool buttonState = msg.ReadBoolean(); + if (buttonState) + { + ButtonClicked(element); + } + break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 9bafa3d1a..a7c52a99f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -106,7 +106,7 @@ namespace Barotrauma.Items.Components private static int? selectedNodeIndex; private static int? highlightedNodeIndex; - [Serialize(0.3f, IsPropertySaveable.No)] + [Serialize(0.3f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f, DecimalCount = 2)] public float Width { get; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index a47e99738..f37ca4657 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -175,6 +175,8 @@ namespace Barotrauma.Items.Components { if (character == null) { return; } + base.DrawHUD(spriteBatch, character); + if (OverlayColor.A > 0) { GUIStyle.UIGlow.Draw(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), @@ -206,44 +208,51 @@ namespace Barotrauma.Items.Components if (ThermalGoggles) { - spriteBatch.End(); - GameMain.LightManager.SolidColorEffect.Parameters["color"].SetValue(Color.Red.ToVector4() * (0.3f + MathF.Sin(thermalEffectState) * 0.05f)); - GameMain.LightManager.SolidColorEffect.CurrentTechnique = GameMain.LightManager.SolidColorEffect.Techniques["SolidColorBlur"]; - GameMain.LightManager.SolidColorEffect.Parameters["blurDistance"].SetValue(0.01f + MathF.Sin(thermalEffectState) * 0.005f); - GameMain.LightManager.SolidColorEffect.CurrentTechnique.Passes[0].Apply(); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: Screen.Selected.Cam.Transform, effect: GameMain.LightManager.SolidColorEffect); - Entity refEntity = equipper; if (!isEquippable || refEntity == null) { refEntity = item; } + DrawThermalOverlay(spriteBatch, refEntity, character, OverlayColor, Range, thermalEffectState, ShowDeadCharacters); - foreach (Character c in Character.CharacterList) - { - if (c == character || !c.Enabled || c.Removed || c.Params.HideInThermalGoggles) { continue; } - if (!ShowDeadCharacters && c.IsDead) { continue; } - - float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); - if (dist > Range * Range) { continue; } - - Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite; - foreach (Limb limb in c.AnimController.Limbs) - { - if (limb.Mass < 0.5f && limb != c.AnimController.MainLimb) { continue; } - float noise1 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.02f); - float noise2 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.008f); - Vector2 spriteScale = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) / pingCircle.size * (noise1 * 0.5f + 2f); - Vector2 drawPos = new Vector2(limb.body.DrawPosition.X + (noise1 - 0.5f) * 100, -limb.body.DrawPosition.Y + (noise2 - 0.5f) * 100); - pingCircle.Draw(spriteBatch, drawPos, 0.0f, scale: Math.Max(spriteScale.X, spriteScale.Y)); - } - } - - spriteBatch.End(); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); } } + public static void DrawThermalOverlay(SpriteBatch spriteBatch, Entity refEntity, Character user, Color overlayColor, float range, float effectState, bool showDeadCharacters) + { + spriteBatch.End(); + float colorIntensityBase = 0.5f; //Multiplies the overlay color by this amount, the higher the value, the more bright/vibrant the color. + float colorIntensityVariance = 0.05f; //The variance of the pulse effect affecting the color's brightness/vibrance + GameMain.LightManager.SolidColorEffect.Parameters["color"].SetValue(overlayColor.ToVector4() * (colorIntensityBase + MathF.Sin(effectState) * colorIntensityVariance)); + GameMain.LightManager.SolidColorEffect.CurrentTechnique = GameMain.LightManager.SolidColorEffect.Techniques["SolidColorBlur"]; + GameMain.LightManager.SolidColorEffect.Parameters["blurDistance"].SetValue(0.01f + MathF.Sin(effectState) * 0.005f); + GameMain.LightManager.SolidColorEffect.CurrentTechnique.Passes[0].Apply(); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: Screen.Selected.Cam.Transform, effect: GameMain.LightManager.SolidColorEffect); + + foreach (Character c in Character.CharacterList) + { + if (c == user || !c.Enabled || c.Removed || c.Params.HideInThermalGoggles) { continue; } + if (!showDeadCharacters && c.IsDead) { continue; } + + float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); + if (dist > range * range) { continue; } + + Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite; + foreach (Limb limb in c.AnimController.Limbs) + { + if (limb.Mass < 0.5f && limb != c.AnimController.MainLimb) { continue; } + float noise1 = PerlinNoise.GetPerlin((effectState + limb.Params.ID + c.ID) * 0.01f, (effectState + limb.Params.ID + c.ID) * 0.02f); + float noise2 = PerlinNoise.GetPerlin((effectState + limb.Params.ID + c.ID) * 0.01f, (effectState + limb.Params.ID + c.ID) * 0.008f); + Vector2 spriteScale = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) / pingCircle.size * (noise1 * 0.5f + 2f); + Vector2 drawPos = new Vector2(limb.body.DrawPosition.X + (noise1 - 0.5f) * 100, -limb.body.DrawPosition.Y + (noise2 - 0.5f) * 100); + pingCircle.Draw(spriteBatch, drawPos, 0.0f, scale: Math.Max(spriteScale.X, spriteScale.Y)); + } + } + + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); + } + private void DrawCharacterInfo(SpriteBatch spriteBatch, Character target, float alpha = 1.0f) { Vector2 hudPos = GameMain.GameScreen.Cam.WorldToScreen(target.DrawPosition); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs index 77fe1ba5a..aafc299b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs @@ -1,12 +1,28 @@ using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; namespace Barotrauma.Items.Components { - partial class TriggerComponent : ItemComponent, IServerSerializable + partial class TriggerComponent : ItemComponent, IServerSerializable, IDrawableComponent { + public Vector2 DrawSize => + Vector2.One * + (Radius > 0.0f ? Radius * 2 : Math.Max(Width, Height)); + + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) + { + if (editing) + { + PhysicsBody.DebugDraw(spriteBatch, Color.LightGray * 0.7f); + } + } + public void ClientEventRead(IReadMessage msg, float sendingTime) { CurrentForceFluctuation = msg.ReadRangedSingle(0.0f, 1.0f, 8); } + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 53117dd8f..107150d20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -385,17 +385,20 @@ namespace Barotrauma.Items.Components if (item.Condition > 0.0f || !HideBarrelWhenBroken) { - railSprite?.Draw(spriteBatch, + var currentRailSprite = item.Condition <= 0.0f && railSpriteBroken != null ? railSpriteBroken : railSprite; + var currentBarrelSprite = item.Condition <= 0.0f && barrelSpriteBroken != null ? barrelSpriteBroken : barrelSprite; + + currentRailSprite?.Draw(spriteBatch, drawPos, overrideColor ?? item.SpriteColor, Rotation + MathHelper.PiOver2, item.Scale, - SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); + SpriteEffects.None, item.SpriteDepth + (currentRailSprite.Depth - item.Sprite.Depth)); - barrelSprite?.Draw(spriteBatch, + currentBarrelSprite?.Draw(spriteBatch, drawPos - GetRecoilOffset() * item.Scale, overrideColor ?? item.SpriteColor, Rotation + MathHelper.PiOver2, item.Scale, - SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); + SpriteEffects.None, item.SpriteDepth + (currentBarrelSprite.Depth - item.Sprite.Depth)); float chargeRatio = currentChargeTime / MaxChargeTime; @@ -702,12 +705,14 @@ namespace Barotrauma.Items.Components public override void DrawHUD(SpriteBatch spriteBatch, Character character) { + base.DrawHUD(spriteBatch, character); + if (HudTint.A > 0) { GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), new Color(HudTint.R, HudTint.G, HudTint.B) * (HudTint.A / 255.0f), true); } - + GetAvailablePower(out float batteryCharge, out float batteryCapacity); List availableAmmo = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index d756c442c..3df2effc7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1359,7 +1359,7 @@ namespace Barotrauma if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container && container.Inventory.CanBePut(item)) { - if (!container.AllowDragAndDrop || !container.AllowAccess) + if (!container.AllowDragAndDrop || !container.IsAccessible()) { allowCombine = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index b626f62d4..7739f2cce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -130,7 +130,7 @@ namespace Barotrauma } } - public float GetDrawDepth() + public override float GetDrawDepth() { return GetDrawDepth(SpriteDepth + DrawDepthOffset, Sprite); } @@ -287,7 +287,7 @@ namespace Barotrauma } else { - int padding = 100; + int padding = 0; RectangleF boundingBox = GetTransformedQuad().BoundingAxisAlignedRectangle; Vector2 min = new Vector2(-boundingBox.Width / 2 - padding, -boundingBox.Height / 2 - padding); @@ -302,11 +302,11 @@ namespace Barotrauma } foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) { - float scale = decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale; - min.X = Math.Min(-decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * scale, min.X); - min.Y = Math.Min(-decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale, min.Y); - max.X = Math.Max(decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * scale, max.X); - max.Y = Math.Max(decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale, max.Y); + Vector2 scale = decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale; + min.X = Math.Min(-decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * scale.X, min.X); + min.Y = Math.Min(-decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale.Y, min.Y); + max.X = Math.Max(decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * scale.X, max.X); + max.Y = Math.Max(decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale.Y, max.Y); } cachedVisibleExtents = extents = new Rectangle(min.ToPoint(), max.ToPoint()); } @@ -316,6 +316,9 @@ namespace Barotrauma if (worldPosition.X + extents.X > worldView.Right || worldPosition.X + extents.Width < worldView.X) { return false; } if (worldPosition.Y + extents.Height < worldView.Y - worldView.Height || worldPosition.Y + extents.Y > worldView.Y) { return false; } + if (extents.Width * Screen.Selected.Cam.Zoom < 1.0f) { return false; } + if (extents.Height * Screen.Selected.Cam.Zoom < 1.0f) { return false; } + return true; } @@ -490,7 +493,7 @@ namespace Barotrauma } } var head = holdable.Picker.AnimController.GetLimb(LimbType.Head); - if (head != null) + if (head?.Sprite != null) { //ensure the holdable item is always drawn in front of the head no matter what the wearables or whatnot do with the sprite depths depth = @@ -523,8 +526,8 @@ namespace Barotrauma Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, decorativeSprite.Sprite.Origin, + rotation, decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } } @@ -680,7 +683,7 @@ namespace Barotrauma offset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(drawPos.X + offset.X, -(drawPos.Y + offset.Y)), decorativeSpriteColor, origin, - -rotation + spriteRotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffects, + -rotation + spriteRotation, decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } } @@ -1069,12 +1072,18 @@ namespace Barotrauma foreach (RelatedItem relatedItem in requiredItems) { - //TODO: add to localization var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), - relatedItem.Type.ToString() + " required", font: GUIStyle.SmallFont) + TextManager.Get($"{relatedItem.Type}.required").Fallback($"{relatedItem.Type} required"), font: GUIStyle.SmallFont) { Padding = new Vector4(10.0f, 0.0f, 10.0f, 0.0f) }; + + var tooltip = TextManager.Get($"{relatedItem.Type}.required.tooltip").Fallback(LocalizedString.EmptyString); + if (!tooltip.IsNullOrWhiteSpace()) + { + textBlock.ToolTip = tooltip; + } + textBlock.RectTransform.IsFixedSize = true; componentEditor.AddCustomContent(textBlock, 1); @@ -2074,6 +2083,17 @@ namespace Barotrauma } } break; + case EventType.SwapItem: + ushort newId = msg.ReadUInt16(); + uint prefabUintId = msg.ReadUInt32(); + ItemPrefab newPrefab = ItemPrefab.Prefabs.FirstOrDefault(p => p.UintIdentifier == prefabUintId); + if (newPrefab is null) + { + DebugConsole.ThrowError($"Error while reading {EventType.SwapItem} message: could not find an item prefab with the hash {prefabUintId}."); + break; + } + ReplaceFromNetwork(newPrefab, newId); + break; default: throw new Exception($"Malformed incoming item event: unsupported event type {eventType}"); } @@ -2171,6 +2191,18 @@ namespace Barotrauma } } + //if the item is outside the level, but not in a sub, it implies the item is inside a sub server-side but the client failed to properly move it + // -> let's correct that by finding the correct sub + if (Level.IsPositionAboveLevel(WorldPosition) && Submarine == null) + { + var newSub = Submarine.FindContainingInLocalCoordinates(ConvertUnits.ToDisplayUnits(body.SimPosition), inflate: 0.0f); + if (newSub != null) + { + Submarine = newSub; + FindHull(); + } + } + Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition); rect.X = (int)(displayPos.X - rect.Width / 2.0f); rect.Y = (int)(displayPos.Y + rect.Height / 2.0f); @@ -2329,15 +2361,15 @@ namespace Barotrauma ownerSheetIndex = (x, y); } - bool tagsChanged = msg.ReadBoolean(); + bool tagsChanged = msg.ReadBoolean(); string tags = ""; if (tagsChanged) { - HashSet addedTags = msg.ReadString().Split(',').ToIdentifiers().ToHashSet(); - HashSet removedTags = msg.ReadString().Split(',').ToIdentifiers().ToHashSet(); + HashSet addedTags = msg.ReadString().ToIdentifiers().ToHashSet(); + HashSet removedTags = msg.ReadString().ToIdentifiers().ToHashSet(); if (itemPrefab != null) { - tags = string.Join(',',itemPrefab.Tags.Where(t => !removedTags.Contains(t)).Concat(addedTags)); + tags = string.Join(',', itemPrefab.Tags.Where(t => !removedTags.Contains(t)).Union(addedTags)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 419253352..f9ac10b38 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -20,7 +20,8 @@ namespace Barotrauma public override bool IsVisible(Rectangle worldView) { - return Screen.Selected == GameMain.SubEditorScreen || GameMain.DebugDraw; + if (Screen.Selected != GameMain.SubEditorScreen && !GameMain.DebugDraw) { return false; } + return base.IsVisible(worldView); } public override void Draw(SpriteBatch sb, bool editing, bool back = true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 5841d8dc6..cdb3fd9d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -81,16 +81,12 @@ namespace Barotrauma public override bool IsVisible(Rectangle worldView) { if (BallastFlora != null) { return true; } - if (Screen.Selected != GameMain.SubEditorScreen && !GameMain.DebugDraw) { if (decals.Count == 0 && paintAmount < minimumPaintAmountToDraw) { return false; } - Rectangle worldRect = WorldRect; - if (worldRect.X > worldView.Right || worldRect.Right < worldView.X) { return false; } - if (worldRect.Y < worldView.Y - worldView.Height || worldRect.Y - worldRect.Height > worldView.Y) { return false; } } - return true; + return base.IsVisible(worldView); } public override bool IsMouseOn(Vector2 position) @@ -103,12 +99,31 @@ namespace Barotrauma private GUIComponent CreateEditingHUD(bool inGame = false) { + int heightScaled = GUI.IntScale(20); editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null) { CanTakeKeyBoardFocus = false }; - new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont); + var hullEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont); + + if (!inGame) + { + if (Linkable) + { + var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("HoldToLink"), font: GUIStyle.SmallFont); + var hullLinkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("hulllinkinfo"), font: GUIStyle.SmallFont); + var itemsText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("AllowedLinks"), font: GUIStyle.SmallFont); + LocalizedString allowedItems = AllowedLinks.None() ? TextManager.Get("None") : string.Join(", ", AllowedLinks); + itemsText.Text = TextManager.AddPunctuation(':', itemsText.Text, allowedItems); + hullEditor.AddCustomContent(linkText, 1); + hullEditor.AddCustomContent(hullLinkText, 2); + hullEditor.AddCustomContent(itemsText, 3); + linkText.TextColor = GUIStyle.Orange; + hullLinkText.TextColor = GUIStyle.Orange; + itemsText.TextColor = GUIStyle.Orange; + } + } PositionEditingHUD(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 34caf829f..d3d2b8b53 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -47,62 +47,75 @@ namespace Barotrauma if (renderer == null) { return; } renderer.DrawDebugOverlay(spriteBatch, cam); - if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) + if (GameMain.DebugDraw) { - foreach (InterestingPosition pos in PositionsOfInterest) + if (Screen.Selected.Cam.Zoom > 0.1f) { - Color color = Color.Yellow; - if (pos.PositionType == PositionType.Cave || pos.PositionType == PositionType.AbyssCave) + foreach (InterestingPosition pos in PositionsOfInterest) { - color = Color.DarkOrange; - } - else if (pos.PositionType == PositionType.Ruin) - { - color = Color.LightGray; - } - if (!pos.IsValid) - { - color = Color.Red; - } + Color color = Color.Yellow; + if (pos.PositionType == PositionType.Cave || pos.PositionType == PositionType.AbyssCave) + { + color = Color.DarkOrange; + } + else if (pos.PositionType == PositionType.Ruin) + { + color = Color.LightGray; + } + if (!pos.IsValid) + { + color = Color.Red; + } - GUI.DrawRectangle(spriteBatch, new Vector2(pos.Position.X - 15.0f, -pos.Position.Y - 15.0f), new Vector2(30.0f, 30.0f), color, true); + GUI.DrawRectangle(spriteBatch, new Vector2(pos.Position.X - 15.0f, -pos.Position.Y - 15.0f), new Vector2(30.0f, 30.0f), color, true); + } + + foreach (RuinGeneration.Ruin ruin in Ruins) + { + Rectangle ruinArea = ruin.Area; + ruinArea.Y = -ruinArea.Y - ruinArea.Height; + + GUI.DrawRectangle(spriteBatch, ruinArea, Color.DarkSlateBlue, false, 0, 5); + } } - - foreach (RuinGeneration.Ruin ruin in Ruins) - { - Rectangle ruinArea = ruin.Area; - ruinArea.Y = -ruinArea.Y - ruinArea.Height; - - GUI.DrawRectangle(spriteBatch, ruinArea, Color.DarkSlateBlue, false, 0, 5); - } - - foreach (var positions in wreckPositions.Values) + + float zoomFactor = MathHelper.Lerp(20, 1, MathUtils.InverseLerp(Screen.Selected.Cam.MinZoom, Screen.Selected.Cam.DefaultZoom, Screen.Selected.Cam.Zoom)); + foreach ((string debugInfo, List positions) in positionHistory) { for (int i = 0; i < positions.Count; i++) { float t = (i + 1) / (float)positions.Count; - float multiplier = MathHelper.Lerp(0, 1, t); + float multiplier = MathHelper.Lerp(0.1f, 1, t); Color color = Color.Red * multiplier; var pos = positions[i]; pos.Y = -pos.Y; - var size = new Vector2(100); - GUI.DrawRectangle(spriteBatch, pos - size / 2, size, color, thickness: 10); + var size = new Vector2(200); + if (i == 0) + { + GUI.DrawRectangle(spriteBatch, pos - size, size * 2, Color.Red, thickness: 2 * zoomFactor); + GUI.DrawString(spriteBatch, pos - new Vector2(10, 20), debugInfo, Color.White, font: GUIStyle.LargeFont, forceUpperCase: ForceUpperCase.Yes); + } if (i < positions.Count - 1) { + if (i > 0) + { + GUI.DrawRectangle(spriteBatch, pos - size / 2, size, Color.Red, isFilled: true); + } var nextPos = positions[i + 1]; nextPos.Y = -nextPos.Y; - GUI.DrawLine(spriteBatch, pos, nextPos, color, width: 10); + GUI.DrawLine(spriteBatch, pos, nextPos, color, width: 4 * zoomFactor); } } } - foreach (var rects in blockedRects.Values) + foreach ((Submarine sub, List rects) in blockedRects) { - foreach (var rect in rects) + foreach (Rectangle t in rects) { - Rectangle newRect = rect; + Rectangle newRect = t; newRect.Y = -newRect.Y; - GUI.DrawRectangle(spriteBatch, newRect, Color.Red, thickness: 5); + GUI.DrawRectangle(spriteBatch, newRect, Color.Red * 0.1f, isFilled: true); + GUI.DrawString(spriteBatch, newRect.Center.ToVector2(), $"{sub.Info.Name}", Color.White, font: GUIStyle.LargeFont, forceUpperCase: ForceUpperCase.Yes); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 8b6a6b6fe..c5df0ac7a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; +using Barotrauma.Particles; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -239,7 +240,8 @@ namespace Barotrauma public void DrawBackground(SpriteBatch spriteBatch, Camera cam, LevelObjectManager backgroundSpriteManager = null, - BackgroundCreatureManager backgroundCreatureManager = null) + BackgroundCreatureManager backgroundCreatureManager = null, + ParticleManager particleManager = null) { spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap); @@ -277,7 +279,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.DepthRead, null, null, - cam.Transform); + cam.Transform); backgroundSpriteManager?.DrawObjectsBack(spriteBatch, cam); if (cam.Zoom > 0.05f) @@ -321,6 +323,9 @@ namespace Barotrauma color: level.GenerationParams.WaterParticleColor * alpha, textureScale: new Vector2(texScale)); } } + + GameMain.ParticleManager?.Draw(spriteBatch, inWater: true, inSub: false, ParticleBlendState.AlphaBlend, background: true); + spriteBatch.End(); RenderWalls(GameMain.Instance.GraphicsDevice, cam); @@ -465,7 +470,8 @@ namespace Barotrauma var wallList = i == 0 ? level.ExtraWalls : level.UnsyncedExtraWalls; foreach (LevelWall wall in wallList) { - if (!(wall is DestructibleLevelWall destructibleWall) || destructibleWall.Destroyed) { continue; } + if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; } + if (!wall.IsVisible(cam.WorldView)) { continue; } wallCenterEffect.Texture = level.GenerationParams.DestructibleWallSprite?.Texture ?? level.GenerationParams.WallSprite.Texture; wallCenterEffect.World = wall.GetTransform() * transformMatrix; @@ -521,6 +527,7 @@ namespace Barotrauma foreach (LevelWall wall in wallList) { if (wall is DestructibleLevelWall) { continue; } + if (!wall.IsVisible(cam.WorldView)) { continue; } //TODO: use LevelWallVertexBuffers for extra walls as well wallCenterEffect.World = wall.GetTransform() * transformMatrix; wallCenterEffect.Alpha = wall.Alpha; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs index 5130227b4..7c60d6793 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs @@ -1,8 +1,10 @@ -using FarseerPhysics; +using Barotrauma.Extensions; +using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -41,5 +43,23 @@ namespace Barotrauma level.GenerationParams.WallEdgeSprite.Texture, color); } + + public bool IsVisible(Rectangle worldView) + { + RectangleF worldViewInSimUnits = new RectangleF( + ConvertUnits.ToSimUnits(worldView.Location.ToVector2()), + ConvertUnits.ToSimUnits(worldView.Size.ToVector2())); + + foreach (var fixture in Body.FixtureList) + { + fixture.GetAABB(out var aabb, 0); + Vector2 lowerBound = aabb.LowerBound + Body.Position; + if (lowerBound.X > worldViewInSimUnits.Right || lowerBound.Y > worldViewInSimUnits.Y) { continue; } + Vector2 upperBound = aabb.UpperBound + Body.Position; + if (upperBound.X < worldViewInSimUnits.X || upperBound.Y < worldViewInSimUnits.Y - worldViewInSimUnits.Height) { continue; } + return true; + } + return false; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 31c08ce71..4c0381ad6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -1,5 +1,4 @@ -using Barotrauma.Items.Components; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; @@ -13,6 +12,7 @@ namespace Barotrauma.Lights public readonly Submarine Submarine; public HashSet IsHidden = new HashSet(); + public HashSet HasBeenVisible = new HashSet(); public readonly List List = new List(); public ConvexHullList(Submarine submarine) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 9bed09947..bee948d79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -174,7 +174,7 @@ namespace Barotrauma.Lights } private readonly List activeLights = new List(capacity: 100); - private readonly List activeLightsWithLightVolume = new List(capacity: 100); + private readonly List activeShadowCastingLights = new List(capacity: 100); public static int ActiveLightCount { get; private set; } @@ -279,7 +279,7 @@ namespace Barotrauma.Lights } //above the top boundary of the level (in an inactive respawn shuttle?) - if (Level.Loaded != null && light.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (Level.IsPositionAboveLevel(light.WorldPosition)) { continue; } float range = light.LightSourceParams.TextureRange; if (light.LightSprite != null) @@ -315,19 +315,20 @@ namespace Barotrauma.Lights } //find the lights with an active light volume - activeLightsWithLightVolume.Clear(); + activeShadowCastingLights.Clear(); foreach (var activeLight in activeLights) { + if (!activeLight.CastShadows) { continue; } if (activeLight.Range < 1.0f || activeLight.Color.A < 1 || activeLight.CurrentBrightness <= 0.0f) { continue; } - activeLightsWithLightVolume.Add(activeLight); + activeShadowCastingLights.Add(activeLight); } //remove some lights with a light volume if there's too many of them - if (activeLightsWithLightVolume.Count > GameSettings.CurrentConfig.Graphics.VisibleLightLimit && Screen.Selected is { IsEditor: false }) + if (activeShadowCastingLights.Count > GameSettings.CurrentConfig.Graphics.VisibleLightLimit && Screen.Selected is { IsEditor: false }) { - for (int i = GameSettings.CurrentConfig.Graphics.VisibleLightLimit; i < activeLightsWithLightVolume.Count; i++) + for (int i = GameSettings.CurrentConfig.Graphics.VisibleLightLimit; i < activeShadowCastingLights.Count; i++) { - activeLights.Remove(activeLightsWithLightVolume[i]); + activeLights.Remove(activeShadowCastingLights[i]); } } activeLights.Sort((l1, l2) => l1.LastRecalculationTime.CompareTo(l2.LastRecalculationTime)); @@ -827,7 +828,7 @@ namespace Barotrauma.Lights public void ClearLights() { activeLights.Clear(); - activeLightsWithLightVolume.Clear(); + activeShadowCastingLights.Clear(); lights.Clear(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 5e9a1f267..498b8d180 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -245,7 +245,7 @@ namespace Barotrauma.Lights } set { - if (!needsRecalculation && value) + if (value) { foreach (ConvexHullList chList in convexHullsInRange) { @@ -550,7 +550,6 @@ namespace Barotrauma.Lights chList.List.Clear(); foreach (var convexHull in fullChList.List) { - if (!convexHull.Enabled) { continue; } if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; } if (lightSourceParams.Directional) { @@ -572,6 +571,7 @@ namespace Barotrauma.Lights chList.List.Add(convexHull); } chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch)); + chList.HasBeenVisible.RemoveWhere(ch => !chList.List.Contains(ch)); HullsUpToDate.Add(sub); } @@ -604,7 +604,8 @@ namespace Barotrauma.Lights foreach (var ch in chList.List) { - if (ch.LastVertexChangeTime > LastRecalculationTime && !chList.IsHidden.Contains(ch)) + if (ch.LastVertexChangeTime > LastRecalculationTime && + (!chList.IsHidden.Contains(ch) || chList.HasBeenVisible.Contains(ch))) { NeedsRecalculation = true; break; @@ -712,8 +713,8 @@ namespace Barotrauma.Lights { foreach (ConvexHull hull in chList.List) { - if (hull.IsInvalid) { continue; } - if (!chList.IsHidden.Contains(hull)) + if (hull.IsInvalid || !hull.Enabled) { continue; } + if (!chList.IsHidden.Contains(hull) || chList.HasBeenVisible.Contains(hull)) { //find convexhull segments that are close enough and facing towards the light source lock (mutex) @@ -732,7 +733,17 @@ namespace Barotrauma.Lights } foreach (ConvexHull hull in chList.List) { - chList.IsHidden.Add(hull); + if (!hull.Enabled) + { + //if the hull is not enabled, we cannot determine if it's visible or hidden from the point of view of the light source + //so let's not mark it as hidden, but instead consider it as something that has been visible, so we know to recalculate if/when it becomes enabled again + chList.IsHidden.Remove(hull); + chList.HasBeenVisible.Add(hull); + continue; + } + + //mark convex hulls as hidden at this point, they're removed if we find any of the segments to be visible + chList.IsHidden.Add(hull); } } @@ -1411,14 +1422,7 @@ namespace Barotrauma.Lights { if (conditionals.None()) { return; } if (conditionalTarget == null) { return; } - if (logicalOperator == PropertyConditional.LogicalOperatorType.And) - { - Enabled = conditionals.All(c => c.Matches(conditionalTarget)); - } - else - { - Enabled = conditionals.Any(c => c.Matches(conditionalTarget)); - } + Enabled = PropertyConditional.CheckConditionals(conditionalTarget, conditionals, logicalOperator); } public void DebugDrawVertices(SpriteBatch spriteBatch) @@ -1501,6 +1505,7 @@ namespace Barotrauma.Lights foreach (var convexHullList in convexHullsInRange) { convexHullList.IsHidden.Remove(visibleConvexHull); + convexHullList.HasBeenVisible.Add(visibleConvexHull); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index a29588def..0636133f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -923,23 +923,25 @@ namespace Barotrauma if (GameMain.DebugDraw) { - Vector2 dPos = pos; + //move the debug texts upwards so they don't go under the info panel that appears when highlighted + Vector2 dPos = pos + new Vector2(15, -100); if (location == HighlightedLocation) { - dPos.Y -= 80; - GUI.DrawString(spriteBatch, dPos + new Vector2(15, 32), "Faction: " + (location.Faction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); - GUI.DrawString(spriteBatch, dPos + new Vector2(15, 50), "Secondary Faction: " + (location.SecondaryFaction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); - dPos.Y += 48; + GUI.DrawString(spriteBatch, dPos, "Faction: " + (location.Faction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); + GUI.DrawString(spriteBatch, dPos + new Vector2(0, 18), "Secondary Faction: " + (location.SecondaryFaction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); + dPos.Y += 50; if (PlayerInput.KeyDown(Keys.LeftShift)) { - GUI.DrawString(spriteBatch, new Vector2(150,150), "Dist: " + + GUI.DrawString(spriteBatch, new Vector2(150, 150), "Dist: " + GetDistanceToClosestLocationOrConnection(CurrentLocation, int.MaxValue, loc => loc == location), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); - } + GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatSingleDecimal()}", + ToolBox.GradientLerp(location.LevelData.Difficulty / 100.0f, GUIStyle.Blue, GUIStyle.Yellow, GUIStyle.Red), Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); + + dPos.Y += 25; + GUI.DrawString(spriteBatch, dPos, $"Biome: {location.LevelData.Biome.DisplayName} ({location.LevelData.GenerationParams.Identifier})", Color.White, Color.Black, font: GUIStyle.SmallFont); } - dPos.Y += 48; - GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatSingleDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); } } } @@ -1196,7 +1198,9 @@ namespace Barotrauma Vector2 center = rectCenter + (connection.CenterPos + viewOffset) * zoom; if (viewArea.Contains(center) && connection.Biome != null) { - GUI.DrawString(spriteBatch, center, (connection.LevelData?.GenerationParams?.Identifier ?? connection.Biome.Identifier) + " (" + connection.Difficulty.FormatSingleDecimal() + ")", Color.White); + GUI.DrawString(spriteBatch, center - Vector2.UnitX * 50, + $"{(connection.LevelData?.GenerationParams?.Identifier ?? connection.Biome.Identifier)} ({connection.Difficulty.FormatSingleDecimal()})", + ToolBox.GradientLerp(connection.Difficulty / 100.0f, GUIStyle.Blue, GUIStyle.Yellow, GUIStyle.Red), backgroundColor: Color.Black * 0.7f, font: GUIStyle.SmallFont); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 6560a4ea1..38549dddf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -81,6 +81,16 @@ namespace Barotrauma public virtual bool IsVisible(Rectangle worldView) { + Rectangle worldRect = WorldRect; + if (worldRect.X > worldView.Right || worldRect.Right < worldView.X) { return false; } + if (worldRect.Y < worldView.Y - worldView.Height || worldRect.Y - worldRect.Height > worldView.Y) { return false; } + //zoomed extremely far out -> no need to render + if (Screen.Selected.Cam.Zoom < 0.05f) { return false; } + if (worldRect.Width * Screen.Selected.Cam.Zoom < 1.0f || + worldRect.Height * Screen.Selected.Cam.Zoom < 1.0f) + { + return false; + } return true; } @@ -91,6 +101,8 @@ namespace Barotrauma public virtual void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { } + public virtual float GetDrawDepth() { return 0.0f; } + /// /// A method that modifies the draw depth to prevent z-fighting between entities with the same sprite depth /// @@ -305,6 +317,15 @@ namespace Barotrauma if (PlayerInput.IsCtrlDown()) { HashSet clones = Clone(SelectedList.ToList()).Where(c => c != null).ToHashSet(); + + if (clones.Count == 1) + { + if (clones.First() is WayPoint wayPoint && SelectedList.First() is WayPoint originalWaypoint && originalWaypoint.SpawnType == SpawnType.Path) + { + originalWaypoint.ConnectTo(wayPoint); + } + } + SelectedList = clones; SelectedList.ForEach(c => c.Move(moveAmount)); SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(clones), false)); @@ -1068,6 +1089,7 @@ namespace Barotrauma } SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false, handleInventoryBehavior: false)); + if (Screen.Selected is SubEditorScreen subEditor) { subEditor.ReconstructLayers(); } } /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs index ca438606f..a309ba35f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -73,9 +73,16 @@ namespace Barotrauma } Sound? existingSound = null; - if (roundSoundByPath.TryGetValue(filename.FullPath, out RoundSound? rs) && rs.Sound is { Disposed: false }) + if (roundSoundByPath.TryGetValue(filename.FullPath, out RoundSound? rs)) { - existingSound = rs.Sound; + if (rs.Sound is { Disposed: false }) + { + existingSound = rs.Sound; + } + else + { + roundSoundByPath.Remove(filename.FullPath); + } } if (existingSound is null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 5f012c8a0..ff6d39108 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -328,11 +328,11 @@ namespace Barotrauma Vector2 max = new Vector2(worldRect.Right, worldRect.Y + worldRect.Height); foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) { - float scale = decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale; - min.X = Math.Min(worldPos.X - decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * scale, min.X); - max.X = Math.Max(worldPos.X + decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * scale, max.X); - min.Y = Math.Min(worldPos.Y - decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale, min.Y); - max.Y = Math.Max(worldPos.Y + decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale, max.Y); + Vector2 scale = decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale; + min.X = Math.Min(worldPos.X - decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * scale.X, min.X); + max.X = Math.Max(worldPos.X + decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * scale.X, max.X); + min.Y = Math.Min(worldPos.Y - decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale.Y, min.Y); + max.Y = Math.Max(worldPos.Y + decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale.Y, max.Y); } Vector2 offset = GetCollapseEffectOffset(); min += offset; @@ -341,6 +341,9 @@ namespace Barotrauma if (min.X > worldView.Right || max.X < worldView.X) { return false; } if (min.Y > worldView.Y || max.Y < worldView.Y - worldView.Height) { return false; } + Vector2 extents = max - min; + if (extents.X * Screen.Selected.Cam.Zoom < 1.0f) { return false; } + if (extents.Y * Screen.Selected.Cam.Zoom < 1.0f) { return false; } return true; } @@ -368,7 +371,7 @@ namespace Barotrauma return SpriteDepthOverrideIsSet ? SpriteOverrideDepth : Prefab.Sprite.Depth; } - public float GetDrawDepth() + public override float GetDrawDepth() { return GetDrawDepth(GetRealDepth(), Prefab.Sprite); } @@ -560,7 +563,8 @@ namespace Barotrauma pos: drawPos.FlipY(), color: color, rotate: rotation, - scale: decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, + origin: decorativeSprite.Sprite.Origin, + scale: decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect: Prefab.Sprite.effects ^ SpriteEffects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - Prefab.Sprite.Depth), 0.999f)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 490077abb..aab1e5d7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -656,6 +656,7 @@ namespace Barotrauma errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning", ("[doorcount]", doorLinks.ToString()), ("[freeconnectioncount]", (item.Connections[i].MaxWires - wireCount).ToString())).Value); + warnings.Add(SubEditorScreen.WarningType.InsufficientFreeConnectionsWarning); break; } } @@ -836,7 +837,7 @@ namespace Barotrauma subBody.PositionBuffer.Insert(index, posInfo); } } - + public void ClientEventRead(IReadMessage msg, float sendingTime) { Identifier layerIdentifier = msg.ReadIdentifier(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index f544b6dc3..c507940b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -572,8 +572,9 @@ namespace Barotrauma Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; if (flippedX) { offset.X = -offset.X; } if (flippedY) { offset.Y = -offset.Y; } - decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, - rotationRad + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, + float throwAway = 0.0f; + decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, decorativeSprite.Sprite.Origin, + rotationRad + rot, decorativeSprite.GetScale(ref throwAway, 0f) * scale, prefab.Sprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 456dbcc0c..22ca81a35 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -15,7 +15,8 @@ namespace Barotrauma public override bool IsVisible(Rectangle worldView) { - return Screen.Selected == GameMain.SubEditorScreen || GameMain.DebugDraw; + if (Screen.Selected != GameMain.SubEditorScreen && !GameMain.DebugDraw) { return false; } + return base.IsVisible(worldView); } public override bool SelectableInEditor @@ -92,6 +93,11 @@ namespace Barotrauma if (sprite != null) { float spriteScale = iconSize / (float)sprite.SourceRect.Width; + if (Ladders == null && ConnectedDoor == null && ConnectedGap != null) + { + clr = Color.White; + spriteScale *= 1.5f; + } sprite.Draw(spriteBatch, drawPos, clr, origin: sprite.size / 2, scale: spriteScale, depth: 0.001f); sprite2?.Draw(spriteBatch, drawPos + sprite.size * spriteScale * 0.5f, clr, origin: sprite2.size / 2, scale: spriteScale, depth: 0.001f); } @@ -100,27 +106,39 @@ namespace Barotrauma { AssignedJob.Icon.Draw(spriteBatch, drawPos, AssignedJob.UIColor, scale: iconSize / (float)AssignedJob.Icon.SourceRect.Width * 0.8f, depth: 0.0f); } - - foreach (MapEntity e in linkedTo) + + // alternate line drawing for when cloning the waypoint: line goes from current position to original position, where moving started + if (StartMovingPos != Vector2.Zero && SelectedList.Contains(this) && PlayerInput.IsCtrlDown()) { GUI.DrawLine(spriteBatch, drawPos, - new Vector2(e.DrawPosition.X, -e.DrawPosition.Y), + new Vector2(StartMovingPos.X, -StartMovingPos.Y), (IsTraversable ? GUIStyle.Green : Color.Gray) * 0.7f, width: 5, depth: 0.002f); } + else + { + foreach (MapEntity e in linkedTo) + { + GUI.DrawLine(spriteBatch, + drawPos, + new Vector2(e.DrawPosition.X, -e.DrawPosition.Y), + (IsTraversable ? GUIStyle.Green : Color.Gray) * 0.7f, width: 5, depth: 0.002f); + } + } + if (ConnectedGap != null) { GUI.DrawLine(spriteBatch, drawPos, new Vector2(ConnectedGap.DrawPosition.X, -ConnectedGap.DrawPosition.Y), - GUIStyle.Green * 0.5f, width: 1); + Color.White, width: 1); } if (Ladders != null) { GUI.DrawLine(spriteBatch, drawPos, new Vector2(Ladders.Item.DrawPosition.X, -Ladders.Item.DrawPosition.Y), - GUIStyle.Green * 0.5f, width: 1); + Color.White, width: 1); } var color = Color.WhiteSmoke; @@ -419,6 +437,7 @@ namespace Barotrauma jobDropDown.AddItem(TextManager.Get("Any"), null); foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) { + if (jobPrefab.HiddenJob) { continue; } jobDropDown.AddItem(jobPrefab.Name, jobPrefab); } jobDropDown.SelectItem(AssignedJob); @@ -432,7 +451,7 @@ namespace Barotrauma }; propertyBox.OnTextChanged += (textBox, text) => { - tags = text.Split(',').ToIdentifiers().ToHashSet(); + tags = text.ToIdentifiers().ToHashSet(); return true; }; propertyBox.OnEnterPressed += (textBox, text) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 1a9fb2c71..5bda4ceb1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -260,7 +260,7 @@ namespace Barotrauma.Networking { try { - Directory.CreateDirectory(downloadFolder); + Directory.CreateDirectory(downloadFolder, catchUnauthorizedAccessExceptions: false); } catch (Exception e) { @@ -572,7 +572,7 @@ namespace Barotrauma.Networking { try { - File.Delete(transfer.FilePath); + File.Delete(transfer.FilePath, catchUnauthorizedAccessExceptions: false); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 3b43b5c1a..e309be0ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -7,9 +7,11 @@ using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; +using Barotrauma.PerkBehaviors; namespace Barotrauma.Networking { @@ -41,8 +43,9 @@ namespace Barotrauma.Networking nameId++; } - public void ForceNameAndJobUpdate() + public void ForceNameJobTeamUpdate() { + // Triggers SendLobbyUpdate() which causes the server to call GameServer.ClientReadLobby() nameId++; } @@ -137,13 +140,14 @@ namespace Barotrauma.Networking } } + public Client MyClient => ConnectedClients.FirstOrDefault(c => c.SessionId == SessionId); + public Option Ping { get { - Client selfClient = ConnectedClients.FirstOrDefault(c => c.SessionId == SessionId); - if (selfClient is null || selfClient.Ping == 0) { return Option.None(); } - return Option.Some(selfClient.Ping); + if (MyClient is null || MyClient.Ping == 0) { return Option.None(); } + return Option.Some(MyClient.Ping); } } @@ -485,14 +489,13 @@ namespace Barotrauma.Networking { if (VoipCapture.Instance.LastEnqueueAudio > DateTime.Now - new TimeSpan(0, 0, 0, 0, milliseconds: 100)) { - var myClient = ConnectedClients.Find(c => c.SessionId == SessionId); if (Screen.Selected == GameMain.NetLobbyScreen) { - GameMain.NetLobbyScreen.SetPlayerSpeaking(myClient); + GameMain.NetLobbyScreen.SetPlayerSpeaking(MyClient); } else { - GameMain.GameSession?.CrewManager?.SetClientSpeaking(myClient); + GameMain.GameSession?.CrewManager?.SetClientSpeaking(MyClient); } } } @@ -689,6 +692,16 @@ namespace Barotrauma.Networking string subName = inc.ReadString(); string subHash = inc.ReadString(); + bool hasEnemySub = inc.ReadBoolean(); + + string enemySubName = subName; + string enemySubHash = subHash; + if (hasEnemySub) + { + enemySubName = inc.ReadString(); + enemySubHash = inc.ReadString(); + } + bool usingShuttle = inc.ReadBoolean(); string shuttleName = inc.ReadString(); string shuttleHash = inc.ReadString(); @@ -709,8 +722,13 @@ namespace Barotrauma.Networking bool readyToStart; if (campaign == null && campaignID == 0) { - readyToStart = GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList) && - GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox); + readyToStart = GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, SelectedSubType.Sub, GameMain.NetLobbyScreen.SubList) && + GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, SelectedSubType.Shuttle, GameMain.NetLobbyScreen.ShuttleList.ListBox); + + if (hasEnemySub && !GameMain.NetLobbyScreen.TrySelectSub(enemySubName, enemySubHash, SelectedSubType.EnemySub, GameMain.NetLobbyScreen.SubList)) + { + readyToStart = false; + } } else { @@ -733,6 +751,19 @@ namespace Barotrauma.Networking CoroutineManager.StartCoroutine(NetLobbyScreen.WaitForStartRound(startButton: null), "WaitForStartRound"); } break; + case ServerPacketHeader.WARN_STARTGAME: + DebugConsole.Log("Received WARN_STARTGAME packet."); + + RoundStartWarningData warningData = INetSerializableStruct.Read(inc); + var team1IncompatiblePerks = ToolBox.UintIdentifierArrayToPrefabCollection(DisembarkPerkPrefab.Prefabs, warningData.Team1IncompatiblePerks); + var team2IncompatiblePerks = ToolBox.UintIdentifierArrayToPrefabCollection(DisembarkPerkPrefab.Prefabs, warningData.Team2IncompatiblePerks); + + GameMain.NetLobbyScreen?.ShowStartRoundWarning(SerializableDateTime.UtcNow + TimeSpan.FromSeconds(warningData.RoundStartsAnywaysTimeInSeconds), warningData.Team1Sub, team1IncompatiblePerks, warningData.Team2Sub, team2IncompatiblePerks); + break; + case ServerPacketHeader.CANCEL_STARTGAME: + DebugConsole.Log("Received CANCEL_STARTGAME packet."); + GameMain.NetLobbyScreen?.CloseStartRoundWarning(); + break; case ServerPacketHeader.STARTGAME: DebugConsole.Log("Received STARTGAME packet."); if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is CampaignMode) @@ -869,6 +900,9 @@ namespace Barotrauma.Networking case ServerPacketHeader.EVENTACTION: GameMain.GameSession?.EventManager.ClientRead(inc); break; + case ServerPacketHeader.SEND_BACKUP_INDICES: + GameMain.NetLobbyScreen?.CampaignSetupUI?.OnBackupIndicesReceived(inc); + break; } } @@ -952,7 +986,7 @@ namespace Barotrauma.Networking ", server value " + stage + ": " + levelEqualityCheckValues[stage].ToString("X") + ", level value count: " + levelEqualityCheckValues.Count + ", seed: " + Level.Loaded.Seed + - ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + + ", sub: " + (Submarine.MainSub == null ? "null" : (Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")")) + ", mirrored: " + Level.Loaded.Mirrored + "). Round init status: " + roundInitStatus + "." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); @@ -969,9 +1003,24 @@ namespace Barotrauma.Networking CrewManager.ClientReadActiveOrders(inc); } + if (inc.ReadBoolean()) + { + ApplyDisembarkPerk(); + } + roundInitStatus = RoundInitStatus.Started; } + private void ApplyDisembarkPerk() + { + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + + ImmutableArray team1Characters = characters.Where(static c => c.TeamID is CharacterTeamType.Team1).ToImmutableArray(), + team2Characters = characters.Where(static c => c.TeamID is CharacterTeamType.Team2).ToImmutableArray(); + + GameSession.GetPerks().ApplyAll(team1Characters, team2Characters); + } + /// /// Fires when the ClientPeer gets disconnected from the server. Does not necessarily mean the client is shutting down, we may still be able to reconnect. /// @@ -1396,6 +1445,7 @@ namespace Barotrauma.Networking ServerSettings.LockAllDefaultWires = inc.ReadBoolean(); ServerSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); + ServerSettings.RespawnMode = (RespawnMode)inc.ReadByte(); bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); ServerSettings.ShowEnemyHealthBars = (EnemyHealthBarMode)inc.ReadByte(); @@ -1419,19 +1469,38 @@ namespace Barotrauma.Networking string subHash = inc.ReadString(); string shuttleName = inc.ReadString(); string shuttleHash = inc.ReadString(); + + bool hasEnemySub = inc.ReadBoolean(); + string enemySubName = subName; + string enemySubHash = subHash; + if (hasEnemySub) + { + enemySubName = inc.ReadString(); + enemySubHash = inc.ReadString(); + } + List missionHashes = new List(); int missionCount = inc.ReadByte(); for (int i = 0; i < missionCount; i++) { missionHashes.Add(inc.ReadUInt32()); } - if (!GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList)) + if (!GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, SelectedSubType.Sub, GameMain.NetLobbyScreen.SubList)) { roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Success; } - if (!GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox)) + if (hasEnemySub) + { + if (!GameMain.NetLobbyScreen.TrySelectSub(enemySubName, enemySubHash, SelectedSubType.EnemySub, GameMain.NetLobbyScreen.SubList)) + { + roundInitStatus = RoundInitStatus.Interrupted; + yield return CoroutineStatus.Success; + } + } + + if (!GameMain.NetLobbyScreen.TrySelectSub(shuttleName, shuttleHash, SelectedSubType.Shuttle, GameMain.NetLobbyScreen.ShuttleList.ListBox)) { roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Success; @@ -1480,8 +1549,10 @@ namespace Barotrauma.Networking var selectedMissions = missionHashes.Select(i => MissionPrefab.Prefabs.Find(p => p.UintIdentifier == i)); - GameMain.GameSession = new GameSession(GameMain.NetLobbyScreen.SelectedSub, gameMode, missionPrefabs: selectedMissions); - GameMain.GameSession.StartRound(levelSeed, levelDifficulty); + var selectedEnemySub = hasEnemySub && GameMain.NetLobbyScreen.SelectedEnemySub is { } enemySub ? Option.Some(enemySub) : Option.None; + + GameMain.GameSession = new GameSession(GameMain.NetLobbyScreen.SelectedSub, selectedEnemySub, gameMode, missionPrefabs: selectedMissions); + GameMain.GameSession.StartRound(levelSeed, levelDifficulty, levelGenerationParams: null, forceBiome: ServerSettings.Biome); } else { @@ -1667,7 +1738,8 @@ namespace Barotrauma.Networking } } - if (GameMain.GameSession.Submarine.Info.IsFileCorrupted) + if (GameMain.GameSession.Submarine != null && + GameMain.GameSession.Submarine.Info.IsFileCorrupted) { DebugConsole.ThrowError($"Failed to start a round. Could not load the submarine \"{GameMain.GameSession.Submarine.Info.Name}\"."); yield return CoroutineStatus.Failure; @@ -1759,12 +1831,15 @@ namespace Barotrauma.Networking refSub = Submarine.MainSubs[1]; } - // Enable characters near the main sub for the endCinematic - foreach (Character c in Character.CharacterList) + if (refSub != null) { - if (Vector2.DistanceSquared(refSub.WorldPosition, c.WorldPosition) < MathUtils.Pow2(c.Params.DisableDistance)) + // Enable characters near the main sub for the endCinematic + foreach (Character c in Character.CharacterList) { - c.Enabled = true; + if (Vector2.DistanceSquared(refSub.WorldPosition, c.WorldPosition) < MathUtils.Pow2(c.Params.DisableDistance)) + { + c.Enabled = true; + } } } @@ -1851,6 +1926,8 @@ namespace Barotrauma.Networking { bool refreshCampaignUI = false; UInt16 listId = inc.ReadUInt16(); + GameMain.NetLobbyScreen.Team1Count = inc.ReadByte(); + GameMain.NetLobbyScreen.Team2Count = inc.ReadByte(); List tempClients = new List(); int clientCount = inc.ReadByte(); for (int i = 0; i < clientCount; i++) @@ -1883,19 +1960,30 @@ namespace Barotrauma.Networking existingClient.NameId = tc.NameId; existingClient.PreferredJob = tc.PreferredJob; existingClient.PreferredTeam = tc.PreferredTeam; + existingClient.TeamID = tc.TeamID; existingClient.Character = null; existingClient.Karma = tc.Karma; existingClient.Muted = tc.Muted; existingClient.InGame = tc.InGame; existingClient.IsOwner = tc.IsOwner; existingClient.IsDownloading = tc.IsDownloading; - GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient); + GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient); // refresh lobby player list in the local UI if (Screen.Selected != GameMain.NetLobbyScreen && tc.CharacterId > 0) { existingClient.CharacterID = tc.CharacterId; } if (existingClient.SessionId == SessionId) { + MultiplayerPreferences.Instance.TeamPreference = existingClient.PreferredTeam; + // If a team is already selected, make sure the UI reflects it + if (MultiplayerPreferences.Instance.TeamPreference != CharacterTeamType.None) + { + GameMain.NetLobbyScreen.TeamPreferenceListBox?.Select(MultiplayerPreferences.Instance.TeamPreference); + } + else + { + GameMain.NetLobbyScreen.RefreshPvpTeamSelectionButtons(); + } existingClient.SetPermissions(permissions, permittedConsoleCommands); if (!NetIdUtils.IdMoreRecent(nameId, tc.NameId)) { @@ -1950,6 +2038,7 @@ namespace Barotrauma.Networking Steam.SteamManager.UpdateLobby(ServerSettings); } + GameMain.NetLobbyScreen?.UpdateDisembarkPointListFromServerSettings(); } if (refreshCampaignUI) @@ -1960,6 +2049,7 @@ namespace Barotrauma.Networking campaign.CampaignUI?.HRManagerUI?.RefreshUI(); } } + } private bool initialUpdateReceived; @@ -1997,6 +2087,15 @@ namespace Barotrauma.Networking string selectSubName = inc.ReadString(); string selectSubHash = inc.ReadString(); + bool usingEnemySub = inc.ReadBoolean(); + string selectEnemySubName = selectSubName; + string selectEnemySubHash = selectSubHash; + if (usingEnemySub) + { + selectEnemySubName = inc.ReadString(); + selectEnemySubHash = inc.ReadString(); + } + bool usingShuttle = inc.ReadBoolean(); string selectShuttleName = inc.ReadString(); string selectShuttleHash = inc.ReadString(); @@ -2011,7 +2110,13 @@ namespace Barotrauma.Networking float traitorProbability = inc.ReadSingle(); int traitorDangerLevel = inc.ReadRangedInteger(TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel); - MissionType missionType = (MissionType)inc.ReadRangedInteger(0, (int)MissionType.All); + List missionTypes = new List(); + uint missionTypeCount = inc.ReadVariableUInt32(); + for (int i = 0; i < missionTypeCount; i++) + { + missionTypes.Add(inc.ReadIdentifier()); + } + int modeIndex = inc.ReadByte(); string levelSeed = inc.ReadString(); @@ -2048,12 +2153,19 @@ namespace Barotrauma.Networking ServerSettings.ServerLog.ServerName = ServerSettings.ServerName; GameMain.NetLobbyScreen.UsingShuttle = usingShuttle; - if (!allowSubVoting || GameMain.NetLobbyScreen.SelectedSub == null) { GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, GameMain.NetLobbyScreen.SubList); } - GameMain.NetLobbyScreen.TrySelectSub(selectShuttleName, selectShuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox); + if (!allowSubVoting || GameMain.NetLobbyScreen.SelectedSub == null) + { + GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, SelectedSubType.Sub, GameMain.NetLobbyScreen.SubList); + if (usingEnemySub) + { + GameMain.NetLobbyScreen.TrySelectSub(selectEnemySubName, selectEnemySubHash, SelectedSubType.EnemySub, GameMain.NetLobbyScreen.SubList); + } + } + GameMain.NetLobbyScreen.TrySelectSub(selectShuttleName, selectShuttleHash, SelectedSubType.Shuttle, GameMain.NetLobbyScreen.ShuttleList.ListBox); GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability); GameMain.NetLobbyScreen.SetTraitorDangerLevel(traitorDangerLevel); - GameMain.NetLobbyScreen.SetMissionType(missionType); + GameMain.NetLobbyScreen.SetMissionTypes(missionTypes); GameMain.NetLobbyScreen.LevelSeed = levelSeed; GameMain.NetLobbyScreen.SelectMode(modeIndex); @@ -2489,14 +2601,20 @@ namespace Barotrauma.Networking ((SubmarineInfo)c.UserData).MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation); if (subElement == null) { continue; } - Color newSubTextColor = new Color(subElement.GetChild().TextColor, 1.0f); - subElement.GetChild().TextColor = newSubTextColor; - - if (subElement.GetChildByUserData("classtext") is GUITextBlock classTextBlock) + //set the dimmed out submarine info back to normal and update texts + if (subElement.FindChild("nametext", recursive: true) is GUITextBlock nameTextBlock) + { + nameTextBlock.TextColor = new Color(nameTextBlock.TextColor, 1.0f); + } + if (subElement.FindChild("classtext", recursive: true) is GUITextBlock classTextBlock) { - Color newSubClassTextColor = new Color(classTextBlock.TextColor, 0.8f); classTextBlock.Text = TextManager.Get($"submarineclass.{newSub.SubmarineClass}"); - classTextBlock.TextColor = newSubClassTextColor; + classTextBlock.TextColor = new Color(classTextBlock.TextColor, 0.8f); + } + if (subElement.FindChild("pricetext", recursive: true) is GUITextBlock priceTextBlock) + { + priceTextBlock.Text = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", newSub.Price)); + priceTextBlock.TextColor = new Color(priceTextBlock.TextColor, 0.8f); } subElement.UserData = newSub; @@ -2507,14 +2625,22 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.FailedSelectedSub.Value.Name == newSub.Name && GameMain.NetLobbyScreen.FailedSelectedSub.Value.Hash == newSub.MD5Hash.StringRepresentation) { - GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, GameMain.NetLobbyScreen.SubList); + GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, SelectedSubType.Sub, GameMain.NetLobbyScreen.SubList); } if (GameMain.NetLobbyScreen.FailedSelectedShuttle.HasValue && GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Name == newSub.Name && GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Hash == newSub.MD5Hash.StringRepresentation) { - GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, GameMain.NetLobbyScreen.ShuttleList.ListBox); + GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, SelectedSubType.Shuttle, GameMain.NetLobbyScreen.ShuttleList.ListBox); + } + + if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.PvP && + GameMain.NetLobbyScreen.FailedSelectedEnemySub.HasValue && + GameMain.NetLobbyScreen.FailedSelectedEnemySub.Value.Name == newSub.Name && + GameMain.NetLobbyScreen.FailedSelectedEnemySub.Value.Hash == newSub.MD5Hash.StringRepresentation) + { + GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, SelectedSubType.EnemySub, GameMain.NetLobbyScreen.SubList); } NetLobbyScreen.FailedSubInfo failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation); @@ -2547,20 +2673,20 @@ namespace Barotrauma.Networking if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.CampaignID != campaignID) { string savePath = transfer.FilePath; - GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty); + GameMain.GameSession = new GameSession(null, Option.None, CampaignDataPath.CreateRegular(savePath), GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty); campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; GameMain.NetLobbyScreen.ToggleCampaignMode(true); } - GameMain.GameSession.SavePath = transfer.FilePath; + GameMain.GameSession.DataPath = CampaignDataPath.CreateRegular(transfer.FilePath); if (GameMain.GameSession.SubmarineInfo == null || campaign.Map == null) { string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDocRoot.GetAttributeString("submarine", "")) + ".sub"; GameMain.GameSession.SubmarineInfo = new SubmarineInfo(subPath, ""); } - campaign.LoadState(GameMain.GameSession.SavePath); + campaign.LoadState(GameMain.GameSession.DataPath.LoadPath); GameMain.GameSession?.SubmarineInfo?.Reload(); GameMain.GameSession?.SubmarineInfo?.CheckSubsLeftBehind(); @@ -2577,7 +2703,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.Select(); } - DebugConsole.Log("Campaign save received (" + GameMain.GameSession.SavePath + "), save ID " + campaign.LastSaveID); + DebugConsole.Log("Campaign save received (" + GameMain.GameSession.DataPath + "), save ID " + campaign.LastSaveID); //decrement campaign update IDs so the server will send us the latest data //(as there may have been campaign updates after the save file was created) foreach (MultiPlayerCampaign.NetFlags flag in Enum.GetValues(typeof(MultiPlayerCampaign.NetFlags))) @@ -2858,14 +2984,14 @@ namespace Barotrauma.Networking /// /// Tell the server to select a submarine (permission required) /// - public void RequestSelectSub(SubmarineInfo sub, bool isShuttle) + public void RequestSelectSub(SubmarineInfo sub, SelectedSubType type) { if (!HasPermission(ClientPermissions.SelectSub) || sub == null) { return; } IWriteMessage msg = new WriteOnlyMessage(); msg.WriteByte((byte)ClientPacketHeader.SERVER_COMMAND); - msg.WriteUInt16((UInt16)ClientPermissions.SelectSub); - msg.WriteBoolean(isShuttle); msg.WritePadBits(); + msg.WriteUInt16((ushort)ClientPermissions.SelectSub); + msg.WriteByte((byte)type); msg.WriteString(sub.MD5Hash.StringRepresentation); ClientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -2909,7 +3035,7 @@ namespace Barotrauma.Networking ClientPeer.Send(msg, DeliveryMethod.Reliable); } - public void SetupLoadCampaign(string saveName) + public void SetupLoadCampaign(string filePath, Option backupIndex) { if (ClientPeer == null) { return; } @@ -2920,7 +3046,19 @@ namespace Barotrauma.Networking msg.WriteByte((byte)ClientPacketHeader.CAMPAIGN_SETUP_INFO); msg.WriteBoolean(false); msg.WritePadBits(); - msg.WriteString(saveName); + msg.WriteString(filePath); + + if (backupIndex.TryUnwrap(out uint index)) + { + msg.WriteBoolean(true); + msg.WritePadBits(); + msg.WriteUInt32(index); + } + else + { + msg.WriteBoolean(false); + msg.WritePadBits(); + } ClientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -3062,7 +3200,8 @@ namespace Barotrauma.Networking public bool EnterChatMessage(GUITextBox textBox, string message) { - textBox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Default]; + var messageType = NetLobbyScreen.TeamChatSelected ? ChatMessageType.Team : ChatMessageType.Default; + textBox.TextColor = ChatMessage.MessageColor[(int)messageType]; if (string.IsNullOrWhiteSpace(message)) { @@ -3070,7 +3209,7 @@ namespace Barotrauma.Networking return false; } chatBox.ChatManager.Store(message); - SendChatMessage(message); + SendChatMessage(message, type: messageType); if (textBox.DeselectAfterMessage) { @@ -3559,9 +3698,9 @@ namespace Barotrauma.Networking { errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Info.Name); } - if (GameMain.NetworkMember?.RespawnManager?.RespawnShuttle != null) + if (GameMain.NetworkMember?.RespawnManager is { } respawnManager) { - errorLines.Add("Respawn shuttle: " + GameMain.NetworkMember.RespawnManager.RespawnShuttle.Info.Name); + errorLines.Add("Respawn shuttles: " + string.Join(", ", respawnManager.RespawnShuttles.Select(s => s.Info.Name))); } if (Level.Loaded != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index 5ecc23923..6a5889af2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -20,6 +20,8 @@ namespace Barotrauma.Networking public bool AllowModDownloads { get; private set; } = true; + public string AutomaticallyAttemptedPassword = string.Empty; + public readonly record struct Callbacks( Callbacks.MessageCallback OnMessageReceived, Callbacks.DisconnectCallback OnDisconnect, @@ -102,8 +104,9 @@ namespace Barotrauma.Networking TaskPool.Add($"{GetType().Name}.{nameof(GetAccountId)}", GetAccountId(), t => { - if (GameMain.Client?.ClientPeer is null) { return; } - + // FIXME what to do with this? + //if (GameMain.Client?.ClientPeer is null) { return; } + if (!t.TryGetResult(out Option accountId)) { Close(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed)); @@ -118,7 +121,7 @@ namespace Barotrauma.Networking var body = new ClientAuthTicketAndVersionPacket { - Name = GameMain.Client.Name, + Name = GameMain.Client?.Name ?? "Unknown", OwnerKey = ownerKey, AccountId = accountId, AuthTicket = authTicket, @@ -177,10 +180,16 @@ namespace Barotrauma.Networking var passwordPacket = INetSerializableStruct.Read(inc.Message); if (WaitingForPassword) { return; } - + passwordPacket.Salt.TryUnwrap(out passwordSalt); passwordPacket.RetriesLeft.TryUnwrap(out var retries); + if (!string.IsNullOrWhiteSpace(AutomaticallyAttemptedPassword)) + { + SendPassword(AutomaticallyAttemptedPassword); + return; + } + LocalizedString pwMsg = TextManager.Get("PasswordRequired"); passwordMsgBox?.Close(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 563c5ecd1..c0656420b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -224,10 +224,13 @@ namespace Barotrauma.Networking ToolBox.ThrowIfNull(netPeerConfiguration); #if DEBUG - netPeerConfiguration.SimulatedDuplicatesChance = GameMain.Client.SimulatedDuplicatesChance; - netPeerConfiguration.SimulatedMinimumLatency = GameMain.Client.SimulatedMinimumLatency; - netPeerConfiguration.SimulatedRandomLatency = GameMain.Client.SimulatedRandomLatency; - netPeerConfiguration.SimulatedLoss = GameMain.Client.SimulatedLoss; + if (GameMain.Client != null) + { + netPeerConfiguration.SimulatedDuplicatesChance = GameMain.Client.SimulatedDuplicatesChance; + netPeerConfiguration.SimulatedMinimumLatency = GameMain.Client.SimulatedMinimumLatency; + netPeerConfiguration.SimulatedRandomLatency = GameMain.Client.SimulatedRandomLatency; + netPeerConfiguration.SimulatedLoss = GameMain.Client.SimulatedLoss; + } #endif byte[] bufAux = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index e4f182e02..68a5e0acb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -22,6 +22,12 @@ namespace Barotrauma.Networking get; private set; } + public DateTime ReturnTime { get; private set; } + public DateTime RespawnTime { get; private set; } + public State CurrentState { get; private set; } + public bool ReturnCountdownStarted { get; private set; } + public bool RespawnCountdownStarted { get; private set; } + public static void ShowDeathPromptIfNeeded(float delay = 1.0f) { if (UseDeathPrompt) @@ -30,13 +36,18 @@ namespace Barotrauma.Networking } } - partial void UpdateTransportingProjSpecific(float deltaTime) + partial void UpdateTransportingProjSpecific(TeamSpecificState teamSpecificState, float deltaTime) { - if (GameMain.Client?.Character == null || GameMain.Client.Character.Submarine != RespawnShuttle) { return; } - if (!ReturnCountdownStarted) { return; } + if (GameMain.Client?.Character == null || + GameMain.Client.Character.Submarine is not { IsRespawnShuttle: true } || + GameMain.Client.Character.TeamID != teamSpecificState.TeamID) + { + return; + } + if (!teamSpecificState.ReturnCountdownStarted) { return; } //show a warning when there's 20 seconds until the shuttle leaves - if ((ReturnTime - DateTime.Now).TotalSeconds < 20.0f && + if ((teamSpecificState.ReturnTime - DateTime.Now).TotalSeconds < 20.0f && (DateTime.Now - lastShuttleLeavingWarningTime).TotalSeconds > 30.0f) { lastShuttleLeavingWarningTime = DateTime.Now; @@ -46,43 +57,56 @@ namespace Barotrauma.Networking public void ClientEventRead(IReadMessage msg, float sendingTime) { - bool respawnPromptPending = false; - var newState = (State)msg.ReadRangedInteger(0, Enum.GetNames(typeof(State)).Length); - switch (newState) + var myTeamId = (CharacterTeamType)msg.ReadByte(); + foreach (var teamSpecificState in teamSpecificStates.Values) { - case State.Transporting: - ReturnCountdownStarted = msg.ReadBoolean(); - maxTransportTime = msg.ReadSingle(); - float transportTimeLeft = msg.ReadSingle(); + var teamId = (CharacterTeamType)msg.ReadByte(); - ReturnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(transportTimeLeft * 1000.0f)); - RespawnCountdownStarted = false; - if (CurrentState != newState) - { - CoroutineManager.StopCoroutines("forcepos"); - } - break; - case State.Waiting: - PendingRespawnCount = msg.ReadUInt16(); - RequiredRespawnCount = msg.ReadUInt16(); - respawnPromptPending = msg.ReadBoolean(); - RespawnCountdownStarted = msg.ReadBoolean(); - ResetShuttle(); - float newRespawnTime = msg.ReadSingle(); - RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(newRespawnTime * 1000.0f)); - break; - case State.Returning: - RespawnCountdownStarted = false; - break; + bool respawnPromptPending = false; + var newState = (State)msg.ReadRangedInteger(0, Enum.GetNames(typeof(State)).Length); + switch (newState) + { + case State.Transporting: + teamSpecificState.ReturnCountdownStarted = msg.ReadBoolean(); + maxTransportTime = msg.ReadSingle(); + float transportTimeLeft = msg.ReadSingle(); + + teamSpecificState.ReturnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(transportTimeLeft * 1000.0f)); + teamSpecificState.RespawnCountdownStarted = false; + break; + case State.Waiting: + teamSpecificState.PendingRespawnCount = msg.ReadUInt16(); + teamSpecificState.RequiredRespawnCount = msg.ReadUInt16(); + respawnPromptPending = msg.ReadBoolean(); + teamSpecificState.RespawnCountdownStarted = msg.ReadBoolean(); + ResetShuttle(teamSpecificState); + float newRespawnTime = msg.ReadSingle(); + teamSpecificState.RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(newRespawnTime * 1000.0f)); + break; + case State.Returning: + teamSpecificState.RespawnCountdownStarted = false; + break; + } + teamSpecificState.CurrentState = newState; + + if (respawnPromptPending) + { + GameMain.Client.HasSpawned = true; + DeathPrompt.Create(delay: 1.0f); + } + + if (teamId == myTeamId) + { + PendingRespawnCount = teamSpecificState.PendingRespawnCount; + RequiredRespawnCount = teamSpecificState.RequiredRespawnCount; + ReturnTime = teamSpecificState.ReturnTime; + RespawnTime = teamSpecificState.RespawnTime; + CurrentState = teamSpecificState.CurrentState; + ReturnCountdownStarted = teamSpecificState.ReturnCountdownStarted; + RespawnCountdownStarted = teamSpecificState.RespawnCountdownStarted; + } } - CurrentState = newState; - - if (respawnPromptPending) - { - GameMain.Client.HasSpawned = true; - DeathPrompt.Create(delay: 1.0f); - } - + msg.ReadPadBits(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs index b2423ab37..9bfa06aaa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs @@ -144,9 +144,9 @@ namespace Barotrauma.Networking var pingLocation = NetPingLocation.TryParseFromString(pingLocationStr); - if (pingLocation.HasValue && Steamworks.SteamNetworkingUtils.LocalPingLocation.HasValue) + if (pingLocation.HasValue) { - int ping = Steamworks.SteamNetworkingUtils.LocalPingLocation.Value.EstimatePingTo(pingLocation.Value); + int ping = Steamworks.SteamNetworkingUtils.EstimatePingTo(pingLocation.Value); if (ping < 0) { return Result.Failure(SteamLobbyPingError.PingEstimationFailed); } return Result.Success(ping); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index b7252310c..082170271 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Linq; @@ -178,6 +179,11 @@ namespace Barotrauma.Networking extraCargoPanel.Visible = true; } } + + if (ReadPerks(incMsg)) + { + GameMain.NetLobbyScreen?.UpdateDisembarkPointListFromServerSettings(); + } } if (requiredFlags.HasFlag(NetFlags.HiddenSubs)) @@ -194,10 +200,41 @@ namespace Barotrauma.Networking } } + public static bool HasPermissionToChangePerks() + { + if (GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return true; } + + bool isPvP = GameMain.NetLobbyScreen?.SelectedMode == GameModePreset.PvP; + bool hasSelectedTeam = MultiplayerPreferences.Instance.TeamPreference is CharacterTeamType.Team1 or CharacterTeamType.Team2; + var otherClients = GameMain.Client?.ConnectedClients.Where(static c => c.SessionId != GameMain.Client.SessionId).ToImmutableArray() ?? ImmutableArray.Empty; + + if (isPvP) + { + if (!hasSelectedTeam) { return false; } + + return !otherClients + .Where(static c => c.PreferredTeam == MultiplayerPreferences.Instance.TeamPreference) + .Any(static c => c.HasPermission(Networking.ClientPermissions.ManageSettings)); + } + else + { + return !otherClients.Any(static c => c.HasPermission(Networking.ClientPermissions.ManageSettings)); + } + } + + public void ClientAdminWritePerks() + { + IWriteMessage outMsg = new WriteOnlyMessage(); + + outMsg.WriteByte((byte)ClientPacketHeader.SERVER_SETTINGS_PERKS); + WritePerks(outMsg); + GameMain.Client?.ClientPeer?.Send(outMsg, DeliveryMethod.Reliable); + } + public void ClientAdminWrite( NetFlags dataToSend, - int? missionTypeOr = null, - int? missionTypeAnd = null, + Identifier addedMissionType = default, + Identifier removedMissionType = default, int traitorDangerLevel = 0) { if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return; } @@ -220,7 +257,7 @@ namespace Barotrauma.Networking outMsg.WriteUInt32(count); foreach (KeyValuePair prop in changedProperties) { - DebugConsole.NewMessage(prop.Value.Name.Value, Color.Lime); + DebugConsole.NewMessage($"Changed {prop.Value.Name.Value} to {prop.Value.GUIComponentValue}", Color.Lime); outMsg.WriteUInt32(prop.Key); prop.Value.Write(outMsg, prop.Value.GUIComponentValue); } @@ -237,8 +274,8 @@ namespace Barotrauma.Networking if (dataToSend.HasFlag(NetFlags.Misc)) { - outMsg.WriteRangedInteger(missionTypeOr ?? (int)Barotrauma.MissionType.None, 0, (int)Barotrauma.MissionType.All); - outMsg.WriteRangedInteger(missionTypeAnd ?? (int)Barotrauma.MissionType.All, 0, (int)Barotrauma.MissionType.All); + outMsg.WriteIdentifier(addedMissionType); + outMsg.WriteIdentifier(removedMissionType); outMsg.WriteByte((byte)(traitorDangerLevel + 1)); outMsg.WritePadBits(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs index f2d99e79d..82bb14d2e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs @@ -418,8 +418,44 @@ namespace Barotrauma.Networking var randomizeLevelBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsRandomizeSeed")); AssignGUIComponent(nameof(RandomizeSeed), randomizeLevelBox); - //*********************************************** - + // ******* PVP ******************************** + NetLobbyScreen.CreateSubHeader("gamemode.pvp", listBox.Content); + + var teamSelectModeLabel = new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.05f), + listBox.Content.RectTransform), + TextManager.Get("TeamSelectionMode")); + teamSelectModeLabel.ToolTip = TextManager.Get("TeamSelectionMode.tooltip"); + var teamSelectionMode = new GUISelectionCarousel( + new RectTransform(new Vector2(0.5f, 0.6f), + teamSelectModeLabel.RectTransform, + Anchor.CenterRight)); + foreach (PvpTeamSelectionMode teamSelectionModeOption in Enum.GetValues(typeof(PvpTeamSelectionMode))) + { + var optionName = teamSelectionModeOption.ToString(); + teamSelectionMode.AddElement(teamSelectionModeOption, + TextManager.Get($"TeamSelectionMode.{optionName}"), + TextManager.Get($"TeamSelectionMode.{optionName}.tooltip")); + } + AssignGUIComponent(nameof(PvpTeamSelectionMode), teamSelectionMode); + + var autoBalanceThresholdLabel = new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.05f), + listBox.Content.RectTransform), + TextManager.Get("AutoBalanceThreshold")); + var autoBalanceThresholdTooltip = TextManager.Get("AutoBalanceThreshold.tooltip"); + autoBalanceThresholdLabel.ToolTip = autoBalanceThresholdTooltip; + var autoBalanceThreshold = new GUISelectionCarousel( + new RectTransform(new Vector2(0.5f, 0.6f), + autoBalanceThresholdLabel.RectTransform, + Anchor.CenterRight)); + autoBalanceThreshold.AddElement(0, TextManager.Get($"AutoBalanceThreshold.Off"), autoBalanceThresholdTooltip); + autoBalanceThreshold.AddElement(1, "1", autoBalanceThresholdTooltip); + autoBalanceThreshold.AddElement(2, "2", autoBalanceThresholdTooltip); + autoBalanceThreshold.AddElement(3, "3", autoBalanceThresholdTooltip); + AssignGUIComponent(nameof(PvpAutoBalanceThreshold), autoBalanceThreshold); + + // ******* GAMEPLAY *************************** NetLobbyScreen.CreateSubHeader("serversettingsroundstab", listBox.Content); var voiceChatEnabled = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), @@ -485,6 +521,9 @@ namespace Barotrauma.Networking AssignGUIComponent(nameof(NewCampaignDefaultSalary), defaultSalarySlider); defaultSalarySlider.OnMoved(defaultSalarySlider, defaultSalarySlider.BarScroll); + var pvpDisembarkPoints = NetLobbyScreen.CreateLabeledNumberInput(listBox.Content, "serversettingsdisembarkpoints", 0, 100, "serversettingsdisembarkpointstooltip"); + AssignGUIComponent(nameof(DisembarkPointAllowance), pvpDisembarkPoints); + //-------------------------------------------------------------------------------- // game settings //-------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index c3dbcca81..66b5544bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -320,7 +320,7 @@ namespace Barotrauma.Networking private Sound overrideSound; private int overridePos; - private short[] overrideBuf = new short[VoipConfig.BUFFER_SIZE]; + private readonly short[] overrideBuf = new short[VoipConfig.BUFFER_SIZE]; private void FillBuffer() { @@ -331,13 +331,13 @@ namespace Barotrauma.Networking { int sampleCount = overrideSound.FillStreamBuffer(overridePos, overrideBuf); overridePos += sampleCount * 2; - Array.Copy(overrideBuf, 0, uncompressedBuffer, totalSampleCount, sampleCount); + Array.Copy(overrideBuf, 0, uncompressedBuffer, totalSampleCount, Math.Min(sampleCount, uncompressedBuffer.Length - totalSampleCount)); totalSampleCount += sampleCount; if (sampleCount == 0) { overridePos = 0; - } + } } int sleepMs = VoipConfig.BUFFER_SIZE * 800 / VoipConfig.FREQUENCY; Thread.Sleep(sleepMs - 1); @@ -382,7 +382,14 @@ namespace Barotrauma.Networking } else { - overrideSound = GameMain.SoundManager.LoadSound(fileName, true); + try + { + overrideSound = GameMain.SoundManager.LoadSound(fileName, true); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to load the sound {fileName}.", e); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 9ca9c34f9..b8fc09f6b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -59,22 +59,77 @@ namespace Barotrauma switch (voteType) { case VoteType.Sub: - case VoteType.Mode: - GUIListBox listBox = (voteType == VoteType.Sub) ? - GameMain.NetLobbyScreen.SubList : GameMain.NetLobbyScreen.ModeList; + var subList = GameMain.NetLobbyScreen.SubList; - foreach (GUIComponent comp in listBox.Content.Children) + foreach (GUIComponent comp in subList.Content.Children) { - if (comp.FindChild("votes") is GUITextBlock voteText) { comp.RemoveChild(voteText); } + TryRemoveVoteText(comp); + + var container = comp.GetChild(); + var imageFrame = container.GetChild(); + var coalIcon = imageFrame.GetChildByUserData(NetLobbyScreen.CoalitionIconUserData); + var sepIcon = imageFrame.GetChildByUserData(NetLobbyScreen.SeparatistsIconUserData); + + coalIcon.Enabled = false; + sepIcon.Enabled = false; + + TryRemoveVoteText(coalIcon); + TryRemoveVoteText(sepIcon); + + static void TryRemoveVoteText(GUIComponent component) + { + if (component.FindChild("votes") is GUITextBlock foundText) + { + component.RemoveChild(foundText); + } + } } if (clients == null) { return; } - - IReadOnlyDictionary voteList = GetVoteCounts(voteType, clients); - foreach (KeyValuePair votable in voteList) + + bool isPvP = GameMain.NetLobbyScreen?.SelectedMode == GameModePreset.PvP; + + if (isPvP) { - SetVoteText(listBox, votable.Key, votable.Value); - } + var coalitionVoteList = GetVoteCounts(voteType, clients.Where(static c => c.PreferredTeam is CharacterTeamType.Team1)); + var separatistVoteList = GetVoteCounts(voteType, clients.Where(static c => c.PreferredTeam is CharacterTeamType.Team2)); + foreach (var (subInfo, amount) in coalitionVoteList) + { + SetSubVoteText(subList, subInfo, amount, CharacterTeamType.Team1); + } + + foreach (var (subInfo, amount) in separatistVoteList) + { + SetSubVoteText(subList, subInfo, amount, CharacterTeamType.Team2); + } + } + else + { + var subVoteList = GetVoteCounts(voteType, clients); + foreach (var (subInfo, amount) in subVoteList) + { + SetSubVoteText(subList, subInfo, amount, CharacterTeamType.None); + } + } + + break; + case VoteType.Mode: + var modeList = GameMain.NetLobbyScreen.ModeList; + foreach (GUIComponent comp in modeList.Content.Children) + { + if (comp.FindChild("votes") is GUITextBlock voteText) + { + comp.RemoveChild(voteText); + } + } + + if (clients == null) { return; } + + var modeVoteList = GetVoteCounts(voteType, clients); + foreach (var (preset, amount) in modeVoteList) + { + SetVoteText(modeList, preset, amount); + } break; case VoteType.StartRound: if (clients == null) { return; } @@ -90,12 +145,70 @@ namespace Barotrauma } } + private void SetSubVoteText(GUIListBox subListBox, SubmarineInfo userData, int votes, CharacterTeamType type) + { + GUIComponent subElement = subListBox.Content.GetChildByUserData(userData); + + if (subElement is null) + { + DebugConsole.ThrowError("Failed to find the submarine element in the listbox"); + return; + } + var (coalIcon, sepIcon) = GetPvPIcons(subElement); + + switch (type) + { + case CharacterTeamType.None: + { + SetVoteText(subListBox, userData, votes); + break; + } + case CharacterTeamType.Team1: + { + coalIcon.Enabled = votes > 0; + CreateSubmarineVoteText(coalIcon, votes); + break; + } + case CharacterTeamType.Team2: + { + sepIcon.Enabled = votes > 0; + CreateSubmarineVoteText(sepIcon, votes); + break; + } + default: + return; + } + + static void CreateSubmarineVoteText(GUIComponent parent, int votes) + { + if (parent is null) { return; } + var voteText = new GUITextBlock(new RectTransform(Vector2.One, parent.RectTransform, Anchor.TopLeft), $"{votes}", textAlignment: Alignment.Center) + { + Padding = Vector4.Zero, + UserData = "votes", + Shadow = true + }; + voteText.RectTransform.RelativeOffset = new Vector2(0.33f, 0.33f); + } + + static (GUIComponent CoalitionIcon, GUIComponent SeparatistsIcon) GetPvPIcons(GUIComponent child) + { + var container = child.GetChild(); + var imageFrame = container.GetChild(); + var coalIcon = imageFrame.GetChildByUserData(NetLobbyScreen.CoalitionIconUserData); + var sepIcon = imageFrame.GetChildByUserData(NetLobbyScreen.SeparatistsIconUserData); + + return (CoalitionIcon: coalIcon, SeparatistsIcon: sepIcon); + } + } + private void SetVoteText(GUIListBox listBox, object userData, int votes) { if (userData == null) { return; } foreach (GUIComponent comp in listBox.Content.Children) { if (comp.UserData != userData) { continue; } + if (comp.FindChild("votes") is not GUITextBlock voteText) { voteText = new GUITextBlock(new RectTransform(new Point(GUI.IntScale(30), comp.Rect.Height), comp.RectTransform, Anchor.CenterRight), @@ -197,28 +310,48 @@ namespace Barotrauma msg.WritePadBits(); return true; } - + public void ClientRead(IReadMessage inc) { GameMain.Client.ServerSettings.AllowSubVoting = inc.ReadBoolean(); if (GameMain.Client.ServerSettings.AllowSubVoting) { UpdateVoteTexts(null, VoteType.Sub); + bool isMultiSub = inc.ReadBoolean(); int votableCount = inc.ReadByte(); + + List serversubs = new List(); + if (GameMain.NetLobbyScreen?.SubList?.Content != null) + { + foreach (GUIComponent item in GameMain.NetLobbyScreen.SubList.Content.Children) + { + if (item.UserData is SubmarineInfo info) + { + serversubs.Add(info); + } + } + } + for (int i = 0; i < votableCount; i++) { int votes = inc.ReadByte(); string subName = inc.ReadString(); - List serversubs = new List(); - if (GameMain.NetLobbyScreen?.SubList?.Content != null) - { - foreach (GUIComponent item in GameMain.NetLobbyScreen.SubList.Content.Children) - { - if (item.UserData != null && item.UserData is SubmarineInfo) { serversubs.Add(item.UserData as SubmarineInfo); } - } - } + SubmarineInfo sub = serversubs.FirstOrDefault(s => s.Name == subName); - SetVoteText(GameMain.NetLobbyScreen.SubList, sub, votes); + SetSubVoteText(GameMain.NetLobbyScreen.SubList, sub, votes, isMultiSub ? CharacterTeamType.Team1 : CharacterTeamType.None); + } + + if (isMultiSub) + { + int separatistsCount = inc.ReadByte(); + for (int i = 0; i < separatistsCount; i++) + { + int votes = inc.ReadByte(); + string subName = inc.ReadString(); + + SubmarineInfo sub = serversubs.FirstOrDefault(s => s.Name == subName); + SetSubVoteText(GameMain.NetLobbyScreen.SubList, sub, votes, CharacterTeamType.Team2); + } } } GameMain.Client.ServerSettings.AllowModeVoting = inc.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index acea82954..76deb5a15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -72,7 +72,7 @@ namespace Barotrauma.Particles public float VelocityChangeMultiplier; - public bool DrawOnTop { get; private set; } + public ParticleDrawOrder DrawOrder { get; private set; } public ParticlePrefab.DrawTargetType DrawTarget { @@ -110,7 +110,7 @@ namespace Barotrauma.Particles { return debugName; } - public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) + public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, ParticleDrawOrder drawOrder = ParticleDrawOrder.Default, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { this.prefab = prefab; #if DEBUG @@ -205,7 +205,7 @@ namespace Barotrauma.Particles prevRotation = rotation; } - DrawOnTop = drawOnTop; + DrawOrder = drawOrder; this.collisionIgnoreTimer = collisionIgnoreTimer; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 2eac72fa2..3a0411e73 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -8,6 +8,7 @@ namespace Barotrauma.Particles { class ParticleEmitterProperties : ISerializableEntity { + private const float MinValue = int.MinValue, MaxValue = int.MaxValue; @@ -98,8 +99,8 @@ namespace Barotrauma.Particles [Editable, Serialize(1f, IsPropertySaveable.Yes)] public float LifeTimeMultiplier { get; set; } - [Editable, Serialize(false, IsPropertySaveable.Yes)] - public bool DrawOnTop { get; set; } + [Editable, Serialize(ParticleDrawOrder.Default, IsPropertySaveable.Yes)] + public ParticleDrawOrder DrawOrder { get; set; } [Serialize(0f, IsPropertySaveable.Yes)] public float Angle @@ -127,6 +128,12 @@ namespace Barotrauma.Particles public ParticleEmitterProperties(XElement element) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + + //backwards compatibility + if (element.GetAttributeBool("drawontop", false)) + { + DrawOrder = ParticleDrawOrder.Foreground; + } } } @@ -215,7 +222,9 @@ namespace Barotrauma.Particles position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); } - var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, particlePrefab.DrawOnTop || Prefab.DrawOnTop, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); + var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, + particlePrefab.DrawOrder != ParticleDrawOrder.Default ? particlePrefab.DrawOrder : Prefab.DrawOrder, + lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); if (particle != null) { @@ -286,7 +295,9 @@ namespace Barotrauma.Particles public readonly ContentPackage? ContentPackage; - public bool DrawOnTop => Properties.DrawOnTop || ParticlePrefab is { DrawOnTop: true }; + public ParticleDrawOrder DrawOrder => Properties.DrawOrder != ParticleDrawOrder.Default ? + Properties.DrawOrder : + (ParticlePrefab?.DrawOrder ?? ParticleDrawOrder.Default); public ParticleEmitterPrefab(ContentXElement element) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 457755dfa..adde46948 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -11,6 +11,13 @@ namespace Barotrauma.Particles AlphaBlend, Additive//, Distortion } + enum ParticleDrawOrder + { + Default, + Foreground, + Background + } + class ParticleManager { private const int MaxOutOfViewDist = 500; @@ -91,7 +98,7 @@ namespace Barotrauma.Particles return CreateParticle(prefab, position, velocity, rotation, hullGuess, collisionIgnoreTimer: collisionIgnoreTimer, tracerPoints:tracerPoints); } - public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) + public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, ParticleDrawOrder drawOrder = ParticleDrawOrder.Default, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { if (prefab == null || prefab.Sprites.Count == 0) { return null; } if (particleCount >= MaxParticles) @@ -134,7 +141,7 @@ namespace Barotrauma.Particles if (particles[particleCount] == null) { particles[particleCount] = new Particle(); } Particle particle = particles[particleCount]; - particle.Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); + particle.Init(prefab, position, velocity, rotation, hullGuess, drawOrder, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); particleCount++; particlesInCreationOrder.AddFirst(particle); @@ -213,7 +220,7 @@ namespace Barotrauma.Particles return activeParticles; } - public void Draw(SpriteBatch spriteBatch, bool inWater, bool? inSub, ParticleBlendState blendState) + public void Draw(SpriteBatch spriteBatch, bool inWater, bool? inSub, ParticleBlendState blendState, bool? background = false) { ParticlePrefab.DrawTargetType drawTarget = inWater ? ParticlePrefab.DrawTargetType.Water : ParticlePrefab.DrawTargetType.Air; @@ -225,12 +232,16 @@ namespace Barotrauma.Particles if (inSub.HasValue) { bool isOutside = particle.CurrentHull == null; - if (!particle.DrawOnTop && isOutside == inSub.Value) + if (particle.DrawOrder != ParticleDrawOrder.Foreground && isOutside == inSub.Value) { continue; } } - + if (background.HasValue) + { + bool isBackgroundParticle = particle.DrawOrder == ParticleDrawOrder.Background; + if (background.Value != isBackgroundParticle) { continue; } + } particle.Draw(spriteBatch); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index c120eca23..21d0f374d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -151,7 +151,7 @@ namespace Barotrauma.Particles public float Friction { get; private set; } [Editable(0.0f, 1.0f)] - [Serialize(0.5f, IsPropertySaveable.No, description: "How much of the particle's velocity is conserved when it collides with something, i.e. the \"bounciness\" of the particle. (1.0 = the particle stops completely).")] + [Serialize(0.5f, IsPropertySaveable.No, description: "How much of the particle's velocity is conserved when it collides with something, i.e. the \"bounciness\" of the particle. (0.0 = the particle stops completely).")] public float Restitution { get; private set; } //size ----------------------------------------- @@ -188,8 +188,8 @@ namespace Barotrauma.Particles [Editable, Serialize(DrawTargetType.Air, IsPropertySaveable.No, description: "Should the particle be rendered in air, water or both.")] public DrawTargetType DrawTarget { get; private set; } - [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the particle be always rendered on top of entities?")] - public bool DrawOnTop { get; private set; } + [Editable, Serialize(ParticleDrawOrder.Default, IsPropertySaveable.No, description: "Should the particle be always forced to render on top of entities or behind everything?")] + public ParticleDrawOrder DrawOrder { get; private set; } [Editable, Serialize(false, IsPropertySaveable.No, description: "Draw the particle even when it's calculated to be outside of view (the formula doesn't take scales into account). ")] public bool DrawAlways { get; private set; } @@ -226,6 +226,16 @@ namespace Barotrauma.Particles SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + //backwards compatibility + if (element.GetAttributeBool("drawontop", false)) + { + DrawOrder = ParticleDrawOrder.Foreground; + } + if (BlendState == ParticleBlendState.Additive && DrawOrder == ParticleDrawOrder.Background) + { + DebugConsole.AddWarning($"Error in particle prefab {Identifier}: additive particles cannot be rendered in the background."); + } + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index e8445be17..425740cad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -9,14 +9,6 @@ namespace Barotrauma { partial class PhysicsBody { - private float bodyShapeTextureScale; - - private Texture2D bodyShapeTexture; - public Texture2D BodyShapeTexture - { - get { return bodyShapeTexture; } - } - public void Draw(DeformableSprite deformSprite, Camera cam, Vector2 scale, Color color, bool invert = false) { if (!Enabled) { return; } @@ -79,78 +71,30 @@ namespace Barotrauma new Vector2(DrawPosition.X, -DrawPosition.Y), Color.Cyan, 0, 5); } - if (bodyShapeTexture == null && IsValidShape(Radius, Height, Width)) + if (IsValidShape(Radius, Height, Width)) { + float radius = ConvertUnits.ToDisplayUnits(Radius); + float height = ConvertUnits.ToDisplayUnits(Height); + float width = ConvertUnits.ToDisplayUnits(Width); + switch (BodyShape) { case Shape.Rectangle: - { - float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(Width), ConvertUnits.ToDisplayUnits(Height)); - if (maxSize > 128.0f) - { - bodyShapeTextureScale = 128.0f / maxSize; - } - else - { - bodyShapeTextureScale = 1.0f; - } - - bodyShapeTexture = GUI.CreateRectangle( - (int)ConvertUnits.ToDisplayUnits(Width * bodyShapeTextureScale), - (int)ConvertUnits.ToDisplayUnits(Height * bodyShapeTextureScale)); - break; - } + GUI.DrawRectangle(spriteBatch, DrawPosition.FlipY(), new Vector2(width, height), new Vector2(width, height) / 2, -DrawRotation, color); + break; case Shape.Capsule: + GUI.DrawCapsule(spriteBatch, DrawPosition.FlipY(), height, radius, -DrawRotation - MathHelper.PiOver2, color); + break; case Shape.HorizontalCapsule: - { - float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(Radius), ConvertUnits.ToDisplayUnits(Math.Max(Height, Width))); - if (maxSize > 128.0f) - { - bodyShapeTextureScale = 128.0f / maxSize; - } - else - { - bodyShapeTextureScale = 1.0f; - } - - bodyShapeTexture = GUI.CreateCapsule( - (int)ConvertUnits.ToDisplayUnits(Radius * bodyShapeTextureScale), - (int)ConvertUnits.ToDisplayUnits(Math.Max(Height, Width) * bodyShapeTextureScale)); - break; - } + GUI.DrawCapsule(spriteBatch, DrawPosition.FlipY(), width, radius, -DrawRotation, color); + break; case Shape.Circle: - if (ConvertUnits.ToDisplayUnits(Radius) > 128.0f) - { - bodyShapeTextureScale = 128.0f / ConvertUnits.ToDisplayUnits(Radius); - } - else - { - bodyShapeTextureScale = 1.0f; - } - bodyShapeTexture = GUI.CreateCircle((int)ConvertUnits.ToDisplayUnits(Radius * bodyShapeTextureScale)); + GUI.DrawDonutSection(spriteBatch, DrawPosition.FlipY(), new Range(radius - 0.5f, radius + 0.5f), MathHelper.TwoPi, color, 0, -DrawRotation); break; default: throw new NotImplementedException(); } } - - float rot = -DrawRotation; - if (bodyShape == Shape.HorizontalCapsule) - { - rot -= MathHelper.PiOver2; - } - - if (bodyShapeTexture != null) - { - spriteBatch.Draw( - bodyShapeTexture, - new Vector2(DrawPosition.X, -DrawPosition.Y), - null, - color, - rot, - new Vector2(bodyShapeTexture.Width / 2, bodyShapeTexture.Height / 2), - 1.0f / bodyShapeTextureScale, SpriteEffects.None, 0.0f); - } } public PosInfo ClientRead(IReadMessage msg, float sendingTime, string parentDebugName) @@ -210,14 +154,5 @@ namespace Barotrauma null : new PosInfo(newPosition, newRotation, newVelocity, newAngularVelocity, sendingTime); } - - partial void DisposeProjSpecific() - { - if (bodyShapeTexture != null) - { - bodyShapeTexture.Dispose(); - bodyShapeTexture = null; - } - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index b081a7449..366b3bc3a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -19,7 +20,9 @@ namespace Barotrauma protected GUIButton loadGameButton; public Action StartNewGame; - public Action LoadGame; + + public delegate void LoadGameDelegate(string loadPath, Option backupIndex); + public LoadGameDelegate LoadGame; protected enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 } protected CategoryFilter subFilter = CategoryFilter.All; @@ -821,5 +824,119 @@ namespace Barotrauma return true; } + + protected void CreateBackupMenu(IEnumerable indexData, Action loadBackup) + { + var backupPopup = new GUIMessageBox("", "", new[] { TextManager.Get("Load"), TextManager.Get("Cancel") }, new Vector2(0.3f, 0.5f), minSize: new Point(500, 500)); + + GUILayoutGroup campaignSettingContent = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.8f), backupPopup.Content.RectTransform, Anchor.TopCenter)); + + GUIListBox backupList = new GUIListBox(new RectTransform(Vector2.One, campaignSettingContent.RectTransform)); + + bool isIronman = GameMain.NetworkMember?.ServerSettings is { IronmanModeActive: true }; + + if (!indexData.Any() || isIronman) + { + LocalizedString errorMsg = isIronman + ? TextManager.Get("ironmanmodebackupdisclaimer") + : TextManager.Get("nobackups"); + var errorBlock = new GUITextBlock(new RectTransform(Vector2.One, campaignSettingContent.RectTransform), errorMsg, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center) + { + TextColor = GUIStyle.Red, + IgnoreLayoutGroups = true + }; + + if (errorBlock.Font.MeasureString(errorMsg).X > campaignSettingContent.Rect.Width) + { + errorBlock.Wrap = true; + errorBlock.SetTextPos(); + } + } + + if (!isIronman) + { + foreach (var data in indexData.OrderByDescending(static i => i.SaveTime)) + { + GUIFrame indexFrame = new GUIFrame( + new RectTransform(new Vector2(1.0f, 1f / SaveUtil.MaxBackupCount), backupList.Content.RectTransform), style: "ListBoxElement") + { + UserData = data + }; + + GUILayoutGroup indexLayout = new GUILayoutGroup( + new RectTransform(Vector2.One, indexFrame.RectTransform), + isHorizontal: true, + childAnchor: Anchor.CenterLeft) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + GUILayoutGroup leftLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.8f), indexLayout.RectTransform), childAnchor: Anchor.TopCenter) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + LocalizedString locationName = data.LocationType.IsEmpty || data.LocationNameIdentifier.IsEmpty ? + TextManager.Get("unknown") : + Location.GetName(data.LocationType, data.LocationNameFormatIndex, data.LocationNameIdentifier); + + var locationNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), leftLayout.RectTransform), locationName, textAlignment: Alignment.CenterLeft) + { + TextColor = Color.White + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), leftLayout.RectTransform), TextManager.Get($"savestate.{data.LevelType}"), textAlignment: Alignment.CenterLeft); + + + GUILayoutGroup rightLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.8f), indexLayout.RectTransform), childAnchor: Anchor.TopCenter) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + TimeSpan difference = SerializableDateTime.UtcNow - data.SaveTime; + + double totalMinutes = difference.TotalMinutes; + + LocalizedString timeFormat = totalMinutes switch + { + < 1 => TextManager.Get("subeditor.savedjustnow"), + > 60 => TextManager.GetWithVariable("saveagehours", "[hours]", ((int)Math.Floor(difference.TotalHours)).ToString()), + _ => TextManager.GetWithVariable("subeditor.saveageminutes", "[minutes]", difference.Minutes.ToString()) + }; + + new GUITextBlock(new RectTransform(Vector2.One, rightLayout.RectTransform), timeFormat, textAlignment: Alignment.CenterRight); + + locationNameBlock.Text = ToolBox.LimitString(locationName, locationNameBlock.Font, locationNameBlock.Rect.Width); + } + } + + backupList.AfterSelected = (selected, _) => + { + // to my understanding, there's no way to unselect an item in a GUIListBox + // so no need to check if selected is null + backupPopup.Buttons[0].Enabled = true; + return true; + }; + + backupPopup.Buttons[1].OnClicked += (button, o) => + { + backupPopup.Close(); + return true; + }; + + backupPopup.Buttons[0].Enabled = false; + backupPopup.Buttons[0].OnClicked += (button, o) => + { + if (backupList.SelectedComponent?.UserData is not SaveUtil.BackupIndexData selectedIndexData) { return false; } + + backupPopup.Close(); + + loadBackup?.Invoke(selectedIndexData); + return true; + }; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 4d153ba33..5818844bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -1,12 +1,16 @@ using Microsoft.Xna.Framework; using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.Linq; +using Barotrauma.Networking; namespace Barotrauma { class MultiPlayerCampaignSetupUI : CampaignSetupUI { + private GUIButton rollbackSaveButton; private GUIButton deleteMpSaveButton; private int prevInitialMoney; @@ -232,34 +236,114 @@ namespace Barotrauma { if (saveList.SelectedData is not CampaignMode.SaveInfo saveInfo) { return false; } if (string.IsNullOrWhiteSpace(saveInfo.FilePath)) { return false; } - LoadGame?.Invoke(saveInfo.FilePath); - + LoadGame?.Invoke(saveInfo.FilePath, backupIndex: Option.None); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); return true; }, Enabled = false }; - deleteMpSaveButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomLeft), - TextManager.Get("Delete"), style: "GUIButtonSmall") + + GUILayoutGroup leftButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 0.15f), loadGameContainer.RectTransform, Anchor.BottomLeft)) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + rollbackSaveButton = new GUIButton(new RectTransform(new Vector2(1f, 0.5f), leftButtonContainer.RectTransform), TextManager.Get("rollbackbutton"), style: "GUIButtonSmallFreeScale") + { + Visible = false, + ToolTip = TextManager.Get("backuptooltip"), + OnClicked = ViewBackupSaveMenu + }; + deleteMpSaveButton = new GUIButton(new RectTransform(new Vector2(1f, 0.5f), leftButtonContainer.RectTransform), + TextManager.Get("Delete"), style: "GUIButtonSmallFreeScale") { OnClicked = DeleteSave, Visible = false }; - } - - + } + + private bool ViewBackupSaveMenu(GUIButton button, object obj) + { + if (obj is not CampaignMode.SaveInfo saveInfo) { return false; } + if (string.IsNullOrWhiteSpace(saveInfo.FilePath)) { return false; } + + if (GameMain.Client.IsServerOwner) + { + CreateBackupMenu(SaveUtil.GetIndexData(saveInfo.FilePath), index => + { + LoadGame(saveInfo.FilePath, backupIndex: Option.Some(index.Index)); + }); + } + else + { + RequestBackupIndexData(saveInfo.FilePath); + } + return true; + } + + private const string PleaseWaitUserData = "PleaseWaitPopup"; + + private void RequestBackupIndexData(string savePath) + { + if (GameMain.Client == null) { return; } + + GUI.SetCursorWaiting(); + var msgBox = new GUIMessageBox(TextManager.Get("CampaignStartingPleaseWait"), TextManager.Get("CampaignStarting"), new[] { TextManager.Get("Cancel") }) + { + UserData = PleaseWaitUserData + }; + msgBox.Buttons[0].OnClicked = (btn, obj) => + { + GUI.ClearCursorWait(); + return true; + }; + + + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.REQUEST_BACKUP_INDICES); + msg.WriteString(savePath); + GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); + } + + public void OnBackupIndicesReceived(IReadMessage message) + { + GUI.ClearCursorWait(); + + foreach (GUIComponent component in GUIMessageBox.MessageBoxes.Where(static mb => mb.UserData is PleaseWaitUserData).ToArray()) + { + if (component is GUIMessageBox msgBox) + { + msgBox.Close(); + } + } + + string path = message.ReadString(); + var indexData = INetSerializableStruct.Read>(message); + CreateBackupMenu(indexData, selectedIndex => + { + LoadGame?.Invoke(path, backupIndex: Option.Some(selectedIndex.Index)); + }); + } + private bool SelectSaveFile(GUIComponent component, object obj) { if (obj is not CampaignMode.SaveInfo saveInfo) { return true; } string fileName = saveInfo.FilePath; loadGameButton.Enabled = true; + rollbackSaveButton.Visible = true; deleteMpSaveButton.Visible = deleteMpSaveButton.Enabled = GameMain.Client.IsServerOwner; - deleteMpSaveButton.Enabled = GameMain.GameSession?.SavePath != fileName; + rollbackSaveButton.Enabled = deleteMpSaveButton.Enabled = GameMain.GameSession?.DataPath.LoadPath != fileName; if (deleteMpSaveButton.Visible) { deleteMpSaveButton.UserData = saveInfo; } + + if (rollbackSaveButton.Visible) + { + rollbackSaveButton.UserData = saveInfo; + } return true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index f2706ca31..0fd7d2128 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -653,8 +653,7 @@ namespace Barotrauma { if (saveList.SelectedData is not CampaignMode.SaveInfo saveInfo) { return false; } if (string.IsNullOrWhiteSpace(saveInfo.FilePath)) { return false; } - LoadGame?.Invoke(saveInfo.FilePath); - + LoadGame?.Invoke(saveInfo.FilePath, backupIndex: Option.None); return true; }, Enabled = false @@ -685,6 +684,15 @@ namespace Barotrauma string mapseed = docRoot.GetAttributeString("mapseed", "unknown"); + Identifier locationNameIdentifier = docRoot.GetAttributeIdentifier("currentlocation", Identifier.Empty); + int locationNameFormatIndex = docRoot.GetAttributeInt("currentlocationnameformatindex", -1); + Identifier locationType = docRoot.GetAttributeIdentifier("locationtype", Identifier.Empty); + LevelData.LevelType levelType = docRoot.GetAttributeEnum("nextleveltype", LevelData.LevelType.LocationConnection); + + LocalizedString locationName = locationType.IsEmpty || locationNameIdentifier.IsEmpty ? + LocalizedString.EmptyString : + Location.GetName(locationType, locationNameFormatIndex, locationNameIdentifier); + var saveFileFrame = new GUIFrame( new RectTransform(new Vector2(0.45f, 0.6f), loadGameContainer.RectTransform, Anchor.TopRight) { @@ -708,6 +716,15 @@ namespace Barotrauma RelativeOffset = new Vector2(0, 0.1f) }); + if (!locationName.IsNullOrEmpty()) + { + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), + locationName, font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), + TextManager.Get($"savestate.{levelType}"), font: GUIStyle.SmallFont); + //spacing + new GUIFrame(new RectTransform(new Vector2(0.0f, 0.05f), layoutGroup.RectTransform), style: null); + } new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("Submarine")} : {subName}", font: GUIStyle.SmallFont); new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), @@ -715,15 +732,40 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUIStyle.SmallFont); - new GUIButton(new RectTransform(new Vector2(0.4f, 0.15f), saveFileFrame.RectTransform, Anchor.BottomCenter) + GUILayoutGroup buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.85f, 0.15f), saveFileFrame.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0, 0.1f) - }, TextManager.Get("Delete"), style: "GUIButtonSmall") + }, isHorizontal: true) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + new GUIButton(new RectTransform(new Vector2(0.5f, 1f), buttonContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("Delete"), style: "GUIButtonSmall") { UserData = saveInfo, OnClicked = DeleteSave }; + new GUIButton(new RectTransform(new Vector2(0.5f, 1f), buttonContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("rollbackbutton"), style: "GUIButtonSmall") + { + UserData = saveInfo, + ToolTip = TextManager.Get("backuptooltip"), + OnClicked = ViewBackupMenu + }; + + return true; + } + + private bool ViewBackupMenu(GUIButton btn, object obj) + { + if (obj is not CampaignMode.SaveInfo saveInfo) { return false; } + + var indexData = SaveUtil.GetIndexData(saveInfo.FilePath); + CreateBackupMenu(indexData, index => + { + LoadGame(saveInfo.FilePath, Option.Some(index.Index)); + }); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 426910cd8..8b390e1fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +using Barotrauma.Sounds; using FarseerPhysics; using FarseerPhysics.Dynamics; #if DEBUG @@ -115,7 +116,7 @@ namespace Barotrauma.CharacterEditor { base.Select(); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f, 0); + GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryWaterAmbience, 0.0f, 0); GUI.ForceMouseOn(null); if (Submarine.MainSub == null) @@ -236,7 +237,7 @@ namespace Barotrauma.CharacterEditor protected override void DeselectEditorSpecific() { SoundPlayer.OverrideMusicType = Identifier.Empty; - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameSettings.CurrentConfig.Audio.SoundVolume, 0); + GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryWaterAmbience, GameSettings.CurrentConfig.Audio.SoundVolume, 0); GUI.ForceMouseOn(null); if (isEndlessRunner) { @@ -746,6 +747,12 @@ namespace Barotrauma.CharacterEditor minorModesToggle?.UpdateOpenState((float)deltaTime, new Vector2(-minorModesPanel.Rect.Width - leftArea.RectTransform.AbsoluteOffset.X, 0), minorModesPanel.RectTransform); modesToggle?.UpdateOpenState((float)deltaTime, new Vector2(-modesPanel.Rect.Width - leftArea.RectTransform.AbsoluteOffset.X, 0), modesPanel.RectTransform); buttonsPanelToggle?.UpdateOpenState((float)deltaTime, new Vector2(-buttonsPanel.Rect.Width - leftArea.RectTransform.AbsoluteOffset.X, 0), buttonsPanel.RectTransform); + totalMassText.Text = GetTotalMassText(); + } + + private LocalizedString GetTotalMassText() + { + return TextManager.GetWithVariable($"{screenTextTag}totalmass", "[mass]", character?.AnimController?.Mass.FormatZeroDecimal() ?? "0"); } public CursorState GetMouseCursorState() @@ -1089,9 +1096,12 @@ namespace Barotrauma.CharacterEditor } else if (PlayerInput.PrimaryMouseButtonClicked()) { - jointStartLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => selectedLimbs.Contains(l) && !l.Hidden); - anchor1Pos = GetLimbSpritesheetRect(jointStartLimb).Center.ToVector2() - PlayerInput.MousePosition; - jointCreationMode = JointCreationMode.Create; + jointStartLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => selectedLimbs.Contains(l)); + if (jointStartLimb != null) + { + anchor1Pos = GetLimbSpritesheetRect(jointStartLimb).Center.ToVector2() - PlayerInput.MousePosition; + jointCreationMode = JointCreationMode.Create; + } } } else @@ -1110,8 +1120,11 @@ namespace Barotrauma.CharacterEditor else if (PlayerInput.PrimaryMouseButtonClicked()) { jointStartLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => selectedLimbs.Contains(l) && !l.Hidden); - anchor1Pos = ConvertUnits.ToDisplayUnits(jointStartLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition))); - jointCreationMode = JointCreationMode.Create; + if (jointStartLimb != null) + { + anchor1Pos = ConvertUnits.ToDisplayUnits(jointStartLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition))); + jointCreationMode = JointCreationMode.Create; + } } } } @@ -1494,10 +1507,10 @@ namespace Barotrauma.CharacterEditor { DebugConsole.NewMessage(GetCharacterEditorTranslation("TryingToSpawnCharacter").Replace("[config]", speciesName.ToString()), Color.HotPink); OnPreSpawn(); - bool dontFollowCursor = true; + bool followCursor = false; if (character != null) { - dontFollowCursor = character.dontFollowCursor; + followCursor = character.FollowCursor; RagdollParams.ClearHistory(); CurrentAnimation.ClearHistory(); if (!character.Removed) @@ -1510,7 +1523,7 @@ namespace Barotrauma.CharacterEditor { var characterInfo = new CharacterInfo(speciesName, jobOrJobPrefab: JobPrefab.Prefabs[selectedJob.Value]); character = Character.Create(speciesName, spawnPosition, ToolBox.RandomSeed(8), characterInfo, hasAi: false, ragdoll: ragdoll); - character.GiveJobItems(); + character.GiveJobItems(isPvPMode: false); HideWearables(); if (displayWearables) { @@ -1525,7 +1538,7 @@ namespace Barotrauma.CharacterEditor } if (character != null) { - character.dontFollowCursor = dontFollowCursor; + character.FollowCursor = followCursor; } if (character == null) { @@ -1693,8 +1706,8 @@ namespace Barotrauma.CharacterEditor } else { - config.SetAttributeValue("speciesname", name, StringComparison.OrdinalIgnoreCase); - config.SetAttributeValue("humanoid", isHumanoid, StringComparison.OrdinalIgnoreCase); + config.TrySetAttributeValue("speciesname", name); + config.TrySetAttributeValue("humanoid", isHumanoid); var ragdollElement = config.GetChildElement("ragdolls"); if (ragdollElement == null) { @@ -1845,6 +1858,7 @@ namespace Barotrauma.CharacterEditor private GUILayoutGroup rightArea, leftArea; private GUIFrame centerArea; + private GUITextBlock totalMassText; private GUIFrame characterSelectionPanel; private GUIFrame fileEditPanel; private GUIFrame modesPanel; @@ -1920,16 +1934,13 @@ namespace Barotrauma.CharacterEditor centerArea = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.95f), parent: Frame.RectTransform, anchor: Anchor.TopRight) { AbsoluteOffset = new Point((int)(rightArea.RectTransform.ScaledSize.X + rightArea.RectTransform.RelativeOffset.X * rightArea.RectTransform.Parent.ScaledSize.X + (int)(20 * GUI.xScale)), (int)(20 * GUI.yScale)) - }, style: null) { CanBeFocused = false }; leftArea = new GUILayoutGroup(new RectTransform(new Vector2(0.15f, 0.95f), parent: Frame.RectTransform, anchor: Anchor.CenterLeft), childAnchor: Anchor.BottomLeft) { RelativeSpacing = 0.02f }; - Vector2 toggleSize = new Vector2(1.0f, 0.03f); - CreateFileEditPanel(); CreateOptionsPanel(toggleSize); CreateCharacterSelectionPanel(); @@ -1941,12 +1952,11 @@ namespace Barotrauma.CharacterEditor optionsPanel.RectTransform.MinSize = new Point(0, (int)(optionsPanel.GetChild().RectTransform.Children.Sum(c => c.Rect.Height) / innerScale.Y)); rightArea.Recalculate(); } - CreateButtonsPanel(); CreateModesPanel(toggleSize); CreateMinorModesPanel(toggleSize); - CreateContextualControls(); + totalMassText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.01f), rightArea.RectTransform), GetTotalMassText()); } private void CreateMinorModesPanel(Vector2 toggleSize) @@ -2209,10 +2219,10 @@ namespace Barotrauma.CharacterEditor }; new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("FollowCursor")) { - Selected = !character.dontFollowCursor, + Selected = character.FollowCursor, OnSelected = box => { - character.dontFollowCursor = !box.Selected; + character.FollowCursor = box.Selected; return true; } }; @@ -2710,7 +2720,7 @@ namespace Barotrauma.CharacterEditor }, elementCount: 8, style: null); jobDropDown.ListBox.Color = new Color(jobDropDown.ListBox.Color.R, jobDropDown.ListBox.Color.G, jobDropDown.ListBox.Color.B, byte.MaxValue); jobDropDown.AddItem("None"); - JobPrefab.Prefabs.ForEach(j => jobDropDown.AddItem(j.Name, j.Identifier)); + JobPrefab.Prefabs.Where(j => !j.HiddenJob).ForEach(j => jobDropDown.AddItem(j.Name, j.Identifier)); jobDropDown.SelectItem(selectedJob); jobDropDown.OnSelected = (component, data) => { @@ -3112,7 +3122,7 @@ namespace Barotrauma.CharacterEditor { CharacterParams.Reset(true); AnimParams.ForEach(p => p.Reset(true)); - character.AnimController.ResetRagdoll(forceReload: true); + character.AnimController.ResetRagdoll(); RecreateRagdoll(); jointCreationMode = JointCreationMode.None; isDrawingLimb = false; @@ -3566,20 +3576,23 @@ namespace Barotrauma.CharacterEditor int offsetX = spriteSheetOffsetX; int offsetY = spriteSheetOffsetY; Rectangle rect = Rectangle.Empty; - for (int i = 0; i < Textures.Count; i++) + if (Textures != null) { - if (limb.ActiveSprite.FilePath != texturePaths[i]) + for (int i = 0; i < Textures.Count; i++) { - offsetY += (int)(Textures[i].Height * spriteSheetZoom); - } - else - { - rect = limb.ActiveSprite.SourceRect; - rect.Size = rect.MultiplySize(spriteSheetZoom); - rect.Location = rect.Location.Multiply(spriteSheetZoom); - rect.X += offsetX; - rect.Y += offsetY; - break; + if (limb.ActiveSprite.FilePath != texturePaths[i]) + { + offsetY += (int)(Textures[i].Height * spriteSheetZoom); + } + else + { + rect = limb.ActiveSprite.SourceRect; + rect.Size = rect.MultiplySize(spriteSheetZoom); + rect.Location = rect.Location.Multiply(spriteSheetZoom); + rect.X += offsetX; + rect.Y += offsetY; + break; + } } } return rect; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 5fb552402..d782ca3f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -300,7 +300,7 @@ namespace Barotrauma.CharacterEditor string destinationDir = Path.GetDirectoryName(destinationPath); if (!Directory.Exists(destinationDir)) { - Directory.CreateDirectory(destinationDir); + Directory.CreateDirectory(destinationDir, catchUnauthorizedAccessExceptions: true); } if (!File.Exists(destinationPath)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 138112575..ed6a1db23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -198,7 +198,7 @@ namespace Barotrauma msgBox.Close(); string path = Path.Combine(exportPath, $"{nameInput.Text}.xml"); - File.WriteAllText(path, save.ToString()); + File.WriteAllText(path, save.ToString(), catchUnauthorizedAccessExceptions: false); AskForConfirmation(TextManager.Get("EventEditor.OpenTextHeader"), TextManager.Get("EventEditor.OpenTextBody"), () => { ToolBox.OpenFileWithShell(path); @@ -942,7 +942,7 @@ namespace Barotrauma return false; } - GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); + GameSession gameSession = new GameSession(subInfo, Option.None, CampaignDataPath.Empty, GameModePreset.TestMode, CampaignSettings.Empty, null); TestGameMode gameMode = ((TestGameMode?)gameSession.GameMode) ?? throw new InvalidCastException(); gameMode.SpawnOutpost = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index c9e0d8f5b..77e765099 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -133,19 +133,7 @@ namespace Barotrauma if (Character.Controlled == null && !GUI.DisableHUD) { - for (int i = 0; i < Submarine.MainSubs.Length; i++) - { - if (Submarine.MainSubs[i] == null) continue; - if (Level.Loaded != null && Submarine.MainSubs[i].WorldPosition.Y < Level.MaxEntityDepth) { continue; } - - Vector2 position = Submarine.MainSubs[i].SubBody != null ? Submarine.MainSubs[i].WorldPosition : Submarine.MainSubs[i].HiddenSubPosition; - - Color indicatorColor = i == 0 ? Color.LightBlue * 0.5f : GUIStyle.Red * 0.5f; - GUI.DrawIndicator( - spriteBatch, position, cam, - Math.Max(Submarine.MainSub.Borders.Width, Submarine.MainSub.Borders.Height), - GUIStyle.SubmarineLocationIcon.Value.Sprite, indicatorColor); - } + DrawPositionIndicators(spriteBatch); } if (!GUI.DisableHUD) @@ -164,7 +152,118 @@ namespace Barotrauma GameMain.PerformanceCounter.AddElapsedTicks("Draw:HUD", sw.ElapsedTicks); sw.Restart(); } + + private void DrawPositionIndicators(SpriteBatch spriteBatch) + { + Sprite subLocationSprite = GUIStyle.SubLocationIcon.Value?.Sprite; + Sprite shuttleSprite = GUIStyle.ShuttleIcon.Value?.Sprite; + Sprite wreckSprite = GUIStyle.WreckIcon.Value?.Sprite; + Sprite caveSprite = GUIStyle.CaveIcon.Value?.Sprite; + Sprite outpostSprite = GUIStyle.OutpostIcon.Value?.Sprite; + Sprite ruinSprite = GUIStyle.RuinIcon.Value?.Sprite; + Sprite enemySprite = GUIStyle.EnemyIcon.Value?.Sprite; + Sprite corpseSprite = GUIStyle.CorpseIcon.Value?.Sprite; + Sprite beaconSprite = GUIStyle.BeaconIcon.Value?.Sprite; + + for (int i = 0; i < Submarine.MainSubs.Length; i++) + { + if (Submarine.MainSubs[i] == null) { continue; } + if (Level.Loaded != null && Submarine.MainSubs[i].WorldPosition.Y < Level.MaxEntityDepth) { continue; } + + Vector2 position = Submarine.MainSubs[i].SubBody != null ? Submarine.MainSubs[i].WorldPosition : Submarine.MainSubs[i].HiddenSubPosition; + + Color indicatorColor = i == 0 ? Color.LightBlue * 0.5f : GUIStyle.Red * 0.5f; + Sprite displaySprite = Submarine.MainSubs[i].Info.HasTag(SubmarineTag.Shuttle) ? shuttleSprite : subLocationSprite; + if (displaySprite != null) + { + GUI.DrawIndicator( + spriteBatch, position, cam, + Math.Max(Submarine.MainSubs[i].Borders.Width, Submarine.MainSubs[i].Borders.Height), + displaySprite, indicatorColor); + } + } + + if (!GameMain.DevMode) { return;} + + if (Level.Loaded != null) + { + foreach (Level.Cave cave in Level.Loaded.Caves) + { + Vector2 position = cave.StartPos.ToVector2(); + + Color indicatorColor = Color.Yellow * 0.5f; + if (caveSprite != null) + { + GUI.DrawIndicator( + spriteBatch, position, cam, hideDist: 3000f, + caveSprite, indicatorColor); + } + } + } + + foreach (Submarine submarine in Submarine.Loaded) + { + if (Submarine.MainSubs.Contains(submarine)) { continue; } + + Vector2 position = submarine.WorldPosition; + Color teamColorIndicator = submarine.TeamID switch + { + CharacterTeamType.Team1 => Color.LightBlue * 0.5f, + CharacterTeamType.Team2 => GUIStyle.Red * 0.5f, + CharacterTeamType.FriendlyNPC => GUIStyle.Yellow * 0.5f, + _ => Color.Green * 0.5f + }; + + Color indicatorColor = submarine.Info.Type switch + { + SubmarineType.Outpost => Color.LightGreen, + SubmarineType.Wreck => Color.SaddleBrown, + SubmarineType.BeaconStation => Color.Azure, + SubmarineType.Ruin => Color.Purple, + _ => teamColorIndicator + }; + + Sprite displaySprite = submarine.Info.Type switch + { + SubmarineType.Outpost => outpostSprite, + SubmarineType.Wreck => wreckSprite, + SubmarineType.BeaconStation => beaconSprite, + SubmarineType.Ruin => ruinSprite, + _ => subLocationSprite + }; + + // use a little dimmer color for transports + if (submarine.Info.SubmarineClass == SubmarineClass.Transport) { indicatorColor *= 0.75f; } + + if (displaySprite != null) + { + GUI.DrawIndicator( + spriteBatch, position, cam, hideDist: Math.Max(submarine.Borders.Width, submarine.Borders.Height), + displaySprite, indicatorColor); + } + } + + // markers for all enemies and corpses + foreach (Character character in Character.CharacterList) + { + Vector2 position = character.WorldPosition; + Color indicatorColor = Color.DarkRed * 0.5f; + if (character.IsDead) { indicatorColor = Color.DarkGray * 0.5f; } + + if (character.TeamID != CharacterTeamType.None) { continue;} + + Sprite displaySprite = character.IsDead ? corpseSprite : enemySprite; + + if (displaySprite != null) + { + GUI.DrawIndicator( + spriteBatch, position, cam, hideDist: 3000f, + displaySprite, indicatorColor); + } + } + } + public void DrawMap(GraphicsDevice graphics, SpriteBatch spriteBatch, double deltaTime) { foreach (Submarine sub in Submarine.Loaded) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 5b9394728..6c398a78d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Microsoft.Xna.Framework.Input; #if DEBUG using System.IO; using System.Xml; @@ -20,23 +21,26 @@ namespace Barotrauma { public override Camera Cam { get; } - private readonly GUIFrame leftPanel, rightPanel, bottomPanel, topPanel; + private GUIFrame leftPanel, rightPanel, bottomPanel, topPanel; private LevelGenerationParams selectedParams; + private RuinGenerationParams selectedRuinGenerationParams; + private OutpostGenerationParams selectedOutpostGenerationParams; private LevelObjectPrefab selectedLevelObject; - private readonly GUIListBox paramsList, ruinParamsList, caveParamsList, outpostParamsList, levelObjectList; - private readonly GUIListBox editorContainer; + private GUIListBox paramsList, ruinParamsList, caveParamsList, outpostParamsList, levelObjectList; + private GUIListBox editorContainer; - private readonly GUIButton spriteEditDoneButton; + private GUIButton spriteEditDoneButton; - private readonly GUITextBox seedBox; + private GUITextBox seedBox; - private readonly GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; + private GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; - private readonly GUIDropDown selectedSubDropDown; - private readonly GUIDropDown selectedBeaconStationDropdown; - private readonly GUIDropDown selectedWreckDropdown; + private GUIDropDown selectedSubDropDown; + private GUIDropDown selectedBeaconStationDropdown; + private GUIDropDown selectedWreckDropdown; + private GUINumberInput forceDifficultyInput; private Sprite editingSprite; @@ -45,15 +49,32 @@ namespace Barotrauma private readonly Color[] tunnelDebugColors = new Color[] { Color.White, Color.Cyan, Color.LightGreen, Color.Red, Color.LightYellow, Color.LightSeaGreen }; private LevelData currentLevelData; - - public LevelEditorScreen() + + private void RefreshUI(bool forceCreate = false) { - Cam = new Camera() + if (forceCreate) { - MinZoom = 0.01f, - MaxZoom = 1.0f - }; - + CreateUI(); + } + + GUI.PreventPauseMenuToggle = false; + pointerLightSource = new LightSource(Vector2.Zero, 1000.0f, Color.White, submarine: null); + GameMain.LightManager.AddLight(pointerLightSource); + topPanel.ClearChildren(); + new SerializableEntityEditor(topPanel.RectTransform, pointerLightSource.LightSourceParams, false, true); + + editingSprite = null; + UpdateParamsList(); + UpdateRuinParamsList(); + UpdateCaveParamsList(); + UpdateOutpostParamsList(); + UpdateLevelObjectsList(); + } + + private void CreateUI() + { + leftPanel?.ClearChildren(); + rightPanel?.ClearChildren(); leftPanel = new GUIFrame(new RectTransform(new Vector2(0.125f, 0.8f), Frame.RectTransform) { MinSize = new Point(150, 0) }); var paddedLeftPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), leftPanel.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f) }) { @@ -72,6 +93,7 @@ namespace Barotrauma editorContainer.ClearChildren(); SortLevelObjectsList(currentLevelData); new SerializableEntityEditor(editorContainer.Content.RectTransform, selectedParams, inGame: false, showName: true, elementHeight: 20, titleFont: GUIStyle.LargeFont); + forceDifficultyInput.FloatValue = (selectedParams.MinLevelDifficulty + selectedParams.MaxLevelDifficulty) / 2f; return true; }; @@ -83,7 +105,30 @@ namespace Barotrauma }; ruinParamsList.OnSelected += (GUIComponent component, object obj) => { - CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); + if (selectedRuinGenerationParams == obj) + { + // need to wait a frame before deselecting or the highlight on the list item gets left on + CoroutineManager.StartCoroutine(DeselectRuinParams()); + + IEnumerable DeselectRuinParams() + { + if (Screen.Selected != this) + { + yield break; + } + + yield return null; + selectedRuinGenerationParams = null; + CreateOutpostGenerationParamsEditor(null); + ruinParamsList.Deselect(); + } + } + else + { + selectedRuinGenerationParams = obj as RuinGenerationParams; + CreateOutpostGenerationParamsEditor(selectedRuinGenerationParams); + } + return true; }; @@ -108,7 +153,8 @@ namespace Barotrauma }; outpostParamsList.OnSelected += (GUIComponent component, object obj) => { - CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); + selectedOutpostGenerationParams = obj as OutpostGenerationParams; + CreateOutpostGenerationParamsEditor(selectedOutpostGenerationParams); return true; }; @@ -218,15 +264,31 @@ namespace Barotrauma selectedWreckDropdown.AddItem(wreck.DisplayName, userData: wreck); } wreckDropDownContainer.RectTransform.MinSize = new Point(0, selectedWreckDropdown.RectTransform.MinSize.Y); + + var forceDifficultyContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), forceDifficultyContainer.RectTransform), TextManager.Get("leveldifficulty")); + forceDifficultyInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1.0f), forceDifficultyContainer.RectTransform), NumberType.Float) + { + MinValueFloat = 0, + MaxValueFloat = 100, + FloatValue = Level.ForcedDifficulty ?? selectedParams?.MinLevelDifficulty ?? 0f, + OnValueChanged = (numberInput) => + { + if (Level.ForcedDifficulty == null) { return; } + Level.ForcedDifficulty = numberInput.FloatValue; + } + }; + forceDifficultyContainer.RectTransform.MinSize = new Point(0, forceDifficultyInput.RectTransform.MinSize.Y); + + var tickBoxContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), paddedRightPanel.RectTransform), isHorizontal: true); + mirrorLevel = new GUITickBox(new RectTransform(new Vector2(0.5f, 0.02f), tickBoxContainer.RectTransform), TextManager.Get("mirrorentityx")); - mirrorLevel = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), TextManager.Get("mirrorentityx")); - - allowInvalidOutpost = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.025f), paddedRightPanel.RectTransform), + allowInvalidOutpost = new GUITickBox(new RectTransform(new Vector2(0.5f, 0.025f), tickBoxContainer.RectTransform), TextManager.Get("leveleditor.allowinvalidoutpost")) { ToolTip = TextManager.Get("leveleditor.allowinvalidoutpost.tooltip") }; - + new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.generate")) { @@ -240,10 +302,11 @@ namespace Barotrauma Submarine.MainSub = new Submarine(subInfo); } GameMain.LightManager.ClearLights(); - currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); + currentLevelData = LevelData.CreateRandom(seedBox.Text, difficulty: forceDifficultyInput.FloatValue, generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; currentLevelData.ForceBeaconStation = selectedBeaconStationDropdown.SelectedData as SubmarineInfo; currentLevelData.ForceWreck = selectedWreckDropdown.SelectedData as SubmarineInfo; + currentLevelData.ForceRuinGenerationParams = selectedRuinGenerationParams; currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); @@ -272,7 +335,6 @@ namespace Barotrauma } }; - new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.test")) { @@ -300,7 +362,7 @@ namespace Barotrauma subInfo ??= SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && !nonPlayerFiles.Any(f => f.Path == s.FilePath)); - GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); + GameSession gameSession = new GameSession(subInfo, Option.None, CampaignDataPath.Empty, GameModePreset.TestMode, CampaignSettings.Empty, null); gameSession.StartRound(Level.Loaded.LevelData); (gameSession.GameMode as TestGameMode).OnRoundEnd = () => { @@ -350,6 +412,17 @@ namespace Barotrauma topPanel = new GUIFrame(new RectTransform(new Point(400, 100), GUI.Canvas) { RelativeOffset = new Vector2(leftPanel.RectTransform.RelativeSize.X * 2, 0.0f) }, style: "GUIFrameTop"); } + + public LevelEditorScreen() + { + Cam = new Camera() + { + MinZoom = 0.01f, + MaxZoom = 1.0f + }; + + RefreshUI(forceCreate: true); + } public void TestLevelGenerationForErrors(int amountOfLevelsToGenerate) { @@ -394,28 +467,13 @@ namespace Barotrauma yield return CoroutineStatus.Running; } } - - } - - - + public override void Select() { base.Select(); - - GUI.PreventPauseMenuToggle = false; - pointerLightSource = new LightSource(Vector2.Zero, 1000.0f, Color.White, submarine: null); - GameMain.LightManager.AddLight(pointerLightSource); - topPanel.ClearChildren(); - new SerializableEntityEditor(topPanel.RectTransform, pointerLightSource.LightSourceParams, false, true); - - editingSprite = null; - UpdateParamsList(); - UpdateRuinParamsList(); - UpdateCaveParamsList(); - UpdateOutpostParamsList(); - UpdateLevelObjectsList(); + + RefreshUI(forceCreate: false); } protected override void DeselectEditorSpecific() @@ -464,7 +522,7 @@ namespace Barotrauma foreach (RuinGenerationParams genParams in RuinGenerationParams.RuinParams.OrderBy(p => p.Identifier)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), ruinParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, - genParams.Name) + genParams.Identifier.Value) { Padding = Vector4.Zero, UserData = genParams @@ -556,6 +614,7 @@ namespace Barotrauma private void CreateOutpostGenerationParamsEditor(OutpostGenerationParams outpostGenerationParams) { editorContainer.ClearChildren(); + if (outpostGenerationParams == null) { return; } var outpostParamsEditor = new SerializableEntityEditor(editorContainer.Content.RectTransform, outpostGenerationParams, false, true, elementHeight: 20); // location type ------------------------- @@ -584,7 +643,7 @@ namespace Barotrauma locationTypeDropDown.SelectItem("any"); } - locationTypeDropDown.OnSelected += (_, __) => + locationTypeDropDown.AfterSelected += (_, __) => { outpostGenerationParams.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); @@ -596,29 +655,29 @@ namespace Barotrauma // module count ------------------------- - var moduleLabel = new GUITextBlock(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(70 * GUI.Scale))), TextManager.Get("submarinetype.outpostmodules"), font: GUIStyle.SubHeadingFont); - outpostParamsEditor.AddCustomContent(moduleLabel, 100); - foreach (var moduleCount in outpostGenerationParams.ModuleCounts) { - var moduleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(25 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Identifier.Value), textAlignment: Alignment.CenterLeft); - new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), NumberType.Int) + var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, moduleCount, inGame: false, showName: true, elementHeight: 20, titleFont: GUIStyle.Font); + foreach (var componentList in editor.Fields.Values) { - MinValueInt = 0, - MaxValueInt = 100, - IntValue = moduleCount.Count, - OnValueChanged = (numInput) => + foreach (var component in componentList) { - outpostGenerationParams.SetModuleCount(moduleCount.Identifier, numInput.IntValue); - if (numInput.IntValue == 0) + if (component is GUINumberInput numberInput) { - outpostParamsList.Select(outpostParamsList.SelectedData); + numberInput.OnValueChanged += (numInput) => + { + if (moduleCount.Count == 0) + { + //refresh to remove this module count from the editor + outpostParamsList.Select(outpostParamsList.SelectedData); + } + }; } } - }; - moduleCountGroup.RectTransform.MinSize = new Point(moduleCountGroup.Rect.Width, moduleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - outpostParamsEditor.AddCustomContent(moduleCountGroup, 100); + } + editor.RectTransform.MaxSize = new Point(int.MaxValue, editor.Rect.Height); + outpostParamsEditor.AddCustomContent(editor, 100); + editor.Recalculate(); } // add module count ------------------------- @@ -648,7 +707,7 @@ namespace Barotrauma }; addModuleCountGroup.RectTransform.MinSize = new Point(addModuleCountGroup.Rect.Width, addModuleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); outpostParamsEditor.AddCustomContent(addModuleCountGroup, 100); - + outpostParamsEditor.Recalculate(); } private void CreateLevelObjectEditor(LevelObjectPrefab levelObjectPrefab) @@ -736,7 +795,7 @@ namespace Barotrauma dropdown.AddItem(objPrefab.Name, objPrefab); if (childObj.AllowedNames.Contains(objPrefab.Name)) { dropdown.SelectItem(objPrefab); } } - dropdown.OnSelected = (selected, obj) => + dropdown.AfterSelected = (selected, obj) => { childObj.AllowedNames = dropdown.SelectedDataMultiple.Select(d => ((LevelObjectPrefab)d).Name).ToList(); return true; @@ -965,11 +1024,17 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); if (Level.Loaded != null) { - float crushDepthScreen = Cam.WorldToScreen(new Vector2(0.0f, -Level.Loaded.CrushDepth)).Y; - if (crushDepthScreen > 0.0f && crushDepthScreen < GameMain.GraphicsHeight) + float hullUpgradePrcIncrease = UpgradePrefab.CrushDepthUpgradePrc / 100f; + for (int upgradeLevel = 0; upgradeLevel <= UpgradePrefab.IncreaseWallHealthMaxLevel; upgradeLevel++) { - GUI.DrawLine(spriteBatch, new Vector2(0, crushDepthScreen), new Vector2(GameMain.GraphicsWidth, crushDepthScreen), GUIStyle.Red * 0.25f, width: 5); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, crushDepthScreen), "Crush depth", GUIStyle.Red, backgroundColor: Color.Black); + float upgradeLevelCrushDepth = Level.DefaultRealWorldCrushDepth + (Level.DefaultRealWorldCrushDepth * upgradeLevel * hullUpgradePrcIncrease); + float subCrushDepth = (upgradeLevelCrushDepth / Physics.DisplayToRealWorldRatio) - Level.Loaded.LevelData.InitialDepth; + string labelText = $"Crush depth (upgrade level {upgradeLevel})"; + if (upgradeLevel == 0) + { + labelText = $"Crush depth (no upgrade)"; + } + DrawCrushDepth(subCrushDepth, labelText, Color.Red); } float abyssStartScreen = Cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Bottom)).Y; @@ -987,9 +1052,18 @@ namespace Barotrauma } GUI.Draw(Cam, spriteBatch); spriteBatch.End(); + + void DrawCrushDepth(float crushDepth, string labelText, Color color) + { + float crushDepthScreen = Cam.WorldToScreen(new Vector2(0.0f, -crushDepth)).Y; + if (crushDepthScreen > 0.0f && crushDepthScreen < GameMain.GraphicsHeight) + { + GUI.DrawLine(spriteBatch, new Vector2(0, crushDepthScreen), new Vector2(GameMain.GraphicsWidth, crushDepthScreen), color * 0.25f, width: 5); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, crushDepthScreen), labelText, GUIStyle.Red, backgroundColor: Color.Black); + } + } } - public override void Update(double deltaTime) { if (lightingEnabled.Selected) @@ -1016,6 +1090,19 @@ namespace Barotrauma { GameMain.SpriteEditorScreen.Update(deltaTime); } + + // in case forced difficulty was changed by console command or such + if (Level.ForcedDifficulty != null && MathHelper.Distance((float)Level.ForcedDifficulty, forceDifficultyInput.FloatValue) > 0.001f) + { + forceDifficultyInput.FloatValue = (float)Level.ForcedDifficulty; + } + +#if DEBUG + if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.R)) + { + RefreshUI(forceCreate: true); + } +#endif } private void SerializeAll() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs index 5a62f17fc..9445f3f12 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs @@ -906,6 +906,7 @@ namespace Barotrauma } var gamesession = new GameSession( selectedSub, + Option.None, GameModePreset.DevSandbox, missionPrefabs: null); //(gamesession.GameMode as SinglePlayerCampaign).GenerateMap(ToolBox.RandomSeed(8)); @@ -1251,12 +1252,12 @@ namespace Barotrauma if (!Directory.Exists(SaveUtil.TempPath)) { - Directory.CreateDirectory(SaveUtil.TempPath); + Directory.CreateDirectory(SaveUtil.TempPath, catchUnauthorizedAccessExceptions: true); } try { - File.Copy(selectedSub.FilePath, Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub"), true); + File.Copy(selectedSub.FilePath, Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub"), overwrite: true, catchUnauthorizedAccessExceptions: false); } catch (System.IO.IOException e) { @@ -1270,7 +1271,7 @@ namespace Barotrauma selectedSub = new SubmarineInfo(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub")); - GameMain.GameSession = new GameSession(selectedSub, savePath, GameModePreset.SinglePlayerCampaign, settings, mapSeed); + GameMain.GameSession = new GameSession(selectedSub, Option.None, CampaignDataPath.CreateRegular(savePath), GameModePreset.SinglePlayerCampaign, settings, mapSeed); GameMain.GameSession.CrewManager.ClearCharacterInfos(); foreach (var characterInfo in campaignSetupUI.CharacterMenus.Select(m => m.CharacterInfo)) { @@ -1279,17 +1280,22 @@ namespace Barotrauma ((SinglePlayerCampaign)GameMain.GameSession.GameMode).LoadNewLevel(); } - private void LoadGame(string saveFile) + private void LoadGame(string path, Option backupIndex) { - if (string.IsNullOrWhiteSpace(saveFile)) return; + if (string.IsNullOrWhiteSpace(path)) return; try { - SaveUtil.LoadGame(saveFile); + CampaignDataPath dataPath = + backupIndex.TryUnwrap(out uint index) + ? new CampaignDataPath(loadPath: SaveUtil.GetBackupPath(path, index), path) + : CampaignDataPath.CreateRegular(path); + + SaveUtil.LoadGame(dataPath); } catch (Exception e) { - DebugConsole.ThrowError("Loading save \"" + saveFile + "\" failed", e); + DebugConsole.ThrowError("Loading save \"" + path + "\" failed", e); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index ba7fc25c6..80a46d7ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1,11 +1,11 @@ using Barotrauma.Extensions; using Barotrauma.Networking; -using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; @@ -14,13 +14,17 @@ namespace Barotrauma partial class NetLobbyScreen : Screen { private GUIListBox chatBox; + private GUILayoutGroup chatRow; private GUIButton serverLogReverseButton; private GUIListBox serverLogBox, serverLogFilterTicks; private GUIComponent jobVariantTooltip; private GUIComponent playStyleIconContainer; - + + private GUIDropDown chatSelector; + public static bool TeamChatSelected = false; + private GUITextBox chatInput; private GUITextBox serverLogFilter; public GUITextBox ChatInput @@ -39,6 +43,9 @@ namespace Barotrauma private GUIScrollBar traitorProbabilitySlider; private GUILayoutGroup traitorDangerGroup; + private GUIDropDown outpostDropdown; + private bool outpostDropdownUpToDate; + public GUIFrame MissionTypeFrame { get; private set; } public GUIFrame CampaignSetupFrame { get; private set; } public GUIFrame CampaignFrame { get; private set; } @@ -47,7 +54,7 @@ namespace Barotrauma private GUITickBox[] missionTypeTickBoxes; private GUIListBox missionTypeList; - + public GUITextBox LevelSeedBox { get; private set; } private GUIButton joinOnGoingRoundButton; @@ -99,6 +106,10 @@ namespace Barotrauma private readonly List permadeathDisabledRespawnSettings = new List(); private readonly List ironmanDisabledRespawnSettings = new List(); private readonly List campaignDisabledElements = new List(); + private readonly List campaignHiddenElements = new List(); + private readonly List pvpOnlyElements = new(); + private readonly List disembarkPerkSettings = new(); + private readonly List respawnSettings = new(); public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu { get; set; } public GUIFrame JobSelectionFrame { get; private set; } @@ -184,16 +195,28 @@ namespace Barotrauma ?? Array.Empty(); public GUIListBox PlayerList; + + public int Team1Count; + public int Team2Count; public GUITextBox CharacterNameBox { get; private set; } public GUIListBox TeamPreferenceListBox { get; private set; } + private GUITextBlock pvpTeamChoiceTeam1; + private GUITextBlock pvpTeamChoiceMiddleButton; + private GUITextBlock pvpTeamChoiceTeam2; + + private CharacterTeamType TeamPreference => SelectedMode == GameModePreset.PvP ? MultiplayerPreferences.Instance.TeamPreference : CharacterTeamType.Team1; public GUIButton StartButton { get; private set; } public GUITickBox ReadyToStartBox { get; private set; } - public SubmarineInfo SelectedSub => SubList.SelectedData as SubmarineInfo; + [AllowNull, MaybeNull] + public SubmarineInfo SelectedSub; + + [AllowNull, MaybeNull] + public SubmarineInfo SelectedEnemySub; public SubmarineInfo SelectedShuttle => ShuttleList.SelectedData as SubmarineInfo; @@ -210,34 +233,27 @@ namespace Barotrauma get { return ModeList.SelectedData as GameModePreset; } } - public MissionType MissionType + public IEnumerable MissionTypes { get { - MissionType retVal = MissionType.None; - int index = 0; - foreach (MissionType type in Enum.GetValues(typeof(MissionType))) - { - if (type == MissionType.None || type == MissionType.All) { continue; } - - if (missionTypeTickBoxes[index].Selected) - { - retVal = (MissionType)((int)retVal | (int)type); - } - - index++; - } - - return retVal; + return missionTypeTickBoxes.Where(t => t.Selected).Select(t => (Identifier)t.UserData); } set { - int index = 0; - foreach (MissionType type in Enum.GetValues(typeof(MissionType))) + bool changed = false; + foreach (var missionTypeTickBox in missionTypeTickBoxes) { - if (type == MissionType.None || type == MissionType.All) { continue; } - missionTypeTickBoxes[index].Selected = ((int)type & (int)value) != 0; - index++; + bool prevSelected = missionTypeTickBox.Selected; + missionTypeTickBox.Selected = value.Contains((Identifier)missionTypeTickBox.UserData); + if (prevSelected != missionTypeTickBox.Selected) + { + changed = true; + } + } + if (changed) + { + RefreshOutpostDropdown(); } } } @@ -710,69 +726,224 @@ namespace Barotrauma }; clientDisabledElements.Add(missionTypeList); - var missionTypes = (MissionType[])Enum.GetValues(typeof(MissionType)); - missionTypeTickBoxes = new GUITickBox[missionTypes.Length - 2]; - int index = 0; - for (int i = 0; i < missionTypes.Length; i++) - { - var missionType = missionTypes[i]; - if (missionType == MissionType.None || missionType == MissionType.All) { continue; } + List missionTypes = MissionPrefab.GetAllMultiplayerSelectableMissionTypes().ToList(); + missionTypeTickBoxes = new GUITickBox[missionTypes.Count]; + int index = 0; + foreach (var missionType in missionTypes.OrderBy(t => TextManager.Get("MissionType." + t.Value).Value)) + { GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), missionTypeList.Content.RectTransform) { MinSize = new Point(0, GUI.IntScale(30)) }, style: null) { UserData = missionType, }; - if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) - { - missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), string.Empty) - { - UserData = (int)missionType, - Visible = false, - CanBeFocused = false - }; - } - else - { - missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), + missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), TextManager.Get("MissionType." + missionType.ToString())) + { + UserData = missionType, + ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString()), + OnSelected = (tickbox) => { - UserData = (int)missionType, - ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString()), - OnSelected = (tickbox) => + RefreshOutpostDropdown(); + if (tickbox.Selected) { - int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; - int missionTypeAnd = (int)MissionType.All & (!tickbox.Selected ? (~(int)tickbox.UserData) : (int)MissionType.All); - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); - return true; + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, addedMissionType: (Identifier)tickbox.UserData); } - }; - frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; - } + else + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, removedMissionType: (Identifier)tickbox.UserData); + } + return true; + } + }; + frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; index++; } clientDisabledElements.AddRange(missionTypeTickBoxes); return gameModeSpecificFrame; } + + private GUIFrame gameModeSettingsContent; + private GUILayoutGroup gameModeSettingsLayout; private GUIComponent CreateGameModeSettingsPanel(GUIComponent parent) { //------------------------------------------------------------------ // settings panel //------------------------------------------------------------------ - - GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) + + gameModeSettingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) { Stretch = true }; - CreateSubHeader("GameModeSettings", settingsLayout); + CreateSubHeader("GameModeSettings", gameModeSettingsLayout); - var settingsContent = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)).Content; + gameModeSettingsContent = new GUIListBox(new RectTransform(Vector2.One, gameModeSettingsLayout.RectTransform)).Content; + + var winScoreHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), gameModeSettingsContent.RectTransform), TextManager.Get("ServerSettingsWinScorePvP")) + { + CanBeFocused = false + }; + clientDisabledElements.Add(winScoreHeader); + pvpOnlyElements.Add(winScoreHeader); + + var winScoreContainer = CreateLabeledSlider(gameModeSettingsContent, headerTag: string.Empty, valueLabelTag: string.Empty, tooltipTag: "ServerSettingsWinScorePvPTooltip", + out var winScorePvPSlider, out var winScorePvPSliderLabel); + winScorePvPSlider.Range = new Vector2(1, 1000); + winScorePvPSlider.StepValue = 1; + winScorePvPSlider.OnMoved = (scrollBar, _) => + { + if (scrollBar.UserData is not GUITextBlock text) { return false; } + text.Text = TextManager.GetWithVariable("ServerSettingsWinScoreValuePvP", "[value]", ((int)Math.Round(scrollBar.BarScrollValue, digits: 0)).ToString()); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + + AssignComponentToServerSetting(winScorePvPSlider, nameof(ServerSettings.WinScorePvP)); + winScorePvPSlider.OnMoved(winScorePvPSlider, winScorePvPSlider.BarScroll); + clientDisabledElements.AddRange(winScoreContainer.GetAllChildren()); + pvpOnlyElements.Add(winScoreContainer); + + //(pvp) stun resistance ------------------------------------------------- + var sliderContainer = CreateLabeledSlider(gameModeSettingsContent, headerTag: string.Empty, valueLabelTag: "gamemodesettings.stunresistance", tooltipTag: "gamemodesettings.stunresistancetooltip", + out var slider, out var sliderLabel); + LocalizedString stunResistLabel = sliderLabel.Text; + slider.Step = 0.1f; + slider.Range = new Vector2(0.0f, 1.0f); + slider.OnReleased = (scrollbar, value) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + ((GUITextBlock)scrollBar.UserData).Text = stunResistLabel.Replace("[percentage]", ((int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f)).ToString()); + return true; + }; + AssignComponentToServerSetting(slider, nameof(ServerSettings.PvPStunResist)); + slider.OnMoved(slider, slider.BarScroll); + clientDisabledElements.AddRange(sliderContainer.GetAllChildren()); + pvpOnlyElements.Add(sliderContainer); + + //(pvp) mark enemy location toggle -------------------------------------- + var markApproximateEnemyLocationToggle = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.06f), gameModeSettingsContent.RectTransform), + TextManager.Get("ServerSettingsTrackOpponentInPvP")) + { + ToolTip = TextManager.Get("gamemodesettings.markenemylocationtooltip"), + Selected = GameMain.Client != null && GameMain.Client.ServerSettings.TrackOpponentInPvP, + OnSelected = (tt) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + } + }; + AssignComponentToServerSetting(markApproximateEnemyLocationToggle, nameof(ServerSettings.TrackOpponentInPvP)); + clientDisabledElements.Add(markApproximateEnemyLocationToggle); + pvpOnlyElements.Add(markApproximateEnemyLocationToggle); + + //make the header use the height of the tickboxes to get the layout to be a little more uniform + winScoreHeader.RectTransform.MinSize = new Point(0, markApproximateEnemyLocationToggle.RectTransform.MinSize.Y); + + //(pvp) spawn monsters tickbox ----------------------------------------- + var spawnMonstersTickbox = new GUITickBox(new RectTransform(Vector2.One, gameModeSettingsContent.RectTransform), TextManager.Get("gamemodesettings.spawnmonsters")) + { + ToolTip = TextManager.Get("gamemodesettings.spawnmonsterstooltip"), + Selected = GameMain.Client != null && GameMain.Client.ServerSettings.PvPSpawnMonsters, + OnSelected = (GUITickBox box) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + } + }; + AssignComponentToServerSetting(spawnMonstersTickbox, nameof(ServerSettings.PvPSpawnMonsters)); + clientDisabledElements.Add(spawnMonstersTickbox); + pvpOnlyElements.Add(spawnMonstersTickbox); + + //(pvp) spawn wrecks tickbox ------------------------------------------- + var spawnWrecksTickbox = new GUITickBox(new RectTransform(Vector2.One, gameModeSettingsContent.RectTransform), TextManager.Get("gamemodesettings.spawnwrecks")) + { + ToolTip = TextManager.Get("gamemodesettings.spawnwreckstooltip"), + Selected = GameMain.Client != null && GameMain.Client.ServerSettings.PvPSpawnWrecks, + OnSelected = (GUITickBox box) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + } + }; + AssignComponentToServerSetting(spawnWrecksTickbox, nameof(ServerSettings.PvPSpawnWrecks)); + clientDisabledElements.Add(spawnWrecksTickbox); + pvpOnlyElements.Add(spawnWrecksTickbox); + + // outpost ----------------------------------------------------------------------------- + GUILayoutGroup outpostHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), gameModeSettingsContent.RectTransform), isHorizontal: true) + { + Visible = false, + Stretch = true + }; + var outpostLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), outpostHolder.RectTransform), TextManager.Get("gamemodesettings.outpost"), wrap: true); + outpostDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), outpostHolder.RectTransform), elementCount: 6, listBoxScale: 2.0f) + { + ToolTip = TextManager.Get("gamemodesettings.outposttooltip"), + AfterSelected = (component, obj) => + { + //don't register selecting the outpost until we've refreshed the available outposts, + //otherwise a client may request selecting "nothing" just because there's nothing in the list yet + if (outpostDropdownUpToDate && obj != null) + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + } + return true; + } + }; + outpostDropdown.ListBox.RectTransform.SetPosition(Anchor.BottomLeft, Pivot.TopLeft); + //do this before adding the contents, otherwise they get disabled too (and we just want to disable the dropdown itself) + clientDisabledElements.AddRange(outpostHolder.GetAllChildren()); + outpostDropdown.AddItem(TextManager.Get("random"), "Random".ToIdentifier()); + foreach (var submarineInfo in SubmarineInfo.SavedSubmarines.DistinctBy(s => s.Name)) + { + outpostDropdown.AddItem(submarineInfo.DisplayName, userData: submarineInfo.Name.ToIdentifier(), toolTip: submarineInfo.Description); + } + + AssignComponentToServerSetting(outpostDropdown, nameof(ServerSettings.SelectedOutpostName)); + outpostHolder.RectTransform.MinSize = new Point(0, outpostDropdown.RectTransform.MinSize.Y); + + campaignHiddenElements.Add(outpostHolder); + + // biome ----------------------------------------------------------------------------- + GUILayoutGroup biomeHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), gameModeSettingsContent.RectTransform), isHorizontal: true) + { + Stretch = true + }; + var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), biomeHolder.RectTransform), TextManager.Get("biome"), wrap: true); + var biomeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), biomeHolder.RectTransform), elementCount: 6, listBoxScale: 2.0f) + { + AfterSelected = (component, obj) => + { + if (obj != null) + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + } + return true; + } + }; + biomeDropdown.ListBox.RectTransform.SetPosition(Anchor.BottomLeft, Pivot.TopLeft); + //do this before adding the contents, otherwise they get disabled too (and we just want to disable the dropdown itself) + clientDisabledElements.AddRange(biomeHolder.GetAllChildren()); + biomeDropdown.AddItem(TextManager.Get("random"), "Random".ToIdentifier()); + foreach (var biome in Biome.Prefabs) + { + if (biome.IsEndBiome) { continue; } + biomeDropdown.AddItem(biome.DisplayName, biome.Identifier); + } + AssignComponentToServerSetting(biomeDropdown, nameof(ServerSettings.Biome)); + biomeHolder.RectTransform.MinSize = new Point(0, biomeDropdown.RectTransform.MinSize.Y); + + campaignHiddenElements.Add(biomeHolder); //seed ------------------------------------------------------------------ - var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), TextManager.Get("LevelSeed")) + var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), gameModeSettingsContent.RectTransform), TextManager.Get("LevelSeed")) { CanBeFocused = false }; @@ -789,7 +960,7 @@ namespace Barotrauma //level difficulty ------------------------------------------------------------------ - var levelDifficultyHolder = CreateLabeledSlider(settingsContent, "LevelDifficulty", "", "LevelDifficultyExplanation", out levelDifficultySlider, out var difficultySliderLabel, + var levelDifficultyHolder = CreateLabeledSlider(gameModeSettingsContent, "LevelDifficulty", "", "LevelDifficultyExplanation", out levelDifficultySlider, out var difficultySliderLabel, step: 0.01f, range: new Vector2(0.0f, 100.0f)); levelDifficultySlider.OnReleased = (scrollbar, value) => { @@ -810,9 +981,9 @@ namespace Barotrauma clientDisabledElements.AddRange(levelDifficultyHolder.GetAllChildren()); //bot count ------------------------------------------------------------------ - CreateSubHeader("BotSettings", settingsContent); + CreateSubHeader("BotSettings", gameModeSettingsContent); - var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), gameModeSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botCountSettingHolder.RectTransform), TextManager.Get("BotCount"), wrap: true); var botCountSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), botCountSettingHolder.RectTransform)); for (int i = 0; i <= NetConfig.MaxPlayers; i++) @@ -823,7 +994,7 @@ namespace Barotrauma clientDisabledElements.AddRange(botCountSettingHolder.GetAllChildren()); botSettingsElements.Add(botCountSelection); - var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), gameModeSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botSpawnModeSettingHolder.RectTransform), TextManager.Get("BotSpawnMode"), wrap: true); var botSpawnModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeSettingHolder.RectTransform)); foreach (var botSpawnMode in Enum.GetValues(typeof(BotSpawnMode)).Cast()) @@ -843,14 +1014,14 @@ namespace Barotrauma //traitor probability ------------------------------------------------------------------ - CreateSubHeader("TraitorSettings", settingsContent); + CreateSubHeader("TraitorSettings", gameModeSettingsContent); //spacing - new GUIFrame(new RectTransform(new Point(1, GUI.IntScale(5)), settingsContent.RectTransform), style: null); + new GUIFrame(new RectTransform(new Point(1, GUI.IntScale(5)), gameModeSettingsContent.RectTransform), style: null); //the probability slider is a traitor element, but we don't add it to traitorElements //because we don't want to disable it when sliding it to 0 (need to be able to slide it back!) - var traitorProbabilityHolder = CreateLabeledSlider(settingsContent, "traitor.probability", "", "traitor.probability.tooltip", + var traitorProbabilityHolder = CreateLabeledSlider(gameModeSettingsContent, "traitor.probability", "", "traitor.probability.tooltip", out traitorProbabilitySlider, out var traitorProbabilityText, step: 0.01f, range: new Vector2(0.0f, 1.0f)); traitorProbabilitySlider.OnMoved = (scrollbar, value) => @@ -869,7 +1040,7 @@ namespace Barotrauma traitorElements.Clear(); clientDisabledElements.AddRange(traitorProbabilityHolder.GetAllChildren()); - var traitorDangerHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + var traitorDangerHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), gameModeSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; @@ -924,7 +1095,7 @@ namespace Barotrauma traitorElements.AddRange(traitorDangerGroup.Children); traitorElements.AddRange(traitorDangerButtons); - var traitorsMinPlayerCountHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var traitorsMinPlayerCountHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), gameModeSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), traitorsMinPlayerCountHolder.RectTransform), TextManager.Get("ServerSettingsTraitorsMinPlayerCount"), wrap: true) { ToolTip = TextManager.Get("ServerSettingsTraitorsMinPlayerCountToolTip") @@ -946,7 +1117,36 @@ namespace Barotrauma } } - return settingsContent; + return gameModeSettingsContent; + } + + private GUIButton upgradesTabButton, + respawnTabButton; + + private void SelectRespawnTab() + => SelectTabShared(buttonToEnable: respawnTabButton, + buttonToDisable: upgradesTabButton, + elementsToEnable: respawnSettings, + elementsToDisable: disembarkPerkSettings); + + private void SelectUpgradesTab() + => SelectTabShared(buttonToEnable: upgradesTabButton, + buttonToDisable: respawnTabButton, + elementsToEnable: disembarkPerkSettings, + elementsToDisable: respawnSettings); + + private void SelectTabShared(GUIButton buttonToEnable, + GUIButton buttonToDisable, + ICollection elementsToEnable, + ICollection elementsToDisable) + + { + if (buttonToEnable is null || buttonToDisable is null) { return; } + + buttonToDisable.Selected = false; + buttonToEnable.Selected = true; + foreach (var element in elementsToDisable) { element.Visible = element.Enabled = false; } + foreach (var element in elementsToEnable) { element.Visible = element.Enabled = true; } } private GUIComponent CreateGeneralSettingsPanel(GUIComponent parent) @@ -955,13 +1155,40 @@ namespace Barotrauma // settings panel //------------------------------------------------------------------ - GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) + GUILayoutGroup mainContainer = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)); + + GUILayoutGroup tabContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.066f), mainContainer.RectTransform), isHorizontal: true) { + RelativeSpacing = 0.02f, Stretch = true }; - var respawnSettingsHeader = CreateSubHeader("RespawnSettings", settingsLayout); - var settingsContent = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)).Content; + respawnTabButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), tabContainer.RectTransform), TextManager.Get("respawnsettings"), style: "GUITabButton") { Selected = true }; + upgradesTabButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), tabContainer.RectTransform), TextManager.Get("disembarkpointsettings"), style: "GUITabButton"); + + respawnTabButton.OnClicked = (button, _) => + { + SelectRespawnTab(); + return true; + }; + + upgradesTabButton.OnClicked = (button, _) => + { + SelectUpgradesTab(); + return true; + }; + + + GUIFrame mainFrame = new GUIFrame(new RectTransform(new Vector2(1f, 1.0f - tabContainer.RectTransform.RelativeSize.Y), mainContainer.RectTransform), style: null); + + GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, mainFrame.RectTransform)); + + var settingsList = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)); + respawnSettings.Add(settingsLayout); + + CreateDisembarkPointPanel(mainFrame); + + var settingsContent = settingsList.Content; // ------------------------------------------------------------------ @@ -1010,7 +1237,7 @@ namespace Barotrauma { OnSelected = (component, obj) => { - GameMain.Client?.RequestSelectSub(obj as SubmarineInfo, isShuttle: true); + SelectShuttle((SubmarineInfo)obj); return true; } }; @@ -1186,6 +1413,342 @@ namespace Barotrauma return settingsContent; } + private GUIListBox disembarkPerkSettingList; + private GUIComponent disembarkPerkDisabledDisclaimer; + private GUIComponent noPerksAvailableDisclaimer; + private GUITextBlock disembarkPerkFooterText; + + /// + /// Used to prevent disembarkPerkSettingList.AfterSelected from firing when the server settings are updated. + /// + private bool isUpdatingPerks; + + public void CreateDisembarkPointPanel(GUIComponent parent) + { + GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) + { + Stretch = true, + Visible = false, + }; + + var settingsList = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)) + { + SelectMultiple = true, + DisabledColor = Color.White * 0.1f + }; + + disembarkPerkSettingList = settingsList; + + noPerksAvailableDisclaimer = new GUIFrame(new RectTransform(Vector2.One, settingsLayout.RectTransform), style: "GUIBackgroundBlocker") + { + Visible = false, + IgnoreLayoutGroups = true + }; + + new GUITextBlock(new RectTransform(Vector2.One, noPerksAvailableDisclaimer.RectTransform), TextManager.Get("noperksavailable"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) + { + TextColor = GUIStyle.Red, + Shadow = true, + }; + + disembarkPerkDisabledDisclaimer = new GUIFrame(new RectTransform(Vector2.One, settingsLayout.RectTransform), style: "GUIBackgroundBlocker") + { + IgnoreLayoutGroups = true, + }; + var disclaimerLayout = new GUILayoutGroup(new RectTransform(Vector2.One, disembarkPerkDisabledDisclaimer.RectTransform)); + + new GUITextBlock(new RectTransform(new Vector2(1f, 0.3f), disclaimerLayout.RectTransform), TextManager.Get("disembarkpointselectteam"), textAlignment: Alignment.BottomCenter, font: GUIStyle.LargeFont) + { + TextColor = GUIStyle.Red + }; + + var teamSelectLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.7f), disclaimerLayout.RectTransform), isHorizontal: true); + CreateTeamDisclaimerButtons(teamSelectLayout); + + disembarkPerkFooterText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(28)) }, + string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight, textColor: GUIStyle.TextColorBright, color: Color.Black * 0.8f, style: null) + { + Padding = new Vector4(10, 0, 10, 0) * GUI.Scale + }; + UpdatePerkFooterText(settingsList); + + settingsList.AfterSelected = (component, o) => + { + if (GameMain.Client?.ServerSettings is not { } settings) { return false; } + + UpdatePerkFooterText(settingsList); + + if (isUpdatingPerks) { return false; } + + bool canChangePerks = ServerSettings.HasPermissionToChangePerks(); + + if (!canChangePerks) { return false; } + + switch (MultiplayerPreferences.Instance.TeamPreference) + { + case CharacterTeamType.Team2: + { + settings.SelectedSeparatistsPerks = PerksFromSelectedElements(); + break; + } + default: + { + settings.SelectedCoalitionPerks = PerksFromSelectedElements(); + break; + } + + Identifier[] PerksFromSelectedElements() + { + var list = settingsList.AllSelected.Select(static c => ((DisembarkPerkPrefab)c.UserData)).ToList(); + + bool potentiallyHasOrphanedPerks = true; + + do + { + potentiallyHasOrphanedPerks = false; + if (list.None()) { break; } + + list.ForEachMod(perk => + { + if (perk.Prerequisite.IsEmpty) { return; } + + if (list.All(p => p.Identifier != perk.Prerequisite)) + { + list.Remove(perk); + potentiallyHasOrphanedPerks = true; + } + }); + } while (potentiallyHasOrphanedPerks); + + return list.Select(static p => p.Identifier).ToArray(); + } + } + + settings.ClientAdminWritePerks(); + + return true; + }; + + disembarkPerkSettings.Add(settingsLayout); + + Identifier disembarkPerkCategory = Identifier.Empty; + + foreach (var disembarkPerkPrefab in DisembarkPerkPrefab.Prefabs + .OrderBy(static p => p.SortCategory) + .ThenBy(static p => p.Cost) + .ThenBy(static p => p.SortKey)) + { + if (disembarkPerkCategory != disembarkPerkPrefab.SortCategory) + { + disembarkPerkCategory = disembarkPerkPrefab.SortCategory; + + if (!disembarkPerkCategory.IsEmpty) + { + GUIFrame categoryFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), settingsList.Content.RectTransform), style: null) + { + CanBeFocused = false + }; + + new GUITextBlock(new RectTransform(Vector2.One, categoryFrame.RectTransform), TextManager.Get($"perkcategory.{disembarkPerkPrefab.SortCategory}"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); + } + } + + GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), settingsList.Content.RectTransform), style: "ListBoxElement") + { + UserData = disembarkPerkPrefab, + ToolTip = disembarkPerkPrefab.Description + }; + GUILayoutGroup prefabLayout = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), isHorizontal: true) + { + Stretch = true + }; + + var perkLabel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), prefabLayout.RectTransform), disembarkPerkPrefab.Name, textAlignment: Alignment.CenterLeft) + { + DisabledTextColor = Color.White * 0.1f, + DisabledColor = Color.White * 0.1f, + CanBeFocused = false, + }; + + perkLabel.Text = ToolBox.LimitString(perkLabel.Text, perkLabel.Font, perkLabel.Rect.Width); + + var costLabel = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), prefabLayout.RectTransform), disembarkPerkPrefab.Cost.ToString(), textAlignment: Alignment.Right) + { + DisabledTextColor = Color.White * 0.1f, + DisabledColor = Color.White * 0.1f, + CanBeFocused = false, + }; + } + + GameMain.Client?.OnPermissionChanged?.RegisterOverwriteExisting(nameof(CreateDisembarkPointPanel).ToIdentifier(), _ => + { + UpdateDisembarkPointListFromServerSettings(); + }); + + void CreateTeamDisclaimerButtons(GUILayoutGroup buttonParent) + { + var team1Button = new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), buttonParent.RectTransform), style: "CoalitionButton") + { + OnClicked = (button, obj) => + { + TeamPreferenceListBox?.Select(CharacterTeamType.Team1); + return true; + } + }; + + var team2Button = new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), buttonParent.RectTransform), style: "SeparatistButton") + { + OnClicked = (button, obj) => + { + TeamPreferenceListBox?.Select(CharacterTeamType.Team2); + return true; + } + }; + } + } + + private void UpdatePerkFooterText(GUIListBox box) + { + int pointsLeft = GameMain.NetworkMember?.ServerSettings?.DisembarkPointAllowance ?? -1; + bool ignorePerksThatCantApplyWithoutSub = GameSession.ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(SelectedMode, MissionTypes); + + foreach (GUIComponent child in box.Content.Children) + { + if (box.AllSelected.Contains(child) && child.UserData is DisembarkPerkPrefab perkPrefab) + { + if (ignorePerksThatCantApplyWithoutSub && perkPrefab.PerkBehaviors.Any(static b => !b.CanApplyWithoutSubmarine())) + { + continue; + } + pointsLeft -= perkPrefab.Cost; + } + } + + disembarkPerkFooterText.Text = TextManager.GetWithVariable("disembarkpointleft", "[amount]", pointsLeft.ToString()); + + disembarkPerkFooterText.TextColor = + pointsLeft < 0 + ? GUIStyle.Red + : GUIStyle.TextColorBright; + } + + public void UpdateDisembarkPointListFromServerSettings() + { + if (disembarkPerkSettingList is null || disembarkPerkDisabledDisclaimer is null || disembarkPerkFooterText is null) { return; } + + CharacterTeamType teamPreference = MultiplayerPreferences.Instance.TeamPreference; + + bool hasTeamPreference = teamPreference is (CharacterTeamType.Team1 or CharacterTeamType.Team2); + + if (SelectedMode != GameModePreset.PvP) + { + teamPreference = CharacterTeamType.Team1; + hasTeamPreference = true; + } + + disembarkPerkDisabledDisclaimer.Visible = !hasTeamPreference; + disembarkPerkFooterText.Visible = hasTeamPreference; + + SetEnabled(hasTeamPreference); + + bool canManagePerks = ServerSettings.HasPermissionToChangePerks(); + + if (!canManagePerks) + { + SetEnabled(false); + } + + isUpdatingPerks = true; + + bool hasAvailablePerks = false; + if (GameMain.Client?.ServerSettings is { } settings) + { + Identifier[] selectedPerks = teamPreference switch + { + CharacterTeamType.Team1 => settings.SelectedCoalitionPerks, + CharacterTeamType.Team2 => settings.SelectedSeparatistsPerks, + _ => Array.Empty() + }; + + bool ignorePerksThatCantApplyWithoutSub = GameSession.ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(SelectedMode, MissionTypes); + disembarkPerkSettingList.Deselect(); + foreach (GUIComponent child in disembarkPerkSettingList.Content.Children) + { + if (child.UserData is not DisembarkPerkPrefab perkPrefab) { continue; } + bool shouldSelect = selectedPerks.Contains(perkPrefab.Identifier); + + bool hasPrerequisite = !perkPrefab.Prerequisite.IsEmpty; + bool isMutuallyExclusivePerkSelected = selectedPerks.Any(p => perkPrefab.MutuallyExclusivePerks.Contains(p)); + TogglePerkElement(enabled: true); + + if (shouldSelect) + { + disembarkPerkSettingList.Select(child.UserData, force: GUIListBox.Force.Yes, GUIListBox.AutoScroll.Disabled); + } + + if (hasPrerequisite) + { + bool enabled = selectedPerks.Contains(perkPrefab.Prerequisite); + TogglePerkElement(enabled); + } + + if (isMutuallyExclusivePerkSelected) + { + TogglePerkElement(enabled: false); + } + + if (ignorePerksThatCantApplyWithoutSub) + { + if (perkPrefab.PerkBehaviors.Any(static b => !b.CanApplyWithoutSubmarine())) + { + TogglePerkElement(enabled: false); + } + } + + if (child.Enabled) + { + hasAvailablePerks = true; + } + + void TogglePerkElement(bool enabled) + { + child.Enabled = enabled; + foreach (GUITextBlock text in child.GetAllChildren()) + { + text.Enabled = enabled; + } + } + } + } + + noPerksAvailableDisclaimer.Visible = !hasAvailablePerks; + if (!hasAvailablePerks) + { + disembarkPerkDisabledDisclaimer.Visible = false; + } + + UpdatePerkFooterText(disembarkPerkSettingList); + isUpdatingPerks = false; + + void SetEnabled(bool enabled) + { + disembarkPerkSettingList.Enabled = enabled; + foreach (GUIComponent child in disembarkPerkSettingList.Content.Children) + { + //child.Enabled = enabled; + foreach (GUITextBlock block in child.GetAllChildren()) + { + block.Enabled = enabled; + } + } + } + } + + public static void SelectShuttle(SubmarineInfo info) + { + GameMain.Client?.RequestSelectSub(info, SelectedSubType.Shuttle); + } + public static GUITextBlock CreateSubHeader(string textTag, GUIComponent parent, string toolTipTag = null) { var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), parent.RectTransform) { MinSize = new Point(0, GUI.IntScale(28)) }, @@ -1200,16 +1763,24 @@ namespace Barotrauma return header; } - public static GUIComponent CreateLabeledSlider(GUIComponent parent, string headerTag, string valueLabelTag, string tooltipTag, out GUIScrollBar slider, out GUITextBlock label, float? step = null, Vector2? range = null) + public static GUIComponent CreateLabeledSlider(GUIComponent parent, string headerTag, string valueLabelTag, string tooltipTag, + out GUIScrollBar slider, out GUITextBlock label, float? step = null, Vector2? range = null) + { + return CreateLabeledSlider(parent, headerTag, valueLabelTag, tooltipTag, out slider, out label, out GUITextBlock _, step, range); + } + + public static GUIComponent CreateLabeledSlider(GUIComponent parent, string headerTag, string valueLabelTag, string tooltipTag, + out GUIScrollBar slider, out GUITextBlock label, out GUITextBlock header, float? step = null, Vector2? range = null) { GUILayoutGroup verticalLayout = null; + header = null; if (!headerTag.IsNullOrEmpty()) { verticalLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), isHorizontal: false) { Stretch = true }; - var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), verticalLayout.RectTransform), + header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), verticalLayout.RectTransform), TextManager.Get(headerTag), textAlignment: Alignment.CenterLeft) { CanBeFocused = false @@ -1418,21 +1989,12 @@ namespace Barotrauma }; // Chat input - - var chatRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), socialHolder.RectTransform), + chatRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), socialHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - - chatInput = new GUITextBox(new RectTransform(new Vector2(0.95f, 1.0f), chatRow.RectTransform)) - { - MaxTextLength = ChatMessage.MaxLength, - Font = GUIStyle.SmallFont, - DeselectAfterMessage = false - }; - - micIcon = new GUIImage(new RectTransform(new Vector2(0.05f, 1.0f), chatRow.RectTransform), style: "GUIMicrophoneUnavailable"); + RefreshChatrow(); serverLogHolder = new GUILayoutGroup(new RectTransform(Vector2.One, logHolderBottom.RectTransform, Anchor.Center)) { @@ -1610,11 +2172,16 @@ namespace Barotrauma GUI.ClearCursorWait(); } + public const string PleaseWaitPopupUserData = "PleaseWaitPopup"; + public static IEnumerable WaitForStartRound(GUIButton startButton) { GUI.SetCursorWaiting(); LocalizedString headerText = TextManager.Get("RoundStartingPleaseWait"); - var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), Array.Empty()); + var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), Array.Empty()) + { + UserData = PleaseWaitPopupUserData + }; if (startButton != null) { @@ -1639,6 +2206,7 @@ namespace Barotrauma public override void Deselect() { + GameMain.Client?.OnPermissionChanged.TryDeregister(nameof(CreateDisembarkPointPanel).ToIdentifier()); SaveAppearance(); chatInput.Deselect(); CampaignCharacterDiscarded = false; @@ -1663,14 +2231,8 @@ namespace Barotrauma changesPendingText?.Parent?.RemoveChild(changesPendingText); changesPendingText = null; - - chatInput.Select(); - chatInput.OnEnterPressed = GameMain.Client.EnterChatMessage; - chatInput.OnTextChanged += GameMain.Client.TypingChatMessage; - chatInput.OnDeselected += (sender, key) => - { - GameMain.Client?.ChatBox.ChatManager.Clear(); - }; + + RefreshChatrow(); //disable/hide elements the clients are not supposed to use/see clientDisabledElements.ForEach(c => c.Enabled = false); @@ -1680,7 +2242,6 @@ namespace Barotrauma if (GameMain.Client != null) { - ChatManager.RegisterKeys(chatInput, GameMain.Client.ChatBox.ChatManager); joinOnGoingRoundButton.Visible = GameMain.Client.GameStarted; ReadyToStartBox.Selected = false; GameMain.Client.SetReadyToStart(ReadyToStartBox); @@ -1999,7 +2560,7 @@ namespace Barotrauma TextManager.Get("deceased"), textAlignment: Alignment.Center, font: GUIStyle.LargeFont); - if (GameMain.Client?.ServerSettings is { IronmanMode: true }) + if (GameMain.Client?.ServerSettings is { IronmanModeActive: true }) { new GUITextBlock( new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), @@ -2073,7 +2634,7 @@ namespace Barotrauma TeamPreferenceListBox.UpdateDimensions(); Color team1Color = new Color(0, 110, 150, 255); - var team1Option = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.team1"), textAlignment: Alignment.Center, style: null) + pvpTeamChoiceTeam1 = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.team1"), textAlignment: Alignment.Center, style: null) { UserData = CharacterTeamType.Team1, CanBeFocused = true, @@ -2084,11 +2645,13 @@ namespace Barotrauma OutlineColor = team1Color, TextColor = Color.White, HoverTextColor = Color.White, - SelectedTextColor = Color.White + SelectedTextColor = Color.White, + DisabledColor = team1Color * 0.25f, + DisabledTextColor = Color.Gray, }; Color noPreferenceColor = new Color(100, 100, 100, 255); - var noPreferenceOption = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.nopreference"), textAlignment: Alignment.Center, style: null) + pvpTeamChoiceMiddleButton = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), TeamPreferenceListBox.Content.RectTransform), "", textAlignment: Alignment.Center, style: null) { UserData = CharacterTeamType.None, CanBeFocused = true, @@ -2099,11 +2662,11 @@ namespace Barotrauma OutlineColor = noPreferenceColor, TextColor = Color.White, HoverTextColor = Color.White, - SelectedTextColor = Color.White + SelectedTextColor = Color.White, }; Color team2Color = new Color(150, 110, 0, 255); - var team2Option = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.team2"), textAlignment: Alignment.Center, style: null) + pvpTeamChoiceTeam2 = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.team2"), textAlignment: Alignment.Center, style: null) { UserData = CharacterTeamType.Team2, CanBeFocused = true, @@ -2114,24 +2677,79 @@ namespace Barotrauma OutlineColor = team2Color, TextColor = Color.White, HoverTextColor = Color.White, - SelectedTextColor = Color.White + SelectedTextColor = Color.White, + DisabledColor = team2Color * 0.25f, + DisabledTextColor = Color.Gray, }; - TeamPreferenceListBox.Select(MultiplayerPreferences.Instance.TeamPreference); + var prevTeamSelection = MultiplayerPreferences.Instance.TeamPreference; + ResetPvpTeamSelection(); + + // Handle special case: middle button in Player Choice mode should pick a random team, if possible TeamPreferenceListBox.OnSelected += (component, obj) => { - if ((CharacterTeamType)obj == MultiplayerPreferences.Instance.TeamPreference) { return true; } - - MultiplayerPreferences.Instance.TeamPreference = (CharacterTeamType)obj; - GameMain.Client.ForceNameAndJobUpdate(); + CharacterTeamType newTeamPreference = (CharacterTeamType)obj; + if (newTeamPreference == CharacterTeamType.None + && GameMain.Client?.ServerSettings?.PvpTeamSelectionMode == PvpTeamSelectionMode.PlayerChoice) + { + TeamPreferenceListBox.Select(Rand.Value() < 0.5 ? CharacterTeamType.Team1 : CharacterTeamType.Team2); + var teamColor = (CharacterTeamType)TeamPreferenceListBox.SelectedData == CharacterTeamType.Team1 ? team1Color : team2Color; + TeamPreferenceListBox.SelectedComponent.Flash(teamColor, useRectangleFlash: true, flashDuration: 1.0f); + return true; + } + return false; // Allow the next delegate to handle other cases + }; + + // Handle everything else + TeamPreferenceListBox.OnSelected += (component, obj) => + { + CharacterTeamType newTeamPreference = (CharacterTeamType)obj; + + if (newTeamPreference == CharacterTeamType.None + && GameMain.Client?.ServerSettings?.PvpTeamSelectionMode == PvpTeamSelectionMode.PlayerChoice) { return false; } // Already handled by delegate above + + MultiplayerPreferences.Instance.TeamPreference = newTeamPreference; + + UpdateSelectedSub(newTeamPreference); + GameMain.Client?.ForceNameJobTeamUpdate(); + RefreshPvpTeamSelectionButtons(); GameSettings.SaveCurrentConfig(); - + UpdateDisembarkPointListFromServerSettings(); + //need to update job preferences and close the selection frame + //because the team selection might affect the uniform sprite and the loadouts + UpdateJobPreferences(GameMain.Client?.CharacterInfo ?? Character.Controlled?.Info); + JobSelectionFrame = null; + RefreshChatrow(); // to enable/disable team chat according to current selection + return true; }; + + if (prevTeamSelection != CharacterTeamType.None) + { + TeamPreferenceListBox.Select(prevTeamSelection); + } } } + public void UpdateSelectedSub(CharacterTeamType preference) + { + bool votingEnabled = GameMain.NetworkMember.ServerSettings.SubSelectionMode == SelectionMode.Vote; + SubList.OnSelected -= VotableClicked; + switch (preference) + { + case CharacterTeamType.Team1 or CharacterTeamType.None when SelectedSub is { } selectedSub: + TrySelectSub(selectedSub.Name, selectedSub.MD5Hash.StringRepresentation, SelectedSubType.Sub, SubList, showPreview: false); + if (!votingEnabled) { SubList.Select(selectedSub, autoScroll: GUIListBox.AutoScroll.Disabled); } + break; + case CharacterTeamType.Team2 when SelectedEnemySub is { } selectedEnemySub: + TrySelectSub(selectedEnemySub.Name, selectedEnemySub.MD5Hash.StringRepresentation, SelectedSubType.EnemySub, SubList, showPreview: false); + if (!votingEnabled) { SubList.Select(selectedEnemySub, autoScroll: GUIListBox.AutoScroll.Disabled); } + break; + } + SubList.OnSelected += VotableClicked; + } + public void TryDiscardCampaignCharacter(Action onYes) { var confirmation = new GUIMessageBox(TextManager.Get("NewCampaignCharacterHeader"), TextManager.Get("NewCampaignCharacterText"), @@ -2151,6 +2769,9 @@ namespace Barotrauma { if (!createPendingChangesText || changesPendingText != null || playerInfoContent == null) { return; } + //remove the previous one + changesPendingText?.Parent?.RemoveChild(changesPendingText); + changesPendingText = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.065f), playerInfoContent.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, -0.03f) }, style: "OuterGlow") { @@ -2177,7 +2798,7 @@ namespace Barotrauma }; } - private void CreateJobVariantTooltip(JobPrefab jobPrefab, int variant, GUIComponent parentSlot) + private void CreateJobVariantTooltip(JobPrefab jobPrefab, CharacterTeamType team, int variant, bool isPvPMode, GUIComponent parentSlot) { jobVariantTooltip = new GUIFrame(new RectTransform(new Point((int)(400 * GUI.Scale), (int)(180 * GUI.Scale)), GUI.Canvas, pivot: Pivot.BottomRight), style: "GUIToolTip") @@ -2194,16 +2815,16 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.GetWithVariable("startingequipmentname", "[number]", (variant + 1).ToString()), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); - var itemIdentifiers = jobPrefab.PreviewItems[variant] + var itemIdentifiers = jobPrefab.JobItems[variant] .Where(it => it.ShowPreview) - .Select(it => it.ItemIdentifier) + .Select(it => it.GetItemIdentifier(team, isPvPMode)) .Distinct(); int itemsPerRow = 5; int rows = (int)Math.Max(Math.Ceiling(itemIdentifiers.Count() / (float)itemsPerRow), 1); new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.4f * rows), content.RectTransform, Anchor.BottomCenter), - onDraw: (sb, component) => { DrawJobVariantItems(sb, component, new JobVariant(jobPrefab, variant), itemsPerRow); }); + onDraw: (sb, component) => { DrawJobVariantItems(sb, component, new JobVariant(jobPrefab, variant), team, isPvPMode, itemsPerRow); }); jobVariantTooltip.RectTransform.MinSize = new Point(0, content.RectTransform.Children.Sum(c => c.Rect.Height + content.AbsoluteSpacing)); } @@ -2245,12 +2866,72 @@ namespace Barotrauma new GUITextBlock(new RectTransform(Vector2.One, playerInfoContent.RectTransform, Anchor.Center), TextManager.Get("PlayingAsSpectator"), textAlignment: Alignment.Center); + + if (SelectedMode == GameModePreset.PvP) + { + // In PvP mode, becoming a spectator should reset any existing team selection + ResetPvpTeamSelection(); + } } else { UpdatePlayerFrame(campaignCharacterInfo, allowEditing: campaignCharacterInfo == null); } } + + public void RefreshPvpTeamSelectionButtons() + { + if (pvpTeamChoiceMiddleButton == null || pvpTeamChoiceTeam1 == null || pvpTeamChoiceTeam2 == null) + { + return; + } + + ServerSettings serverSettings = GameMain.Client.ServerSettings; + + CharacterTeamType currentTeam = MultiplayerPreferences.Instance.TeamPreference; + bool pvpPlayerChoiceMode = serverSettings.PvpTeamSelectionMode == PvpTeamSelectionMode.PlayerChoice; + + pvpTeamChoiceMiddleButton.Text = TextManager.Get(pvpPlayerChoiceMode ? "PvP.PickRandom" : "teampreference.nopreference"); + if (pvpPlayerChoiceMode && serverSettings.PvpAutoBalanceThreshold > 0) + { + pvpTeamChoiceTeam1.Enabled = currentTeam == CharacterTeamType.Team1 || CanJoinTeam1(); + pvpTeamChoiceTeam2.Enabled = currentTeam == CharacterTeamType.Team2 || CanJoinTeam2(); + pvpTeamChoiceTeam1.ToolTip = !pvpTeamChoiceTeam1.Enabled ? TextManager.Get("PvP.TeamDisabledBecauseBalance") : null; + pvpTeamChoiceTeam2.ToolTip = !pvpTeamChoiceTeam2.Enabled ? TextManager.Get("PvP.TeamDisabledBecauseBalance") : null; + pvpTeamChoiceMiddleButton.Enabled = CanJoinTeam1() && CanJoinTeam2(); + } + else + { + pvpTeamChoiceTeam1.Enabled = true; + pvpTeamChoiceTeam2.Enabled = true; + pvpTeamChoiceTeam1.ToolTip = null; + pvpTeamChoiceTeam2.ToolTip = null; + pvpTeamChoiceMiddleButton.Enabled = true; + } + + bool CanJoinTeam1() + { + int newTeam1Count = Team1Count + (currentTeam == CharacterTeamType.Team1 ? 0 : 1); + int newTeam2Count = Team2Count - (currentTeam == CharacterTeamType.Team2 ? 1 : 0); + return newTeam1Count - newTeam2Count <= serverSettings.PvpAutoBalanceThreshold; + } + + bool CanJoinTeam2() + { + int newTeam2Count = Team2Count + (currentTeam == CharacterTeamType.Team2 ? 0 : 1); + int newTeam1Count = Team1Count - (currentTeam == CharacterTeamType.Team1 ? 1 : 0); + return newTeam2Count - newTeam1Count <= serverSettings.PvpAutoBalanceThreshold; + } + } + + public void ResetPvpTeamSelection() + { + TeamPreferenceListBox?.Deselect(); + MultiplayerPreferences.Instance.TeamPreference = CharacterTeamType.None; + RefreshPvpTeamSelectionButtons(); + RefreshChatrow(); + GameMain.Client.ForceNameJobTeamUpdate(); + } public void SetAllowSpectating(bool allowSpectating) { @@ -2274,9 +2955,57 @@ namespace Barotrauma autoRestartTimer = timer; } - public void SetMissionType(MissionType missionType) + public void SetMissionTypes(IEnumerable missionTypes) { - MissionType = missionType; + MissionTypes = missionTypes; + } + + private void RefreshOutpostDropdown() + { + outpostDropdown.Parent.Visible = MissionTypeFrame.Visible; + if (!outpostDropdown.Parent.Visible) { return; } + + outpostDropdownUpToDate = false; + + Identifier prevSelected = GameMain.NetworkMember?.ServerSettings.SelectedOutpostName ?? Identifier.Empty; + + outpostDropdown.ClearChildren(); + outpostDropdown.AddItem(TextManager.Get("Random"), "Random".ToIdentifier()); + HashSet validOutpostTagsForMissions = new HashSet(); + + IEnumerable suitableMissionClasses = + SelectedMode == GameModePreset.PvP ? + MissionPrefab.PvPMissionClasses.Values : + MissionPrefab.CoOpMissionClasses.Values; + foreach (var missionType in MissionTypes) + { + foreach (var missionPrefab in MissionPrefab.Prefabs) + { + if (!suitableMissionClasses.Contains(missionPrefab.MissionClass)) { continue; } + if (missionPrefab.Type != missionType || missionPrefab.SingleplayerOnly) { continue; } + if (!missionPrefab.AllowOutpostSelectionFromTag.IsEmpty) + { + validOutpostTagsForMissions.Add(missionPrefab.AllowOutpostSelectionFromTag); + } + } + } + if (validOutpostTagsForMissions.Any()) + { + foreach (var submarineInfo in SubmarineInfo.SavedSubmarines.DistinctBy(s => s.Name)) + { + if (submarineInfo.Type == SubmarineType.Outpost && + validOutpostTagsForMissions.Any(tag => submarineInfo.OutpostTags.Contains(tag))) + { + outpostDropdown.AddItem(submarineInfo.DisplayName, userData: submarineInfo.Name.ToIdentifier(), toolTip: submarineInfo.Description); + } + } + outpostDropdown.ListBox.Select(prevSelected); + } + else + { + outpostDropdown.Parent.Visible = false; + } + outpostDropdownUpToDate = true; } public void UpdateSubList(GUIComponent subList, IEnumerable submarines) @@ -2313,11 +3042,25 @@ namespace Barotrauma UserData = sub }; - int buttonSize = (int)(frame.Rect.Height * 0.8f); - var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft), + var frameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 1f), frame.RectTransform), isHorizontal: true); + + var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), frameLayout.RectTransform, Anchor.CenterLeft), ToolBox.LimitString(sub.DisplayName.Value, GUIStyle.Font, subList.Rect.Width - 65), textAlignment: Alignment.CenterLeft) { - CanBeFocused = false + UserData = "nametext", + CanBeFocused = true + }; + + var pvpContainer = new GUIFrame(new RectTransform(new Vector2(0.3f, 1f), frameLayout.RectTransform, Anchor.CenterRight), style: null); + var coalitionIcon = new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), pvpContainer.RectTransform, Anchor.CenterLeft), style: "CoalitionIcon") + { + Visible = false, + UserData = CoalitionIconUserData, + }; + var separatistsIcon = new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), pvpContainer.RectTransform, Anchor.CenterRight), style: "SeparatistIcon") + { + Visible = false, + UserData = SeparatistsIconUserData, }; var matchingSub = @@ -2382,10 +3125,11 @@ namespace Barotrauma } else { - var infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, isHorizontal: false); + var infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, isHorizontal: false); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.BottomRight, font: GUIStyle.SmallFont) { + Padding = Vector4.Zero, UserData = "pricetext", TextColor = subTextBlock.TextColor * 0.8f, CanBeFocused = false @@ -2393,6 +3137,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.TopRight, font: GUIStyle.SmallFont) { + Padding = Vector4.Zero, UserData = "classtext", TextColor = subTextBlock.TextColor * 0.8f, ToolTip = subTextBlock.ToolTip, @@ -2408,9 +3153,33 @@ namespace Barotrauma VoteType voteType; if (component.Parent == GameMain.NetLobbyScreen.SubList.Content) { + if (SelectedMode == GameModePreset.PvP && MultiplayerPreferences.Instance.TeamPreference is not (CharacterTeamType.Team1 or CharacterTeamType.Team2)) + { + // we are in PvP but don't have a team selected, so we can't select a sub + // and also highlight the team selection list + + foreach (GUIComponent child in TeamPreferenceListBox.Content.Children) + { + if (child.UserData is CharacterTeamType.None) { continue; } + child.Flash(GUIStyle.Red, useRectangleFlash: true, flashDuration: 1f); + } + + return false; + } if (!GameMain.Client.ServerSettings.AllowSubVoting) { - var selectedSub = component.UserData as SubmarineInfo; + var selectedSub = (SubmarineInfo)component.UserData; + var type = SelectedMode != GameModePreset.PvP + ? SelectedSubType.Sub + : MultiplayerPreferences.Instance.TeamPreference switch + { + CharacterTeamType.None or CharacterTeamType.Team1 + => SelectedSubType.Sub, + CharacterTeamType.Team2 + => SelectedSubType.EnemySub, + _ => throw new NotImplementedException() + }; + if (SelectedMode == GameModePreset.MultiPlayerCampaign && CampaignSetupUI != null) { if (selectedSub.Price > CampaignSettings.CurrentSettings.InitialMoney) @@ -2433,7 +3202,7 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = msgBox.Close; msgBox.Buttons[0].OnClicked += (button, obj) => { - GameMain.Client.RequestSelectSub(obj as SubmarineInfo, isShuttle: false); + GameMain.Client.RequestSelectSub(obj as SubmarineInfo, type); return true; }; msgBox.Buttons[1].OnClicked = msgBox.Close; @@ -2441,7 +3210,7 @@ namespace Barotrauma } else if (GameMain.Client.HasPermission(ClientPermissions.SelectSub)) { - GameMain.Client.RequestSelectSub(selectedSub, isShuttle: false); + GameMain.Client.RequestSelectSub(selectedSub, type); return true; } return false; @@ -3248,9 +4017,11 @@ namespace Barotrauma if (GUI.MouseOn?.UserData is JobVariant jobPrefab && GUI.MouseOn.Style?.Name == "JobVariantButton") { - if (jobVariantTooltip?.UserData is not JobVariant prevVisibleVariant || prevVisibleVariant.Prefab != jobPrefab.Prefab || prevVisibleVariant.Variant != jobPrefab.Variant) + if (jobVariantTooltip?.UserData is not JobVariant prevVisibleVariant || + prevVisibleVariant.Prefab != jobPrefab.Prefab || + prevVisibleVariant.Variant != jobPrefab.Variant) { - CreateJobVariantTooltip(jobPrefab.Prefab, jobPrefab.Variant, GUI.MouseOn.Parent); + CreateJobVariantTooltip(jobPrefab.Prefab, TeamPreference, jobPrefab.Variant, isPvPMode: SelectedMode == GameModePreset.PvP, GUI.MouseOn.Parent); } } if (jobVariantTooltip != null) @@ -3334,11 +4105,11 @@ namespace Barotrauma } } - private static void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobPrefab, int itemsPerRow) + private static void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobPrefab, CharacterTeamType team, bool isPvPMode, int itemsPerRow) { - var itemIdentifiers = jobPrefab.Prefab.PreviewItems[jobPrefab.Variant] + var itemIdentifiers = jobPrefab.Prefab.JobItems[jobPrefab.Variant] .Where(it => it.ShowPreview) - .Select(it => it.ItemIdentifier) + .Select(it => it.GetItemIdentifier(team, isPvPMode)) .Distinct(); Point slotSize = new Point(component.Rect.Height); @@ -3360,7 +4131,7 @@ namespace Barotrauma LocalizedString tooltip = null; foreach (Identifier itemIdentifier in itemIdentifiers) { - if (!(MapEntityPrefab.Find(null, identifier: itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab)) { continue; } + if (MapEntityPrefab.FindByIdentifier(identifier: itemIdentifier) is not ItemPrefab itemPrefab) { continue; } int row = (int)Math.Floor(i / (float)slotCountPerRow); int slotsPerThisRow = Math.Min((slotCount - row * slotCountPerRow), slotCountPerRow); @@ -3377,7 +4148,7 @@ namespace Barotrauma float iconScale = Math.Min(Math.Min(slotSize.X / icon.size.X, slotSize.Y / icon.size.Y), 2.0f) * 0.9f; icon.Draw(spriteBatch, slotPos + slotSize.ToVector2() * 0.5f, scale: iconScale); - int count = jobPrefab.Prefab.PreviewItems[jobPrefab.Variant].Count(it => it.ShowPreview && it.ItemIdentifier == itemIdentifier); + int count = jobPrefab.Prefab.JobItems[jobPrefab.Variant].Where(it => it.ShowPreview && it.ItemIdentifier == itemIdentifier).Sum(it => it.Amount); if (count > 1) { string itemCountText = "x" + count; @@ -3405,9 +4176,20 @@ namespace Barotrauma { chatBox.RemoveChild(chatBox.Content.Children.First()); } - + + LocalizedString displayedChatRow = ChatMessage.GetTimeStamp(); + if (message.Type == ChatMessageType.Private) + { + displayedChatRow += TextManager.Get("PrivateMessageTag") + " "; + } + else if (message.Type == ChatMessageType.Team) + { + displayedChatRow += TextManager.Get("PvP.ChatMode.Team.ChatPrefixTag") + " "; + } + displayedChatRow += message.TextWithSender; + GUITextBlock msg = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), chatBox.Content.RectTransform), - text: RichString.Rich(ChatMessage.GetTimeStamp() + (message.Type == ChatMessageType.Private ? TextManager.Get("PrivateMessageTag") + " " : "") + message.TextWithSender), + text: RichString.Rich(displayedChatRow), textColor: message.Color, color: ((chatBox.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f, wrap: true, font: GUIStyle.SmallFont) @@ -3617,7 +4399,10 @@ namespace Barotrauma }; itemsInRow++; - var images = AddJobSpritesToGUIComponent(jobButton, jobPrefab.Prefab, selectedByPlayer: false); + var images = AddJobSpritesToGUIComponent(jobButton, jobPrefab.Prefab, + team: TeamPreference, + isPvPMode: SelectedMode == GameModePreset.PvP, + selectedByPlayer: false); if (images != null && images.Length > 1) { jobPrefab.Variant = Math.Min(jobPrefab.Variant, images.Length); @@ -3625,23 +4410,17 @@ namespace Barotrauma GUIButton currSelected = null; for (int variantIndex = 0; variantIndex < images.Length; variantIndex++) { - foreach (GUIImage image in images[variantIndex]) - { - image.Visible = currVisible == variantIndex; - } + images[variantIndex].Visible = currVisible == variantIndex; var variantButton = CreateJobVariantButton(jobPrefab, variantIndex, images.Length, jobButton); variantButton.OnClicked = (btn, obj) => { if (currSelected != null) { currSelected.Selected = false; } - int k = ((JobVariant)obj).Variant; + int selectedVariantIndex = ((JobVariant)obj).Variant; btn.Parent.UserData = obj; - for (int j = 0; j < images.Length; j++) + for (int i = 0; i < images.Length; i++) { - foreach (GUIImage image in images[j]) - { - image.Visible = k == j; - } + images[i].Visible = selectedVariantIndex == i; } currSelected = btn; currSelected.Selected = true; @@ -3664,36 +4443,28 @@ namespace Barotrauma return true; } - private static GUIImage[][] AddJobSpritesToGUIComponent(GUIComponent parent, JobPrefab jobPrefab, bool selectedByPlayer) + private static GUIImage[] AddJobSpritesToGUIComponent(GUIComponent parent, JobPrefab jobPrefab, CharacterTeamType team, bool isPvPMode, bool selectedByPlayer) { GUIFrame innerFrame = null; - List outfitPreviews = jobPrefab.GetJobOutfitSprites(CharacterPrefab.HumanPrefab.CharacterInfoPrefab, useInventoryIcon: true, out var maxDimensions); + List outfitPreviews = jobPrefab.GetJobOutfitSprites(team, isPvPMode).ToList(); innerFrame = new GUIFrame(new RectTransform(Vector2.One * 0.85f, parent.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; - GUIImage[][] retVal = Array.Empty(); + GUIImage[] retVal = new GUIImage[outfitPreviews.Count]; if (outfitPreviews != null && outfitPreviews.Any()) { - retVal = new GUIImage[outfitPreviews.Count][]; for (int i = 0; i < outfitPreviews.Count; i++) { - JobPrefab.OutfitPreview outfitPreview = outfitPreviews[i]; - retVal[i] = new GUIImage[outfitPreview.Sprites.Count]; - for (int j = 0; j < outfitPreview.Sprites.Count; j++) + Sprite outfitPreview = outfitPreviews[i]; + float aspectRatio = outfitPreview.size.Y / outfitPreview.size.X; + retVal[i] = new GUIImage(new RectTransform(new Vector2(0.7f / aspectRatio, 0.7f), innerFrame.RectTransform, Anchor.Center), outfitPreview, scaleToFit: true) { - Sprite sprite = outfitPreview.Sprites[j].sprite; - Vector2 drawOffset = outfitPreview.Sprites[j].drawOffset; - float aspectRatio = outfitPreview.Dimensions.Y / outfitPreview.Dimensions.X; - retVal[i][j] = new GUIImage(new RectTransform(new Vector2(0.7f / aspectRatio, 0.7f), innerFrame.RectTransform, Anchor.Center) - { RelativeOffset = drawOffset / outfitPreview.Dimensions }, sprite, scaleToFit: true) - { - PressedColor = Color.White, - CanBeFocused = false - }; - } + PressedColor = Color.White, + CanBeFocused = false + }; } } @@ -3744,7 +4515,8 @@ namespace Barotrauma { SaveAppearance(); UpdatePlayerFrame(null); - GameMain.Client.ConnectedClients.ForEach(c => SetPlayerNameAndJobPreference(c)); + GameMain.Client.ConnectedClients.ForEach(SetPlayerNameAndJobPreference); + ResetPvpTeamSelection(); } if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this) @@ -3755,6 +4527,7 @@ namespace Barotrauma respawnModeSelection.Refresh(); // not all respawn modes are compatible with all game modes RefreshGameModeContent(); RefreshEnabledElements(); + UpdateDisembarkPointListFromServerSettings(); } public void HighlightMode(int modeIndex) @@ -3768,23 +4541,58 @@ namespace Barotrauma private void RefreshMissionTypes() { + IEnumerable suitableMissionClasses; + if (SelectedMode == GameModePreset.Mission) + { + suitableMissionClasses = MissionPrefab.CoOpMissionClasses.Values; + } + else if (SelectedMode == GameModePreset.PvP) + { + suitableMissionClasses = MissionPrefab.PvPMissionClasses.Values; + } + else + { + return; + } for (int i = 0; i < missionTypeTickBoxes.Length; i++) { - MissionType missionType = (MissionType)(int)missionTypeTickBoxes[i].UserData; - if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) + Identifier missionType = (Identifier)missionTypeTickBoxes[i].UserData; + missionTypeTickBoxes[i].Parent.Visible = + MissionPrefab.Prefabs.Any(p => p.Type == missionType && suitableMissionClasses.Contains(p.MissionClass)); + } + } + + private void RefreshGameModeSettingsContent() + { + foreach (var element in campaignHiddenElements) + { + SetElementVisible(element, SelectedMode != GameModePreset.MultiPlayerCampaign && + SelectedMode != GameModePreset.SinglePlayerCampaign); + } + foreach (var element in pvpOnlyElements) + { + SetElementVisible(element, SelectedMode == GameModePreset.PvP); + } + + if (respawnTabButton != null && upgradesTabButton != null) + { + if (SelectedMode == GameModePreset.MultiPlayerCampaign) { - missionTypeTickBoxes[i].Parent.Visible = false; - continue; + SelectRespawnTab(); + respawnTabButton.Enabled = upgradesTabButton.Enabled = false; } - if (SelectedMode == GameModePreset.Mission) + else { - missionTypeTickBoxes[i].Parent.Visible = MissionPrefab.CoOpMissionClasses.ContainsKey(missionType); - } - else if (SelectedMode == GameModePreset.PvP) - { - missionTypeTickBoxes[i].Parent.Visible = MissionPrefab.PvPMissionClasses.ContainsKey(missionType); + respawnTabButton.Enabled = upgradesTabButton.Enabled = true; } } + + static void SetElementVisible(GUIComponent element, bool enabled) + { + element.Visible = enabled; + } + + gameModeSettingsLayout.Recalculate(); } private void RefreshGameModeContent() @@ -3808,6 +4616,33 @@ namespace Barotrauma }); autoRestartBox.Parent.Visible = true; + + UpdateDisembarkPointListFromServerSettings(); + + bool isPvP = SelectedMode == GameModePreset.PvP; + foreach (GUIComponent child in SubList.Content.Children) + { + var container = child.GetChild(); + + var imageFrame = container.GetChild(); + + var coalIcon = imageFrame.GetChildByUserData(CoalitionIconUserData); + var sepIcon = imageFrame.GetChildByUserData(SeparatistsIconUserData); + coalIcon.Visible = isPvP; + sepIcon.Visible = isPvP; + + if (GameMain.NetworkMember.ServerSettings.SubSelectionMode != SelectionMode.Vote) + { + coalIcon.Enabled = sepIcon.Enabled = false; + if (child.UserData is not SubmarineInfo info) { continue; } + if (SelectedSub == info) { coalIcon.Enabled = true; } + if (SelectedEnemySub == info) { sepIcon.Enabled = true; } + } + } + + UpdateSelectedSub(isPvP ? MultiplayerPreferences.Instance.TeamPreference : CharacterTeamType.None); + + RefreshGameModeSettingsContent(); if (SelectedMode == GameModePreset.Mission || SelectedMode == GameModePreset.PvP) { MissionTypeFrame.Visible = true; @@ -3871,6 +4706,7 @@ namespace Barotrauma ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; RefreshStartButtonVisibility(); + RefreshOutpostDropdown(); } public void RefreshStartButtonVisibility() @@ -3878,8 +4714,8 @@ namespace Barotrauma if (CampaignSetupUI != null && CampaignSetupFrame is { Visible: true }) { //setting up a campaign -> start button only visible if we're in the "new game" tab (load game menu not visible) - StartButton.Visible = - !GameMain.Client.GameStarted && + StartButton.Visible = + !GameMain.Client.GameStarted && !CampaignSetupUI.LoadGameMenuVisible && (GameMain.Client.HasPermission(ClientPermissions.ManageRound) || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)); } @@ -3887,10 +4723,71 @@ namespace Barotrauma { //if a campaign is currently running, we must show the start button to allow continuing bool campaignActive = GameMain.GameSession?.GameMode is CampaignMode; - StartButton.Visible = + StartButton.Visible = (SelectedMode != GameModePreset.MultiPlayerCampaign || campaignActive) && !GameMain.Client.GameStarted && GameMain.Client.HasPermission(ClientPermissions.ManageRound); } + + StartButton.Enabled = true; + if (GameSession.ShouldApplyDisembarkPoints(SelectedMode)) + { + StartButton.Enabled = GameSession.ValidatedDisembarkPoints(SelectedMode, MissionTypes); + + StartButton.ToolTip = + !StartButton.Enabled + ? TextManager.Get("DisembarkPointsNotValid") + : string.Empty; + } + } + + public void RefreshChatrow() + { + chatRow.ClearChildren(); + + // Team chat only makes sense when in a team (in "player preference" team selection mode, team assignments only happen at round start) + if (SelectedMode == GameModePreset.PvP && GameMain.Client?.ServerSettings?.PvpTeamSelectionMode == PvpTeamSelectionMode.PlayerChoice + && MultiplayerPreferences.Instance.TeamPreference != CharacterTeamType.None) + { + var chatSelectorRT = new RectTransform(new Vector2(0.25f, 1.0f), chatRow.RectTransform, Anchor.CenterLeft); + chatSelector = new GUIDropDown(chatSelectorRT, elementCount: 2) + { + OnSelected = (_, userdata) => + { + TeamChatSelected = (bool)userdata; + return true; + } + }; + chatSelector.AddItem(TextManager.Get($"PvP.ChatMode.Team"), userData: true, color: ChatMessage.MessageColor[(int)ChatMessageType.Team]); + chatSelector.AddItem(TextManager.Get($"PvP.ChatMode.All"), userData: false, color: ChatMessage.MessageColor[(int)ChatMessageType.Default]); + chatSelector.SelectItem(TeamChatSelected); + } + else + { + TeamChatSelected = false; + } + + chatInput = new GUITextBox(new RectTransform(new Vector2(0.75f, 1.0f), chatRow.RectTransform, Anchor.CenterRight)) + { + MaxTextLength = ChatMessage.MaxLength, + Font = GUIStyle.SmallFont, + DeselectAfterMessage = false + }; + + micIcon = new GUIImage(new RectTransform(new Vector2(0.05f, 1.0f), chatRow.RectTransform), style: "GUIMicrophoneUnavailable"); + + chatInput.Select(); + if (GameMain.Client != null) + { + chatInput.OnEnterPressed = GameMain.Client.EnterChatMessage; + chatInput.OnTextChanged += GameMain.Client.TypingChatMessage; + chatInput.OnDeselected += (sender, key) => + { + GameMain.Client?.ChatBox.ChatManager.Clear(); + }; + ChatManager.RegisterKeys(chatInput, GameMain.Client.ChatBox.ChatManager); + } + + chatRow.Recalculate(); } public void ToggleCampaignMode(bool enabled) @@ -3948,7 +4845,7 @@ namespace Barotrauma { if (button.UserData is not JobVariant jobPrefab) { return false; } - JobInfoFrame = jobPrefab.Prefab.CreateInfoFrame(out GUIComponent buttonContainer); + JobInfoFrame = jobPrefab.Prefab.CreateInfoFrame(isPvP: SelectedMode == GameModePreset.PvP, out GUIComponent buttonContainer); GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Close")) { @@ -3989,15 +4886,15 @@ namespace Barotrauma slot.CanBeFocused = !disableNext; if (slot.UserData is JobVariant jobPrefab) { - var images = AddJobSpritesToGUIComponent(slot, jobPrefab.Prefab, selectedByPlayer: true); + var images = AddJobSpritesToGUIComponent(slot, jobPrefab.Prefab, + team: TeamPreference, + isPvPMode: SelectedMode == GameModePreset.PvP, + selectedByPlayer: true); for (int variantIndex = 0; variantIndex < images.Length; variantIndex++) { - foreach (GUIImage image in images[variantIndex]) - { - //jobPreferenceSprites.Add(image.Sprite); - int selectedVariantIndex = Math.Min(jobPrefab.Variant, images.Length); - image.Visible = images.Length == 1 || selectedVariantIndex == variantIndex; - } + int selectedVariantIndex = Math.Min(jobPrefab.Variant, images.Length); + images[variantIndex].Visible = images.Length == 1 || selectedVariantIndex == variantIndex; + if (images.Length > 1) { var variantButton = CreateJobVariantButton(jobPrefab, variantIndex, images.Length, slot); @@ -4060,7 +4957,7 @@ namespace Barotrauma disableNext = true; } } - GameMain.Client.ForceNameAndJobUpdate(); + GameMain.Client.ForceNameJobTeamUpdate(); if (!MultiplayerPreferences.Instance.AreJobPreferencesEqual(jobPreferences)) { @@ -4121,12 +5018,13 @@ namespace Barotrauma } public FailedSubInfo? FailedSelectedSub; + public FailedSubInfo? FailedSelectedEnemySub; public FailedSubInfo? FailedSelectedShuttle; public List FailedCampaignSubs = new List(); public List FailedOwnedSubs = new List(); - public bool TrySelectSub(string subName, string md5Hash, GUIListBox subList) + public bool TrySelectSub(string subName, string md5Hash, SelectedSubType type, GUIListBox subList, bool showPreview = true) { UpdateSubVisibility(); if (GameMain.Client == null) { return false; } @@ -4144,13 +5042,34 @@ namespace Barotrauma //matching sub found and already selected, all good if (sub != null) { - if (subList == SubList) + if (subList == SubList && showPreview) { - CreateSubPreview(sub); + if (type is not SelectedSubType.EnemySub || MultiplayerPreferences.Instance.TeamPreference == CharacterTeamType.Team2) + { + CreateSubPreview(sub); + } } - if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.StringRepresentation == md5Hash && Barotrauma.IO.File.Exists(sub.FilePath)) + SubmarineInfo selectedSub = type switch { + SelectedSubType.Sub => SelectedSub, + SelectedSubType.EnemySub => SelectedEnemySub, + SelectedSubType.Shuttle => SelectedShuttle, + _ => null + }; + + if (selectedSub != null && selectedSub.MD5Hash?.StringRepresentation == md5Hash && Barotrauma.IO.File.Exists(sub.FilePath)) + { + //ensure the selected sub matches the correct submarineInfo instance (which may have been just downloaded from the server) + switch (type) + { + case SelectedSubType.Sub: + SelectedSub = sub; + break; + case SelectedSubType.EnemySub: + SelectedEnemySub = sub; + break; + } return true; } } @@ -4173,17 +5092,39 @@ namespace Barotrauma else { subList.OnSelected -= VotableClicked; - subList.Select(sub, GUIListBox.Force.Yes); + + var preference = MultiplayerPreferences.Instance.TeamPreference; + switch (type) + { + case SelectedSubType.Sub: + if (preference is CharacterTeamType.Team1 or CharacterTeamType.None) + { + subList.Select(sub); + } + SelectedSub = sub; + break; + case SelectedSubType.EnemySub: + if (preference is CharacterTeamType.Team2) + { + subList.Select(sub); + } + SelectedEnemySub = sub; + break; + } subList.OnSelected += VotableClicked; } - if (subList == SubList) + switch (type) { - FailedSelectedSub = null; - } - else - { - FailedSelectedShuttle = null; + case SelectedSubType.Sub: + FailedSelectedSub = null; + break; + case SelectedSubType.EnemySub: + FailedSelectedEnemySub = null; + break; + case SelectedSubType.Shuttle: + FailedSelectedShuttle = null; + break; } //hashes match, all good @@ -4196,13 +5137,17 @@ namespace Barotrauma //------------------------------------------------------------------------------------- //if we get to this point, a matching sub was not found or it has an incorrect MD5 hash - if (subList == SubList) + switch (type) { - FailedSelectedSub = new FailedSubInfo(subName, md5Hash); - } - else - { - FailedSelectedShuttle = new FailedSubInfo(subName, md5Hash); + case SelectedSubType.Sub: + FailedSelectedSub = new FailedSubInfo(subName, md5Hash); + break; + case SelectedSubType.EnemySub: + FailedSelectedEnemySub = new FailedSubInfo(subName, md5Hash); + break; + case SelectedSubType.Shuttle: + FailedSelectedShuttle = new FailedSubInfo(subName, md5Hash); + break; } LocalizedString errorMsg = ""; @@ -4295,6 +5240,9 @@ namespace Barotrauma } private readonly List visibilityMenuOrder = new List(); + public const string SeparatistsIconUserData = "separatistsIcon"; + public const string CoalitionIconUserData = "coalitionIcon"; + private void CreateSubmarineVisibilityMenu() { var messageBox = new GUIMessageBox(TextManager.Get("SubmarineVisibility"), "", @@ -4402,6 +5350,7 @@ namespace Barotrauma var subName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), frameContent.RectTransform), text: sub.DisplayName) { + UserData = "nametext", CanBeFocused = false }; @@ -4529,6 +5478,7 @@ namespace Barotrauma public void UpdateSubVisibility() { + if (GameMain.Client == null) { return; } foreach (GUIComponent child in SubList.Content.Children) { if (child.UserData is not SubmarineInfo sub) { continue; } @@ -4543,5 +5493,91 @@ namespace Barotrauma { CampaignCharacterDiscarded = false; } + + private const string RoundStartWarningBoxUserData = "RoundStartWarningBox"; + + public void ShowStartRoundWarning(SerializableDateTime waitUntilTime, string team1SubName, ImmutableArray team1IncompatiblePerks, string team2SubName, ImmutableArray team2IncompatiblePerks) + { + DateTime startTime = DateTime.UtcNow; + TimeSpan differenceFromStart = waitUntilTime.ToUtcValue() - startTime; + + StopWaitingForStartRound(); + GUIMessageBox.MessageBoxes.OfType().ForEachMod(static mod => + { + if (mod.UserData is PleaseWaitPopupUserData) + { + mod.Close(); + } + }); + + var messageBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("startgamewarning"), Array.Empty(), relativeSize: new Vector2(0.3f / GUI.AspectRatioAdjustment, 0.4f), minSize: new Point(400, 300)) + { + UserData = RoundStartWarningBoxUserData + }; + + GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.7f), messageBox.Content.RectTransform, Anchor.BottomCenter), isHorizontal: false); + + GUIListBox errorList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), contentLayout.RectTransform)); + + foreach (DisembarkPerkPrefab perk in team1IncompatiblePerks) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), errorList.Content.RectTransform), FormatWarning(perk, team1SubName)); + } + + foreach (DisembarkPerkPrefab perk in team2IncompatiblePerks) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), errorList.Content.RectTransform), FormatWarning(perk, team2SubName)); + } + + GUIProgressBar progress = new GUIProgressBar(new RectTransform(new Vector2(1f, 0.15f), contentLayout.RectTransform), 0.0f, GUIStyle.Orange); + GUITextBlock progressText = new GUITextBlock(new RectTransform(Vector2.One, progress.RectTransform), TextManager.GetWithVariable("startggamewarningprogress", "[seconds]", ((int)differenceFromStart.TotalSeconds).ToString()), textAlignment: Alignment.Center) + { + Shadow = true, + TextColor = Color.White + }; + + new GUICustomComponent(new RectTransform(Vector2.Zero, progress.RectTransform), + onDraw: static (batch, component) => { }, + onUpdate: (f, component) => + { + TimeSpan difference = waitUntilTime.ToUtcValue() - DateTime.UtcNow; + float seconds = (float)difference.TotalSeconds; + + progress.BarSize = seconds / (float)differenceFromStart.TotalSeconds; + + progressText.Text = TextManager.GetWithVariable("startggamewarningprogress", "[seconds]", ((int)seconds).ToString()); + }); + + GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), contentLayout.RectTransform), childAnchor: Anchor.BottomCenter); + GUIButton cancelButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), buttonLayout.RectTransform), TextManager.Get("Cancel")); + + + cancelButton.OnClicked += (button, userData) => + { + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.RESPONSE_CANCEL_STARTGAME); + GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); + messageBox.Close(); + return true; + }; + + static LocalizedString FormatWarning(DisembarkPerkPrefab prefab, string subName) + { + return TextManager.GetWithVariables("startgamewarningformat", + ("[category]", TextManager.Get($"perkcategory.{prefab.SortCategory}")), + ("[perk]", prefab.Name), + ("[submarine]", subName)); + } + } + + public void CloseStartRoundWarning() + { + GUIMessageBox.MessageBoxes.OfType().ForEachMod(static mod => + { + if (mod.UserData is RoundStartWarningBoxUserData) + { + mod.Close(); + } + }); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index 4b3ff25ff..d0d472d2a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -448,7 +448,7 @@ namespace Barotrauma .ToArray(); bool inSelectedCall = false; - languageDropdown.OnSelected = (_, userData) => + languageDropdown.AfterSelected = (_, userData) => { if (inSelectedCall) { return true; } try diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index ae8b70f03..24fc2ae37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -503,8 +503,18 @@ namespace Barotrauma { if (PlayerInput.ScrollWheelSpeed != 0) { - zoom = MathHelper.Clamp(zoom + PlayerInput.ScrollWheelSpeed * (float)deltaTime * 0.05f * zoom, MinZoom, MaxZoom); + float newZoom = MathHelper.Clamp(zoom + PlayerInput.ScrollWheelSpeed * (float)deltaTime * 0.05f * zoom, MinZoom, MaxZoom); + float zoomDeltaPrc = ((newZoom - zoom) / zoom); + zoom = newZoom; zoomBar.BarScroll = GetBarScrollValue(); + + // modify view area offset as well when zooming, to zoom into mouse cursor position + Point mouseToViewAreaScreenCenterDelta = (GetViewArea.Center - viewAreaOffset) - PlayerInput.MousePosition.ToPoint(); + Vector2 mouseDelta = mouseToViewAreaScreenCenterDelta.ToVector2(); + + Vector2 newViewAreaOffset = viewAreaOffset.ToVector2(); + newViewAreaOffset += (mouseDelta + newViewAreaOffset) * zoomDeltaPrc; + viewAreaOffset = newViewAreaOffset.ToPoint(); } widgets.Values.ForEach(w => w.Update((float)deltaTime)); if (PlayerInput.MidButtonHeld()) @@ -516,6 +526,36 @@ namespace Barotrauma } if (GUI.KeyboardDispatcher.Subscriber == null) { + if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.C)) + { + string text = ""; + if (selectedSprites.Count == 1) + { + var selectedSprite = selectedSprites.First(); + if (selectedSprite.SourceElement != null) + { + string sourceRectText = $"sourcerect=\"{XMLExtensions.RectToString(selectedSprite.SourceRect)}\""; + text += $"{sourceRectText}"; + } + } + else + { + foreach (var selectedSprite in selectedSprites) + { + if (selectedSprite.SourceElement == null) { continue; } + XElement xElement = new XElement(selectedSprite.SourceElement.Element); + xElement.SetAttributeValue("sourcerect", XMLExtensions.RectToString(selectedSprite.SourceRect)); + xElement.SetAttributeValue("origin", XMLExtensions.Vector2ToString(selectedSprite.RelativeOrigin)); + text += $"{xElement}"; + if (selectedSprites.Last() != selectedSprite) + { + text += Environment.NewLine; + } + } + } + + Clipboard.SetText(text); + } if (PlayerInput.KeyHit(Keys.Left)) { Nudge(Keys.Left); @@ -568,6 +608,28 @@ namespace Barotrauma { holdTimer = 0; } + + float moveSpeed = 600f * zoom; + float moveSpeedDeltaTime = (float)(moveSpeed * deltaTime); + Vector2 viewOffsetMove = Vector2.Zero; + if (PlayerInput.KeyDown(Keys.W)) + { + viewOffsetMove.Y += moveSpeedDeltaTime; + } + if (PlayerInput.KeyDown(Keys.S)) + { + viewOffsetMove.Y -= moveSpeedDeltaTime; + } + if (PlayerInput.KeyDown(Keys.A)) + { + viewOffsetMove.X += moveSpeedDeltaTime; + } + if (PlayerInput.KeyDown(Keys.D)) + { + viewOffsetMove.X -= moveSpeedDeltaTime; + } + + viewAreaOffset += viewOffsetMove.ToPoint(); } } @@ -963,8 +1025,14 @@ namespace Barotrauma //foreach (Sprite sprite in loadedSprites.OrderBy(s => GetSpriteName(s))) foreach (Sprite sprite in loadedSprites.OrderBy(s => s.SourceElement.GetAttributeContentPath("texture")?.Value ?? string.Empty)) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), spriteList.Content.RectTransform) { MinSize = new Point(0, 20) }, - GetSpriteName(sprite) + " (" + sprite.SourceRect.X + ", " + sprite.SourceRect.Y + ", " + sprite.SourceRect.Width + ", " + sprite.SourceRect.Height + ")") + string elementLocalName = sprite.SourceElement.Element.Name.LocalName; + string text = $"{GetSpriteName(sprite)} ({sprite.SourceRect.X}, {sprite.SourceRect.Y}, {sprite.SourceRect.Width}, {sprite.SourceRect.Height}) [{elementLocalName}]"; + if (string.Equals(elementLocalName, "sprite", StringComparison.InvariantCultureIgnoreCase)) + { + text = $"{GetSpriteName(sprite)} ({sprite.SourceRect.X}, {sprite.SourceRect.Y}, {sprite.SourceRect.Width}, {sprite.SourceRect.Height})"; + } + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), spriteList.Content.RectTransform) { MinSize = new Point(0, 20) }, text: text) { UserData = sprite }; @@ -1005,11 +1073,11 @@ namespace Barotrauma string name = sprite.Name; if (string.IsNullOrWhiteSpace(name)) { - name = sourceElement.Parent.GetAttributeString("identifier", string.Empty); + name = sourceElement.Parent?.GetAttributeString("identifier", string.Empty); } if (string.IsNullOrEmpty(name)) { - name = sourceElement.Parent.GetAttributeString("name", string.Empty); + name = sourceElement.Parent?.GetAttributeString("name", string.Empty); } return string.IsNullOrEmpty(name) ? Path.GetFileNameWithoutExtension(sprite.FilePath.Value) : name; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 008b62f46..03870cdd9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -11,6 +11,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Xml.Linq; +using Barotrauma.Sounds; namespace Barotrauma { @@ -20,7 +21,7 @@ namespace Barotrauma public const int MaxWalls = 500; public const int MaxItems = 5000; public const int MaxLights = 600; - public const int MaxShadowCastingLights = 60; + public const int MaxShadowCastingLights = 100; private static Submarine MainSub { @@ -46,6 +47,7 @@ namespace Barotrauma NoBallastTag, NonLinkedGaps, NoHiddenContainers, + InsufficientFreeConnectionsWarning, StructureCount, WallCount, ItemCount, @@ -1071,9 +1073,16 @@ namespace Barotrauma backedUpSubInfo = new SubmarineInfo(MainSub); + GameSession gameSession = new GameSession(backedUpSubInfo, Option.None, CampaignDataPath.Empty, GameModePreset.TestMode, CampaignSettings.Empty, null); + + // if testing an outpost module, we will generate an entire outpost in place of the main submarine down the line + if (backedUpSubInfo.OutpostModuleInfo != null) + { + gameSession.ForceOutpostModule = new SubmarineInfo(MainSub); + } + GameMain.GameScreen.Select(); - - GameSession gameSession = new GameSession(backedUpSubInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); + gameSession.StartRound(null, false); foreach ((string layerName, LayerData layerData) in Layers) @@ -1382,10 +1391,12 @@ namespace Barotrauma GUI.PreventPauseMenuToggle = false; if (!Directory.Exists(autoSavePath)) { - System.IO.DirectoryInfo e = Directory.CreateDirectory(autoSavePath); - e.Attributes = System.IO.FileAttributes.Directory | System.IO.FileAttributes.Hidden; - if (!e.Exists) + if (Directory.CreateDirectory(autoSavePath, catchUnauthorizedAccessExceptions: true) is { Exists: true } directoryInfo) { + directoryInfo.Attributes = System.IO.FileAttributes.Directory | System.IO.FileAttributes.Hidden; + } + else + { DebugConsole.ThrowError("Failed to create auto save directory!"); } } @@ -1395,7 +1406,7 @@ namespace Barotrauma try { AutoSaveInfo = new XDocument(new XElement("AutoSaves")); - IO.SafeXML.SaveSafe(AutoSaveInfo, autoSaveInfoPath); + AutoSaveInfo.SaveSafe(autoSaveInfoPath, throwExceptions: true); } catch (Exception e) { @@ -1456,8 +1467,8 @@ namespace Barotrauma MainSub.UpdateTransform(interpolate: false); cam.Position = MainSub.Position + MainSub.HiddenSubPosition; - GameMain.SoundManager.SetCategoryGainMultiplier("default", 0.0f); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f); + GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryDefault, 0.0f); + GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryWaterAmbience, 0.0f); string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); linkedSubBox.ClearChildren(); @@ -1617,8 +1628,8 @@ namespace Barotrauma SetMode(Mode.Default); SoundPlayer.OverrideMusicType = Identifier.Empty; - GameMain.SoundManager.SetCategoryGainMultiplier("default", GameSettings.CurrentConfig.Audio.SoundVolume); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameSettings.CurrentConfig.Audio.SoundVolume); + GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryDefault, GameSettings.CurrentConfig.Audio.SoundVolume); + GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryWaterAmbience, GameSettings.CurrentConfig.Audio.SoundVolume); if (CoroutineManager.IsCoroutineRunning("SubEditorAutoSave")) { @@ -1788,6 +1799,21 @@ namespace Barotrauma nameBox.Flash(); return false; } + + // when creating a new local package, prevent name clashes with existing packages + if (packageToSaveTo == null) + { + var subFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()); + + var nameConflictFile = subFiles.FirstOrDefault(file => Path.GetFileNameWithoutExtension(file.Path.Value).Equals(nameBox.Text, StringComparison.InvariantCultureIgnoreCase)); + + if (nameConflictFile != null) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("subeditor.duplicatefilenameerror", "[packagename]", nameConflictFile.ContentPackage.Name)); + return false; + } + } if (MainSub.Info.Type != SubmarineType.Player) { @@ -2060,7 +2086,7 @@ namespace Barotrauma saveFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.65f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 500) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.7f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 500) }); var paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; var columnArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), paddedSaveFrame.RectTransform), isHorizontal: true) { RelativeSpacing = 0.02f, Stretch = true }; @@ -2162,17 +2188,15 @@ namespace Barotrauma layerVisibilityDropDown.SelectItem(layerName); } } - layerVisibilityDropDown.OnSelected += (button, obj) => + layerVisibilityDropDown.AfterSelected += (button, _) => { - string layerName = (string)obj; - bool isVisible = layerVisibilityDropDown.SelectedDataMultiple.Contains(obj); - if (isVisible) + MainSub.Info.LayersHiddenByDefault.Clear(); + foreach (var layer in Layers) { - MainSub.Info.LayersHiddenByDefault.Remove(layerName.ToIdentifier()); - } - else - { - MainSub.Info.LayersHiddenByDefault.Add(layerName.ToIdentifier()); + if (!layerVisibilityDropDown.SelectedDataMultiple.Contains(layer.Key)) + { + MainSub.Info.LayersHiddenByDefault.Add(layer.Key.ToIdentifier()); + } } UpdateLayerPanel(); layerVisibilityDropDown.Text = ToolBox.LimitString(layerVisibilityDropDown.Text.Value, layerVisibilityDropDown.Font, layerVisibilityDropDown.Rect.Width); @@ -2183,9 +2207,9 @@ namespace Barotrauma //--------------------------------------- - var subTypeDependentSettingFrame = new GUIFrame(new RectTransform((1.0f, 0.5f), leftColumn.RectTransform), style: "InnerFrame"); + var subTypeDependentSettingFrame = new GUIFrame(new RectTransform((1.0f, 0.6f), leftColumn.RectTransform), style: "InnerFrame"); - var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) + var outpostModuleSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) { CanBeFocused = true, Visible = false, @@ -2194,7 +2218,7 @@ namespace Barotrauma // module flags --------------------- - var outpostModuleGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var outpostModuleGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), TextManager.Get("outpostmoduletype"), textAlignment: Alignment.CenterLeft); HashSet availableFlags = new HashSet(); @@ -2209,7 +2233,13 @@ namespace Barotrauma availableFlags.Add(flag); } } - + if (MainSub?.Info?.OutpostModuleInfo is { } moduleInfo) + { + foreach (var moduleType in moduleInfo.ModuleFlags) + { + availableFlags.Add(moduleType); + } + } var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.ModuleFlags.Select(s => TextManager.Capitalize(s.Value)) ?? ((LocalizedString)"None").ToEnumerable()), selectMultiple: true); foreach (Identifier flag in availableFlags.OrderBy(f => f.Value, StringComparer.InvariantCultureIgnoreCase)) @@ -2221,7 +2251,7 @@ namespace Barotrauma moduleTypeDropDown.SelectItem(flag); } } - moduleTypeDropDown.OnSelected += (_, __) => + moduleTypeDropDown.AfterSelected += (_, __) => { if (MainSub?.Info?.OutpostModuleInfo == null) { return false; } MainSub.Info.OutpostModuleInfo.SetFlags(moduleTypeDropDown.SelectedDataMultiple.Cast()); @@ -2232,9 +2262,34 @@ namespace Barotrauma }; outpostModuleGroup.RectTransform.MinSize = new Point(0, outpostModuleGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + var addTypeGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), addTypeGroup.RectTransform), TextManager.Get("leveleditor.addmoduletype"), textAlignment: Alignment.CenterLeft); + var textBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1f), addTypeGroup.RectTransform)); + + new GUIButton(new RectTransform(new Vector2(0.1f, 0.9f), addTypeGroup.RectTransform), text: "+", style: "GUIButtonSmallFreeScale") + { + OnClicked = (btn, _) => + { + if (textBox.Text.IsNullOrEmpty()) + { + textBox.Flash(); + return false; + } + if (MainSub?.Info?.OutpostModuleInfo is { } moduleInfo) + { + moduleInfo.SetFlags(moduleInfo.ModuleFlags.Append(textBox.Text.ToIdentifier()).ToList()); + //refresh + saveFrame = null; + CreateSaveScreen(); + } + return true; + } + }; + addTypeGroup.RectTransform.MinSize = new Point(0, addTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + // module flags --------------------- - var allowAttachGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var allowAttachGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform), TextManager.Get("outpostmoduleallowattachto"), textAlignment: Alignment.CenterLeft); @@ -2257,7 +2312,7 @@ namespace Barotrauma allowAttachDropDown.SelectItem(flag); } } - allowAttachDropDown.OnSelected += (_, __) => + allowAttachDropDown.AfterSelected += (_, __) => { if (MainSub?.Info?.OutpostModuleInfo == null) { return false; } MainSub.Info.OutpostModuleInfo.SetAllowAttachTo(allowAttachDropDown.SelectedDataMultiple.Cast()); @@ -2270,7 +2325,7 @@ namespace Barotrauma // location types --------------------- - var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); HashSet availableLocationTypes = new HashSet(); @@ -2290,7 +2345,7 @@ namespace Barotrauma } if (!MainSub.Info?.OutpostModuleInfo?.AllowedLocationTypes?.Any() ?? true) { locationTypeDropDown.SelectItem("any".ToIdentifier()); } - locationTypeDropDown.OnSelected += (_, __) => + locationTypeDropDown.AfterSelected += (_, __) => { MainSub?.Info?.OutpostModuleInfo?.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text.Value, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); @@ -2300,7 +2355,7 @@ namespace Barotrauma // gap positions --------------------- - var gapPositionGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var gapPositionGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), TextManager.Get("outpostmodulegappositions"), textAlignment: Alignment.CenterLeft); var gapPositionDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), text: "", selectMultiple: true); @@ -2323,7 +2378,7 @@ namespace Barotrauma } } - gapPositionDropDown.OnSelected += (_, __) => + gapPositionDropDown.AfterSelected += (_, __) => { if (MainSub.Info?.OutpostModuleInfo == null) { return false; } MainSub.Info.OutpostModuleInfo.GapPositions = OutpostModuleInfo.GapPosition.None; @@ -2345,7 +2400,7 @@ namespace Barotrauma }; gapPositionGroup.RectTransform.MinSize = new Point(0, gapPositionGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - var canAttachToPrevGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var canAttachToPrevGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), canAttachToPrevGroup.RectTransform), TextManager.Get("canattachtoprevious"), textAlignment: Alignment.CenterLeft) { ToolTip = TextManager.Get("canattachtoprevious.tooltip") @@ -2365,7 +2420,7 @@ namespace Barotrauma } } - canAttachToPrevDropDown.OnSelected += (_, __) => + canAttachToPrevDropDown.AfterSelected += (_, __) => { if (Submarine.MainSub.Info?.OutpostModuleInfo == null) { return false; } Submarine.MainSub.Info.OutpostModuleInfo.CanAttachToPrevious = OutpostModuleInfo.GapPosition.None; @@ -2390,7 +2445,7 @@ namespace Barotrauma // ------------------- - var maxModuleCountGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), outpostSettingsContainer.RectTransform), isHorizontal: true) + var maxModuleCountGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true }; @@ -2410,8 +2465,9 @@ namespace Barotrauma MainSub.Info.OutpostModuleInfo.MaxCount = numberInput.IntValue; } }; + maxModuleCountGroup.RectTransform.MinSize = new Point(0, maxModuleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - var commonnessGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), outpostSettingsContainer.RectTransform), isHorizontal: true) + var commonnessGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true }; @@ -2427,7 +2483,9 @@ namespace Barotrauma MainSub.Info.OutpostModuleInfo.Commonness = numberInput.FloatValue; } }; - outpostSettingsContainer.RectTransform.MinSize = new Point(0, outpostSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); + commonnessGroup.RectTransform.MinSize = new Point(0, commonnessGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + + outpostModuleSettingsContainer.RectTransform.MinSize = new Point(0, outpostModuleSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); //--------------------------------------- @@ -2476,6 +2534,108 @@ namespace Barotrauma //--------------------------------------- + var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) + { + CanBeFocused = true, + Visible = false, + Stretch = true + }; + + var outpostTagsGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), outpostSettingsContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), outpostTagsGroup.RectTransform), + TextManager.Get("sp.item.tags.name"), textAlignment: Alignment.CenterLeft, wrap: true); + var outpostTagsBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), outpostTagsGroup.RectTransform)) + { + OnEnterPressed = (GUITextBox textBox, string text) => + { + MainSub.Info.OutpostTags = text.ToIdentifiers().ToImmutableHashSet(); + return true; + }, + OverflowClip = true, + Text = "default" + }; + outpostTagsBox.OnDeselected += (textbox, _) => + { + MainSub.Info.OutpostTags = outpostTagsBox.Text.ToIdentifiers().ToImmutableHashSet(); + }; + if (MainSub.Info.OutpostTags != null) + { + outpostTagsBox.Text = MainSub.Info.OutpostTags.ConvertToString(); + } + + outpostTagsGroup.RectTransform.MaxSize = outpostTagsBox.RectTransform.MaxSize; + + //--------------------------------------- + + var enemySubmarineSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) + { + CanBeFocused = true, + Visible = false, + Stretch = true + }; + + // ------------------- + + var enemySubmarineRewardGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), enemySubmarineSettingsContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), enemySubmarineRewardGroup.RectTransform), + TextManager.Get("enemysub.reward"), textAlignment: Alignment.CenterLeft, wrap: true); + numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), enemySubmarineRewardGroup.RectTransform), NumberType.Int, buttonVisibility: GUINumberInput.ButtonVisibility.ForceHidden) + { + IntValue = (int)(MainSub?.Info?.EnemySubmarineInfo?.Reward ?? 4000), + MinValueInt = 0, + MaxValueInt = 999999, + OnValueChanged = (numberInput) => + { + MainSub.Info.EnemySubmarineInfo.Reward = numberInput.IntValue; + } + }; + enemySubmarineRewardGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; + var enemySubmarineDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), enemySubmarineSettingsContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), enemySubmarineDifficultyGroup.RectTransform), + TextManager.Get("preferreddifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); + numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), enemySubmarineDifficultyGroup.RectTransform), NumberType.Int) + { + IntValue = (int)(MainSub?.Info?.EnemySubmarineInfo?.PreferredDifficulty ?? 50), + MinValueInt = 0, + MaxValueInt = 100, + OnValueChanged = (numberInput) => + { + MainSub.Info.EnemySubmarineInfo.PreferredDifficulty = numberInput.IntValue; + } + }; + enemySubmarineDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; + var enemySubmarineTagsGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), enemySubmarineSettingsContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), enemySubmarineTagsGroup.RectTransform), + TextManager.Get("sp.item.tags.name"), textAlignment: Alignment.CenterLeft, wrap: true); + var tagsBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), enemySubmarineTagsGroup.RectTransform)) + { + OnEnterPressed = ChangeEnemySubTags, + OverflowClip = true, + Text = "default" + }; + tagsBox.OnDeselected += (textbox, _) => ChangeEnemySubTags(textbox, textbox.Text); + if (MainSub?.Info?.EnemySubmarineInfo?.MissionTags != null) + { + tagsBox.Text = string.Join(',', MainSub.Info.EnemySubmarineInfo.MissionTags); + } + + enemySubmarineTagsGroup.RectTransform.MaxSize = tagsBox.RectTransform.MaxSize; + enemySubmarineSettingsContainer.RectTransform.MinSize = new Point(0, enemySubmarineSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); + + //-------------------------------------------------------- + var beaconSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, extraSettingsContainer.RectTransform)) { CanBeFocused = true, @@ -2530,7 +2690,7 @@ namespace Barotrauma Stretch = true }; - var priceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) + var priceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true }; @@ -2554,7 +2714,7 @@ namespace Barotrauma MainSub.Info.Price = Math.Max(MainSub.Info.Price, basePrice); } - var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; @@ -2587,7 +2747,7 @@ namespace Barotrauma }; classDropDown.SelectItem(!MainSub.Info.HasTag(SubmarineTag.Shuttle) ? MainSub.Info.SubmarineClass : (object)SubmarineTag.Shuttle); - var tierGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) + var tierGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true }; @@ -2612,14 +2772,14 @@ namespace Barotrauma MainSub.Info.Tier = Math.Clamp(MainSub.Info.Tier, 1, SubmarineInfo.HighestTier); } - var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) + var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true, AbsoluteSpacing = 5 }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), crewSizeArea.RectTransform), - TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont); + TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.CenterLeft, wrap: true); var crewSizeMin = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), NumberType.Int, relativeButtonAreaWidth: 0.25f) { MinValueInt = 1, @@ -2646,14 +2806,14 @@ namespace Barotrauma MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; }; - var crewExpArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) + var crewExpArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true, AbsoluteSpacing = 5 }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), crewExpArea.RectTransform), - TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont); + TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.CenterLeft, wrap: true); var toggleExpLeft = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), crewExpArea.RectTransform), style: "GUIButtonToggleLeft"); var experienceText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), crewExpArea.RectTransform), @@ -2682,13 +2842,13 @@ namespace Barotrauma return true; }; - var hideInMenusArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + var hideInMenusArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, AbsoluteSpacing = 5 }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), hideInMenusArea.RectTransform), - TextManager.Get("HideInMenus"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont); + TextManager.Get("HideInMenus"), textAlignment: Alignment.CenterLeft, wrap: true); new GUITickBox(new RectTransform((0.4f, 1.0f), hideInMenusArea.RectTransform), "") { @@ -2707,13 +2867,13 @@ namespace Barotrauma } }; - var outFittingArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + var outFittingArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, AbsoluteSpacing = 5 }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), outFittingArea.RectTransform), - TextManager.Get("ManuallyOutfitted"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont) + TextManager.Get("ManuallyOutfitted"), textAlignment: Alignment.CenterLeft, wrap: true) { ToolTip = TextManager.Get("manuallyoutfittedtooltip") }; @@ -2757,15 +2917,27 @@ namespace Barotrauma { MainSub.Info.WreckInfo ??= new WreckInfo(MainSub.Info); } + else if (type == SubmarineType.EnemySubmarine) + { + MainSub.Info.EnemySubmarineInfo ??= new EnemySubmarineInfo(MainSub.Info); + } previewImageButtonHolder.Children.ForEach(c => c.Enabled = MainSub.Info.AllowPreviewImage); - outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; + outpostModuleSettingsContainer.Visible = type == SubmarineType.OutpostModule; extraSettingsContainer.Visible = type == SubmarineType.BeaconStation || type == SubmarineType.Wreck; beaconSettingsContainer.Visible = type == SubmarineType.BeaconStation; + enemySubmarineSettingsContainer.Visible = type == SubmarineType.EnemySubmarine; subSettingsContainer.Visible = type == SubmarineType.Player; + outpostSettingsContainer.Visible = type == SubmarineType.Outpost; return true; }; subSettingsContainer.RectTransform.MinSize = new Point(0, subSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); + int minHeight = subSettingsContainer.Children.First().Children.Max(c => c.RectTransform.MinSize.Y); + foreach (var child in subSettingsContainer.Children) + { + child.RectTransform.MinSize = new Point(0, minHeight); + } + // right column --------------------------------------------------- new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("SubPreviewImage"), font: GUIStyle.SubHeadingFont); @@ -3036,11 +3208,12 @@ namespace Barotrauma paddedSaveFrame.Recalculate(); leftColumn.Recalculate(); - subSettingsContainer.RectTransform.MinSize = outpostSettingsContainer.RectTransform.MinSize = beaconSettingsContainer.RectTransform.MinSize = - new Point(0, Math.Max(subSettingsContainer.Rect.Height, outpostSettingsContainer.Rect.Height)); + subSettingsContainer.RectTransform.MinSize = outpostModuleSettingsContainer.RectTransform.MinSize = beaconSettingsContainer.RectTransform.MinSize = + new Point(0, Math.Max(subSettingsContainer.Rect.Height, outpostModuleSettingsContainer.Rect.Height)); subSettingsContainer.Recalculate(); - outpostSettingsContainer.Recalculate(); + outpostModuleSettingsContainer.Recalculate(); beaconSettingsContainer.Recalculate(); + enemySubmarineSettingsContainer.Recalculate(); descriptionBox.Text = MainSub == null ? "" : MainSub.Info.Description.Value; submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; @@ -3344,7 +3517,7 @@ namespace Barotrauma }; searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = sender.Text.IsNullOrEmpty(); }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; var sortedSubs = GetLoadableSubs() @@ -3542,7 +3715,7 @@ namespace Barotrauma /// private void LoadAutoSave(object userData) { - if (!(userData is XElement element)) { return; } + if (userData is not XElement element) { return; } #warning TODO: revise string filePath = element.GetAttributeStringUnrestricted("file", ""); @@ -3566,6 +3739,8 @@ namespace Barotrauma MainSub.Info.Name = loadedSub.Info.Name; subNameLabel.Text = ToolBox.LimitString(loadedSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); + ReconstructLayers(); + CreateDummyCharacter(); cam.Position = MainSub.Position + MainSub.HiddenSubPosition; @@ -3769,10 +3944,10 @@ namespace Barotrauma { if (subPackage != null) { - File.Delete(sub.FilePath); + File.Delete(sub.FilePath, catchUnauthorizedAccessExceptions: false); ModProject modProject = new ModProject(subPackage); modProject.RemoveFile(modProject.Files.First(f => ContentPath.FromRaw(subPackage, f.Path) == sub.FilePath)); - modProject.Save(subPackage.Path); + modProject.Save(subPackage.Path, catchUnauthorizedAccessExceptions: true); ReloadModifiedPackage(subPackage); if (MainSub?.Info != null && MainSub.Info.FilePath == sub.FilePath) { @@ -4458,7 +4633,7 @@ namespace Barotrauma private bool SelectLinkedSub(GUIComponent selected, object userData) { - if (!(selected.UserData is SubmarineInfo submarine)) return false; + if (userData is not SubmarineInfo submarine) { return false; } var prefab = new LinkedSubmarinePrefab(submarine); MapEntityPrefab.SelectPrefab(prefab); return true; @@ -4573,11 +4748,32 @@ namespace Barotrauma return false; } - if (MainSub != null) MainSub.Info.Name = text; + if (MainSub != null) { MainSub.Info.Name = text; } textBox.Deselect(); - textBox.Text = text; + textBox.Flash(GUIStyle.Green); + return true; + } + + private bool ChangeEnemySubTags(GUITextBox textBox, string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + textBox.Flash(GUIStyle.Red); + return false; + } + + if (MainSub.Info.EnemySubmarineInfo is { } enemySubInfo) + { + enemySubInfo.MissionTags.Clear(); + string[] tags = text.Split(','); + foreach (string tag in tags) + { + enemySubInfo.MissionTags.Add(tag.ToIdentifier()); + } + } + textBox.Text = text; textBox.Flash(GUIStyle.Green); return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index 115402131..b68beafc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Linq; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; @@ -49,7 +49,7 @@ namespace Barotrauma } dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); - dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.FirstOrDefault(static jp => jp.Identifier == "captain")); + dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.FirstOrDefault(static jp => jp.Identifier == "captain"), isPvP: false); dummyCharacter.Info.Name = "Galldren"; dummyCharacter.Inventory.CreateSlots(); dummyCharacter.Info.GiveExperience(999999); @@ -59,6 +59,9 @@ namespace Barotrauma Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); + + dummyCharacter.Info.TalentRefundPoints = 2; + TabMenu = new TabMenu(); } public override void AddToGUIUpdateList() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index fffd275a6..668b1bde8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -748,7 +748,7 @@ namespace Barotrauma } } enumDropDown.MustSelectAtLeastOne = !hasNoneOption; - enumDropDown.OnSelected += (selected, val) => + enumDropDown.AfterSelected += (selected, val) => { if (SetPropertyValue(property, entity, string.Join(", ", enumDropDown.SelectedDataMultiple.Select(d => d.ToString())))) { @@ -788,8 +788,12 @@ namespace Barotrauma ToolTip = toolTip, Font = GUIStyle.SmallFont, Text = StripPrefabTags(value), - OverflowClip = true + OverflowClip = true, }; + if (editableAttribute != null && editableAttribute.MaxLength > 0) + { + propertyBox.MaxTextLength = editableAttribute.MaxLength; + } HashSet editedEntities = new HashSet(); propertyBox.OnTextChanged += (textBox, text) => @@ -805,8 +809,7 @@ namespace Barotrauma refresh += () => { if (propertyBox.Selected) { return; } - - propertyBox.Text = StripPrefabTags(property.GetValue(entity).ToString()); + propertyBox.Text = StripPrefabTags(property.GetValue(entity)?.ToString()); }; bool OnApply(GUITextBox textBox) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 0e5e54d1c..44dcd3c79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -7,6 +7,7 @@ using System.Linq; using Barotrauma.Eos; using Barotrauma.Extensions; using Barotrauma.Networking; +using Barotrauma.Sounds; using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -281,8 +282,8 @@ namespace Barotrauma Spacer(right); Label(right, TextManager.Get("VisibleLightLimit"), GUIStyle.SubHeadingFont); - Slider(right, (10, 210), 21, v => v > 200 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit, - v => unsavedConfig.Graphics.VisibleLightLimit = v > 200 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip")); + Slider(right, (10, 510), 21, v => v > 500 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit, + v => unsavedConfig.Graphics.VisibleLightLimit = v > 500 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip")); Spacer(right); Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, v => unsavedConfig.Graphics.RadialDistortion = v); @@ -403,20 +404,37 @@ namespace Barotrauma Spacer(audio); Label(audio, TextManager.Get("SoundVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, v => unsavedConfig.Audio.SoundVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, v => + { + unsavedConfig.Audio.SoundVolume = v; + GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryDefault, v); + GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryWaterAmbience, v); + }); Label(audio, TextManager.Get("MusicVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, v => unsavedConfig.Audio.MusicVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, v => + { + unsavedConfig.Audio.MusicVolume = v; + GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryMusic, v); + }); Label(audio, TextManager.Get("UiSoundVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, v => unsavedConfig.Audio.UiVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, v => + { + unsavedConfig.Audio.UiVolume = v; + GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryUi, v); + }); Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, v => unsavedConfig.Audio.MuteOnFocusLost = v); Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, v => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); Spacer(audio); Label(audio, TextManager.Get("VoiceChatVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, v => unsavedConfig.Audio.VoiceChatVolume = v); + Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, v => + { + unsavedConfig.Audio.VoiceChatVolume = v; + GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryVoip, v); + }); Tickbox(audio, TextManager.Get("DirectionalVoiceChat"), TextManager.Get("DirectionalVoiceChatTooltip"), unsavedConfig.Audio.UseDirectionalVoiceChat, v => unsavedConfig.Audio.UseDirectionalVoiceChat = v); Tickbox(audio, TextManager.Get("VoipAttenuation"), TextManager.Get("VoipAttenuationTooltip"), unsavedConfig.Audio.VoipAttenuationEnabled, v => unsavedConfig.Audio.VoipAttenuationEnabled = v); @@ -832,6 +850,8 @@ namespace Barotrauma { OnClicked = (btn, obj) => { + // reset any modified audio settings to current config + GameMain.SoundManager?.ApplySettings(); Close(); return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index f5145ff10..03490b2c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -122,7 +122,7 @@ namespace Barotrauma.Sounds static void MuffleBuffer(float[] buffer, int sampleRate) { - var filter = new LowpassFilter(sampleRate, 1600); + var filter = new LowpassFilter(sampleRate, SoundPlayer.MuffleFilterFrequency); filter.Process(buffer); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index d90705fdb..3c53de54c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Sounds private set; } - public SoundSourcePool(int sourceCount = SoundManager.SOURCE_COUNT) + public SoundSourcePool(int sourceCount = SoundManager.SourceCount) { int alError; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 1c55f6070..c5745b5dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -11,7 +11,12 @@ namespace Barotrauma.Sounds { class SoundManager : IDisposable { - public const int SOURCE_COUNT = 32; + public const int SourceCount = 32; + public const string SoundCategoryDefault = "default"; + public const string SoundCategoryUi = "ui"; + public const string SoundCategoryWaterAmbience = "waterambience"; + public const string SoundCategoryMusic = "music"; + public const string SoundCategoryVoip = "voip"; public bool Disabled { @@ -204,7 +209,7 @@ namespace Barotrauma.Sounds updateChannelsThread = null; sourcePools = new SoundSourcePool[2]; - playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT]; + playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SourceCount]; playingChannels[(int)SourcePoolIndex.Voice] = new SoundChannel[16]; string deviceName = GameSettings.CurrentConfig.Audio.AudioOutputDevice; @@ -334,7 +339,7 @@ namespace Barotrauma.Sounds return false; } - sourcePools[(int)SourcePoolIndex.Default] = new SoundSourcePool(SOURCE_COUNT); + sourcePools[(int)SourcePoolIndex.Default] = new SoundSourcePool(SourceCount); sourcePools[(int)SourcePoolIndex.Voice] = new SoundSourcePool(16); ReloadSounds(); @@ -353,11 +358,19 @@ namespace Barotrauma.Sounds throw new System.IO.FileNotFoundException("Sound file \"" + filename + "\" doesn't exist!"); } +#if DEBUG + System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); + sw.Start(); +#endif Sound newSound = new OggSound(this, filename, stream, null); lock (loadedSounds) { loadedSounds.Add(newSound); } +#if DEBUG + sw.Stop(); + System.Diagnostics.Debug.WriteLine($"Loaded sound \"{filename}\" ({sw.ElapsedMilliseconds} ms)."); +#endif return newSound; } @@ -672,10 +685,10 @@ namespace Barotrauma.Sounds { voipAttenuatedGain = 1.0f; } - SetCategoryGainMultiplier("default", VoipAttenuatedGain, 1); - SetCategoryGainMultiplier("ui", VoipAttenuatedGain, 1); - SetCategoryGainMultiplier("waterambience", VoipAttenuatedGain, 1); - SetCategoryGainMultiplier("music", VoipAttenuatedGain, 1); + SetCategoryGainMultiplier(SoundCategoryDefault, VoipAttenuatedGain, 1); + SetCategoryGainMultiplier(SoundCategoryUi, VoipAttenuatedGain, 1); + SetCategoryGainMultiplier(SoundCategoryWaterAmbience, VoipAttenuatedGain, 1); + SetCategoryGainMultiplier(SoundCategoryMusic, VoipAttenuatedGain, 1); if (GameSettings.CurrentConfig.Audio.DynamicRangeCompressionEnabled) { @@ -721,11 +734,11 @@ namespace Barotrauma.Sounds public void ApplySettings() { - SetCategoryGainMultiplier("default", GameSettings.CurrentConfig.Audio.SoundVolume, 0); - SetCategoryGainMultiplier("ui", GameSettings.CurrentConfig.Audio.UiVolume, 0); - SetCategoryGainMultiplier("waterambience", GameSettings.CurrentConfig.Audio.SoundVolume, 0); - SetCategoryGainMultiplier("music", GameSettings.CurrentConfig.Audio.MusicVolume, 0); - SetCategoryGainMultiplier("voip", Math.Min(GameSettings.CurrentConfig.Audio.VoiceChatVolume, 1.0f), 0); + SetCategoryGainMultiplier(SoundCategoryDefault, GameSettings.CurrentConfig.Audio.SoundVolume, 0); + SetCategoryGainMultiplier(SoundCategoryUi, GameSettings.CurrentConfig.Audio.UiVolume, 0); + SetCategoryGainMultiplier(SoundCategoryWaterAmbience, GameSettings.CurrentConfig.Audio.SoundVolume, 0); + SetCategoryGainMultiplier(SoundCategoryMusic, GameSettings.CurrentConfig.Audio.MusicVolume, 0); + SetCategoryGainMultiplier(SoundCategoryVoip, Math.Min(GameSettings.CurrentConfig.Audio.VoiceChatVolume, 1.0f), 0); } /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index eaee24b70..d3e15983a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -1,12 +1,11 @@ using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Sounds; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; using System.Threading; -using System.Xml.Linq; namespace Barotrauma { @@ -16,6 +15,8 @@ namespace Barotrauma private const float MusicLerpSpeed = 1.0f; private const float UpdateMusicInterval = 5.0f; + public const float MuffleFilterFrequency = 600; + const int MaxMusicChannels = 6; private readonly static BackgroundMusic[] currentMusic = new BackgroundMusic[MaxMusicChannels]; @@ -28,9 +29,9 @@ namespace Barotrauma private static float updateMusicTimer; //ambience - private static Sound waterAmbienceIn => SoundPrefab.WaterAmbienceIn.ActivePrefab.Sound; - private static Sound waterAmbienceOut => SoundPrefab.WaterAmbienceOut.ActivePrefab.Sound; - private static Sound waterAmbienceMoving => SoundPrefab.WaterAmbienceMoving.ActivePrefab.Sound; + private static SoundPrefab waterAmbienceIn => SoundPrefab.WaterAmbienceIn.ActivePrefab; + private static SoundPrefab waterAmbienceOut => SoundPrefab.WaterAmbienceOut.ActivePrefab; + private static SoundPrefab waterAmbienceMoving => SoundPrefab.WaterAmbienceMoving.ActivePrefab; private static readonly HashSet waterAmbienceChannels = new HashSet(); private static float ambientSoundTimer; @@ -213,9 +214,9 @@ namespace Barotrauma } } - updateWaterAmbience(waterAmbienceIn, ambienceVolume * (1.0f - movementSoundVolume) * insideSubFactor); - updateWaterAmbience(waterAmbienceMoving, ambienceVolume * movementSoundVolume * insideSubFactor); - updateWaterAmbience(waterAmbienceOut, 1.0f - insideSubFactor); + updateWaterAmbience(waterAmbienceIn.Sound, ambienceVolume * (1.0f - movementSoundVolume) * insideSubFactor * waterAmbienceIn.Volume); + updateWaterAmbience(waterAmbienceMoving.Sound, ambienceVolume * movementSoundVolume * insideSubFactor * waterAmbienceMoving.Volume); + updateWaterAmbience(waterAmbienceOut.Sound, (1.0f - insideSubFactor) * waterAmbienceOut.Volume); } private static void UpdateWaterFlowSounds(float deltaTime) @@ -956,7 +957,7 @@ namespace Barotrauma PlayDamageSound(damageType, damage, bodyPosition, 800.0f); } - public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable tags = null) + public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable tags = null, float gain = 1.0f) { var suitableSounds = damageSounds.Where(s => s.DamageType == damageType && @@ -972,7 +973,7 @@ namespace Barotrauma s.DamageRange == Vector2.Zero || (randomizedDamage >= s.DamageRange.X && randomizedDamage <= s.DamageRange.Y)); var damageSound = suitableSounds.GetRandomUnsynced(); - damageSound?.Sound?.Play(1.0f, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); + damageSound?.Sound?.Play(gain, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); } public static void PlayUISound(GUISoundType soundType) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs index f6c8a2beb..9861b961f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -183,6 +183,9 @@ namespace Barotrauma public readonly ContentPath SoundPath; public readonly ContentXElement Element; public readonly Identifier ElementName; + + public readonly float Volume; + public Sound Sound { get; private set; } public SoundPrefab(ContentXElement element, SoundsFile file, bool stream = false) : base(file, element) @@ -191,6 +194,8 @@ namespace Barotrauma Element = element; ElementName = element.NameAsIdentifier(); Sound = GameMain.SoundManager.LoadSound(element, stream: stream); + + Volume = element.GetAttributeFloat(nameof(Volume), 1.0f); } public bool IsPlaying() @@ -235,7 +240,6 @@ namespace Barotrauma public readonly Identifier Type; public readonly bool DuckVolume; - public readonly float Volume; public readonly Vector2 IntensityRange; public readonly bool MuteIntensityTracks; @@ -255,7 +259,6 @@ namespace Barotrauma { ForceIntensityTrack = element.GetAttributeFloat(nameof(ForceIntensityTrack), 0.0f); } - Volume = element.GetAttributeFloat(nameof(Volume), 1.0f); StartFromRandomTime = element.GetAttributeBool(nameof(StartFromRandomTime), false); ContinueFromPreviousTime = element.GetAttributeBool(nameof(ContinueFromPreviousTime), false); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index bbd74e417..54d26f5cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -12,6 +12,7 @@ namespace Barotrauma { public float RotationState; public float OffsetState; + public float ScaleState; public Vector2 RandomOffsetMultiplier = new Vector2(Rand.Range(-1.0f, 1.0f), Rand.Range(-1.0f, 1.0f)); public float RandomRotationFactor = Rand.Range(0.0f, 1.0f); public float RandomScaleFactor = Rand.Range(0.0f, 1.0f); @@ -27,7 +28,8 @@ namespace Barotrauma { None, Sine, - Noise + Noise, + Circle } [Serialize(0.0f, IsPropertySaveable.Yes), Editable] @@ -47,6 +49,15 @@ namespace Barotrauma [Serialize(0.0f, IsPropertySaveable.Yes), Editable] public float OffsetAnimSpeed { get; private set; } + [Serialize(AnimationType.None, IsPropertySaveable.No), Editable] + public AnimationType ScaleAnim { get; private set; } + + [Serialize("0,0", IsPropertySaveable.Yes), Editable] + public Vector2 ScaleAnimAmount { get; private set; } + + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] + public float ScaleAnimSpeed { get; private set; } + private float rotationSpeedRadians; private float absRotationSpeedRadians; @@ -136,7 +147,7 @@ namespace Barotrauma foreach (var subElement in element.Elements()) { //choose which list the new conditional should be placed to - List conditionalList = null; + List conditionalList; switch (subElement.Name.ToString().ToLowerInvariant()) { case "conditional": @@ -162,15 +173,14 @@ namespace Barotrauma { case AnimationType.Sine: offsetState %= (MathHelper.TwoPi / OffsetAnimSpeed); - offset *= (float)Math.Sin(offsetState * OffsetAnimSpeed); + offset *= MathF.Sin(offsetState * OffsetAnimSpeed); + break; + case AnimationType.Circle: + offsetState %= (MathHelper.TwoPi / OffsetAnimSpeed); + offset *= new Vector2(MathF.Cos(offsetState * OffsetAnimSpeed), MathF.Sin(offsetState * OffsetAnimSpeed)); break; case AnimationType.Noise: - offsetState %= 1.0f / (OffsetAnimSpeed * 0.1f); - - float t = offsetState * 0.1f * OffsetAnimSpeed; - offset = new Vector2( - offset.X * (PerlinNoise.GetPerlin(t, t) - 0.5f), - offset.Y * (PerlinNoise.GetPerlin(t + 0.5f, t + 0.5f) - 0.5f)); + offset *= GetNoiseVector(ref offsetState, OffsetAnimSpeed); break; } } @@ -192,7 +202,7 @@ namespace Barotrauma case AnimationType.Sine: rotationState %= MathHelper.TwoPi / absRotationSpeedRadians; return - rotationRadians * (float)Math.Sin(rotationState * rotationSpeedRadians) + rotationRadians * MathF.Sin(rotationState * rotationSpeedRadians) + MathHelper.Lerp(randomRotationRadians.X, randomRotationRadians.Y, randomRotationFactor); case AnimationType.Noise: rotationState %= 1.0f / absRotationSpeedRadians; @@ -207,13 +217,40 @@ namespace Barotrauma } } - public float GetScale(float randomScaleModifier) + public Vector2 GetScale(ref float scaleState, float randomScaleModifier) { - if (RandomScale == Vector2.Zero) - { - return scale; - } - return MathHelper.Lerp(RandomScale.X, RandomScale.Y, randomScaleModifier); + Vector2 currentScale = Vector2.One * + (RandomScale == Vector2.Zero ? scale : MathHelper.Lerp(RandomScale.X, RandomScale.Y, randomScaleModifier)); + if (ScaleAnimSpeed > 0.0f) + { + switch (ScaleAnim) + { + case AnimationType.Sine: + scaleState %= (MathHelper.TwoPi / ScaleAnimSpeed); + currentScale *= Vector2.One + ScaleAnimAmount * MathF.Sin(scaleState * ScaleAnimSpeed); + break; + case AnimationType.Noise: + currentScale *= Vector2.One + ScaleAnimAmount * GetNoiseVector(ref scaleState, ScaleAnimSpeed); + break; + } + } + return currentScale; + } + + private static Vector2 GetNoiseVector(ref float state, float speed) + { + //multiply speed by a magic constant, because otherwise a speed of 1 would already be very fast (looping through the noise texture once per second) + //just makes the values more intuitive / closer to what constitutes as "fast" on the other types of animations + float modifiedSpeed = speed * 0.1f; + // wrap around the edge of the noise (t == 1) + state %= 1.0f / modifiedSpeed; + float t = state * modifiedSpeed; + Vector2 noiseValue = new Vector2( + PerlinNoise.GetPerlin(t, t), + //sample the y coordinate from a different position in the noise texture + PerlinNoise.GetPerlin(t + 0.5f, t + 0.5f)); + //move the value so it's in the range of -0.5 and 0.5, as opposed to 0-1. + return noiseValue - new Vector2(0.5f, 0.5f); } public static void UpdateSpriteStates(ImmutableDictionary> spriteGroups, Dictionary animStates, @@ -264,6 +301,7 @@ namespace Barotrauma if (!checkConditional(conditional)) { animate = false; break; } } if (!animate) { continue; } + spriteState.ScaleState += deltaTime; spriteState.OffsetState += deltaTime; spriteState.RotationState += deltaTime; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index c253984c8..091591c63 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using Barotrauma.IO; @@ -10,7 +10,19 @@ namespace Barotrauma { public partial class Sprite { - public Identifier Identifier { get; private set; } + private Identifier identifier; + public Identifier Identifier + { + get + { + if (identifier.IsEmpty) + { + identifier = GetIdentifier(SourceElement); + } + return identifier; + } + } + public static IEnumerable LoadedSprites { get @@ -71,8 +83,6 @@ namespace Barotrauma } } - private string disposeStackTrace; - public bool Loaded { get { return texture != null && !cannotBeLoaded; } @@ -419,7 +429,6 @@ namespace Barotrauma partial void DisposeTexture() { - disposeStackTrace = Environment.StackTrace; if (texture != null) { //check if another sprite is using the same texture diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index e74ee34bf..93d871cbf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -78,14 +78,22 @@ namespace Barotrauma { bool entityAngleAssigned = false; Limb targetLimb = null; - if (entity is Item item && item.body != null) + if (entity is Item item) { - angle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); - particleRotation = -item.body.Rotation; - if (emitter.Prefab.Properties.CopyEntityDir && item.body.Dir < 0.0f) + if (item.body != null) { - particleRotation += MathHelper.Pi; - mirrorAngle = true; + angle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + particleRotation = -item.body.Rotation; + if (emitter.Prefab.Properties.CopyEntityDir && item.body.Dir < 0.0f) + { + particleRotation += MathHelper.Pi; + mirrorAngle = true; + } + } + else + { + angle = -item.RotationRad; + particleRotation = item.RotationRad; } entityAngleAssigned = true; } @@ -135,6 +143,7 @@ namespace Barotrauma private void PlaySound(Entity entity, Hull hull, Vector2 worldPosition) { if (sounds.Count == 0) { return; } + if (entity is { Submarine.Loading: true }) { return; } if (soundChannel == null || !soundChannel.IsPlaying || forcePlaySounds) { @@ -185,13 +194,8 @@ namespace Barotrauma soundChannel.Position = new Vector3(worldPosition, 0.0f); } - if (soundChannel != null && soundChannel.Looping) - { - ActiveLoopingSounds.Add(this); - soundEmitter = entity; - loopStartTime = Timing.TotalTime; - } - + KeepLoopingSoundAlive(soundChannel); + void PlaySoundOrDelayIfNotLoaded(RoundSound selectedSound) { if (playSoundAfterLoadedCoroutine != null) { return; } @@ -222,14 +226,28 @@ namespace Barotrauma void PlaySound(RoundSound selectedSound) { - //if the sound loops, we must make sure the existing channel + //if the sound loops, we must make sure the existing channel has been stopped first before attempting to play a new one System.Diagnostics.Debug.Assert( soundChannel == null || !soundChannel.IsPlaying || soundChannel.FadingOutAndDisposing || !soundChannel.Looping, "A StatusEffect attempted to play a sound, but an looping sound is already playing. The looping sound should be stopped before playing a new one, or it will keep looping indefinitely."); soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling, freqMult: selectedSound.GetRandomFrequencyMultiplier()); ignoreMuffling = selectedSound.IgnoreMuffling; - if (soundChannel != null) { soundChannel.Looping = loopSound; } + if (soundChannel != null) + { + soundChannel.Looping = loopSound; + KeepLoopingSoundAlive(soundChannel); + } + } + + void KeepLoopingSoundAlive(SoundChannel soundChannel) + { + if (soundChannel != null && soundChannel.Looping) + { + ActiveLoopingSounds.Add(this); + soundEmitter = entity; + loopStartTime = Timing.TotalTime; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs index c2b404b07..875369abc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -41,7 +41,13 @@ namespace Barotrauma.Steam } } - Steamworks.SteamNetworkingUtils.OnDebugOutput += LogSteamworksNetworking; + Steamworks.SteamNetworkingUtils.OnDebugOutput += (Steamworks.NetDebugOutput nType, string pszMsg) => + { + if (GameSettings.CurrentConfig.VerboseLogging) + { + LogSteamworksNetworking(nType, pszMsg); + } + }; // Needed to detect invites for social overlay Steamworks.SteamFriends.ListenForFriendsMessages = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index f2d9e207e..cc1ef9c79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -173,7 +173,7 @@ namespace Barotrauma.Steam } DeletePublishStagingCopy(); - Directory.CreateDirectory(PublishStagingDir); + Directory.CreateDirectory(PublishStagingDir, catchUnauthorizedAccessExceptions: false); await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No); var stagingFileListPath = Path.Combine(PublishStagingDir, ContentPackage.FileListFileName); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index 2feff3989..2316c122c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -674,77 +674,120 @@ namespace Barotrauma.Steam var contextMenuHandler = new GUICustomComponent(new RectTransform(Vector2.Zero, modFrame.RectTransform), onUpdate: (f, component) => { - var parentList = modFrame.Parent?.Parent?.Parent as GUIListBox; //lovely jank :) - if (parentList is null) { return; } - if (GUI.MouseOn == modFrame && parentList.DraggedElement is null && PlayerInput.SecondaryMouseButtonClicked()) + if (modFrame.Parent?.Parent?.Parent is not GUIListBox parentList || GUI.MouseOn != modFrame || parentList.DraggedElement is not null || !PlayerInput.SecondaryMouseButtonClicked()) { return; } + if (!parentList.AllSelected.Contains(modFrame)) { - if (!parentList.AllSelected.Contains(modFrame)) { parentList.Select(parentList.Content.GetChildIndex(modFrame)); } - static void noop() { } - - List contextMenuOptions = new List(); - if (ContentPackageManager.WorkshopPackages.Contains(mod)) + parentList.Select(parentList.Content.GetChildIndex(modFrame)); + } + List contextMenuOptions = new List(); + ContentPackage[] selectedMods = parentList.AllSelected.Select(it => it.UserData).OfType().ToArray(); + Identifier swapLabel = ((parentList == enabledRegularModsList, selectedMods.Length > 1) switch + { + (false, true) => "EnableSelectedWorkshopMods", + (false, false) => "EnableWorkshopMod", + (true, true) => "DisableSelectedWorkshopMods", + (true, false) => "DisableWorkshopMod" + }).ToIdentifier(); + contextMenuOptions.Add(new(swapLabel, isEnabled: true, currentSwapFunc ?? NoOp)); + if (ContentPackageManager.WorkshopPackages.Contains(mod)) + { + if (selectedMods.Length == 1) { - contextMenuOptions.Add( - new ContextMenuOption("ViewWorkshopModDetails".ToIdentifier(), isEnabled: true, onSelected: () => PrepareToShowModInfo(mod))); + contextMenuOptions.Add(new("ViewWorkshopModDetails".ToIdentifier(), isEnabled: true, () => PrepareToShowModInfo(mod))); + contextMenuOptions.Add(new("CopyWorkshopToLocal".ToIdentifier(), isEnabled: true, () => CopyToLocal())); } - - var labelConditions - = (parentList == enabledRegularModsList, parentList.AllSelected.Count > 1); - Identifier swapLabel = (labelConditions switch + if (selectedMods.All(ContentPackageManager.WorkshopPackages.Contains)) { - (false, true) => "EnableSelectedWorkshopMods", - (false, false) => "EnableWorkshopMod", - (true, true) => "DisableSelectedWorkshopMods", - (true, false) => "DisableWorkshopMod" - }).ToIdentifier(); - - contextMenuOptions.Add(new ContextMenuOption(swapLabel, - isEnabled: true, onSelected: currentSwapFunc ?? noop)); - - var selectedMods = parentList.AllSelected.Select(it => it.UserData) - .OfType().ToArray(); - if (selectedMods.All(ContentPackageManager.LocalPackages.Contains) && selectedMods.Length > 1) - { - contextMenuOptions.Add(new ContextMenuOption("MergeSelectedMods".ToIdentifier(), isEnabled: true, - onSelected: () => ModMerger.AskMerge(selectedMods))); - } - - GUIButton? iconBtn(GUIComponent component) => component.GetChild()?.GetAllChildren().Last(); - if (selectedMods.All(ContentPackageManager.WorkshopPackages.Contains) - && parentList.AllSelected.All(c => iconBtn(c)?.Style?.Identifier == "WorkshopMenu.DownloadedIcon") - && selectedMods.Length > 0) - { - contextMenuOptions.Add(new ContextMenuOption( - (selectedMods.Length > 1 ? "UnsubscribeFromAllSelected" : "WorkshopItemUnsubscribe").ToIdentifier(), - isEnabled: true, - onSelected: () => + if (parentList.AllSelected.All(c => c.GetChild()?.GetAllChildren().Last()?.Style?.Identifier == "WorkshopMenu.DownloadedIcon") && selectedMods.Length > 0 && SteamManager.IsInitialized) + { + contextMenuOptions.Add(new((selectedMods.Length > 1 ? "UnsubscribeFromAllSelected" : "WorkshopItemUnsubscribe").ToIdentifier(), true, () => { - var workshopIds = selectedMods - .Select(m => m.UgcId) - .NotNone() - .OfType() - .Select(id => id.Value); - TaskPool.AddIfNotFound($"UnsubFromSelected", Task.WhenAll(workshopIds.Select(SteamManager.Workshop.GetItem)), - t => + TaskPool.AddIfNotFound("UnsubFromSelected", Task.WhenAll(selectedMods.Select(m => m.UgcId).NotNone().OfType().Select(id => SteamManager.Workshop.GetItem(id.Value))), t => + { + if (!t.TryGetResult(out Option[]? itemOptions)) { return; } + itemOptions.ForEach(io => { - if (!t.TryGetResult(out Steamworks.Ugc.Item?[]? items)) { return; } - items.ForEach(it => - { - if (!(it is { } item)) { return; } - - item.Unsubscribe(); - SteamManager.Workshop.Uninstall(item); - PopulateInstalledModLists(); - }); + if (!io.TryUnwrap(out Steamworks.Ugc.Item item) || !item.IsSubscribed) { return; } + item.Unsubscribe(); + SteamManager.Workshop.Uninstall(item); + PopulateInstalledModLists(); }); + }); })); + } } - - GUIContextMenu.CreateContextMenu( - pos: PlayerInput.MousePosition, - header: ToolBox.LimitString(mod.Name, GUIStyle.SubHeadingFont, GUI.IntScale(300f)), - headerColor: null, - contextMenuOptions.ToArray()); + } + else if (ContentPackageManager.LocalPackages.Contains(mod)) + { + if (selectedMods.Length == 1) + { + contextMenuOptions.Add(new ContextMenuOption("RenamePackage".ToIdentifier(), isEnabled: true, AskToRenameLocal)); + } + if (selectedMods.All(ContentPackageManager.LocalPackages.Contains)) + { + if (selectedMods.Length > 1) + { + contextMenuOptions.Add(new("MergeSelectedMods".ToIdentifier(), isEnabled: true, () => ModMerger.AskMerge(selectedMods))); + } + contextMenuOptions.Add(new("Delete".ToIdentifier(), isEnabled: true, AskToDeleteLocal)); + } + } + GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, ToolBox.LimitString(mod.Name, GUIStyle.SubHeadingFont, GUI.IntScale(300)), null, contextMenuOptions.ToArray()); + static void NoOp() { } + void AskToRenameLocal() + { + GUIMessageBox msgBox = new(TextManager.Get("RenamePackage"), mod.Name, new LocalizedString[] { TextManager.Get("Confirm"), TextManager.Get("Cancel") }, minSize: new Point(0, GUI.IntScale(195))); + GUITextBox textBox = new(new(Vector2.One, msgBox.Content.RectTransform), mod.Name) + { + OnEnterPressed = (textBox, text) => + { + textBox.Text = text.Trim(); + return true; + } + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked += (_, _) => + { + if (textBox.Text == mod.Name) + { + msgBox.Close(); + return true; + } + if (mod.TryRenameLocal(textBox.Text)) + { + PopulateInstalledModLists(); + msgBox.Close(); + return true; + } + else + { + textBox.Flash(); + return false; + } + }; + } + void CopyToLocal() + { + if (mod.TryCreateLocalFromWorkshop()) + { + PopulateInstalledModLists(); + } + } + void AskToDeleteLocal() + { + GUIMessageBox msgBox = new(TextManager.Get("DeleteMods"), TextManager.GetWithVariables("DeleteModsConfirm", ("[amount]", selectedMods.Length.ToString())), + new LocalizedString[] { TextManager.Get("Confirm"), TextManager.Get("Cancel")}, textAlignment: Alignment.TopCenter); + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked += (_, _) => + { + foreach (ContentPackage mod in selectedMods) + { + mod.TryDeleteLocal(); + PopulateInstalledModLists(); + } + msgBox.Close(); + return true; + }; } }); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs index 12b4b2876..a628471b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs @@ -127,7 +127,7 @@ namespace Barotrauma writePkgElem(CorePackage); RegularPackages.ForEach(writePkgElem); - if (!Directory.Exists(SavePath)) { Directory.CreateDirectory(SavePath); } + if (!Directory.Exists(SavePath)) { Directory.CreateDirectory(SavePath, catchUnauthorizedAccessExceptions: false); } newDoc.SaveSafe(Path.Combine(SavePath, ToolBox.RemoveInvalidFileNameChars($"{Name}.xml"))); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index 45111625f..f8459f3bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -45,7 +45,7 @@ namespace Barotrauma Dictionary> xmlContent; try { - xmlContent = ConvertInfoTextToXML(File.ReadAllLines(textFilePath, Encoding.UTF8)); + xmlContent = ConvertInfoTextToXML(File.ReadAllLines(textFilePath, Encoding.UTF8, catchUnauthorizedAccessExceptions: false)); } catch (Exception e) { @@ -156,7 +156,7 @@ namespace Barotrauma List xmlContent; try { - xmlContent = ConvertInfoTextToXML(File.ReadAllLines(infoTextFiles[j], Encoding.UTF8), language); + xmlContent = ConvertInfoTextToXML(File.ReadAllLines(infoTextFiles[j], Encoding.UTF8, catchUnauthorizedAccessExceptions: false), language); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index c01e38223..b87448e6f 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.5.9.2 + 1.6.17.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 30f1c4d4b..540024b9b 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.5.9.2 + 1.6.17.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 215e40abe..dc99b9460 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.5.9.2 + 1.6.17.0 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 9917a17df..5f3b02bac 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.5.9.2 + 1.6.17.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 7ff4c3e1f..bcfa3e2e6 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.5.9.2 + 1.6.17.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 144b8fbdc..86467b421 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -40,7 +40,7 @@ namespace Barotrauma { matchingData.ApplyPermadeath(); - if (GameMain.Server is { ServerSettings.IronmanMode: true }) + if (GameMain.Server?.ServerSettings is { IronmanModeActive: true }) { mpCampaign.SaveSingleCharacter(matchingData); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 6b35b7d99..c964fb354 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -21,16 +21,16 @@ namespace Barotrauma CauseOfDeath = null; } - partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel) + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel, bool forceNotification) { if (Character == null || Character.Removed) { return; } if (!prevSentSkill.ContainsKey(skillIdentifier)) { prevSentSkill[skillIdentifier] = prevLevel; } - if (Math.Abs(prevSentSkill[skillIdentifier] - newLevel) > 0.01f) + if (Math.Abs(prevSentSkill[skillIdentifier] - newLevel) > 0.1f || forceNotification) { - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdateSkillsEventData()); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdateSkillsEventData(skillIdentifier, forceNotification)); prevSentSkill[skillIdentifier] = newLevel; } } @@ -98,6 +98,8 @@ namespace Barotrauma msg.WriteInt32(ExperiencePoints); msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints); msg.WriteBoolean(PermanentlyDead); + msg.WriteInt32(TalentRefundPoints); + msg.WriteInt32(TalentResetCount); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 9d5bce1a6..6d8c1801e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -298,17 +298,12 @@ namespace Barotrauma Kill(causeOfDeath.type, causeOfDeath.affliction); } break; + case EventType.ConfirmTalentRefund: + if (!CanManageTalents(c)) { return; } + Info?.RefundTalents(); + break; case EventType.UpdateTalents: - if (c.Character != this) - { - if (!IsBot || !c.HasPermission(ClientPermissions.ManageBotTalents)) - { -#if DEBUG - DebugConsole.Log("Received a character update message from a client who's not controlling the character"); -#endif - return; - } - } + if (!CanManageTalents(c)) { return; } // get the full list of talents from the player, only give the ones // that are not already given (or otherwise not viable) @@ -332,6 +327,22 @@ namespace Barotrauma } break; } + + bool CanManageTalents(Client client) + { + if (client.Character != this) + { + if (client.TeamID != TeamID || !IsBot || !client.HasPermission(ClientPermissions.ManageBotTalents) || client.Spectating) + { +#if DEBUG + DebugConsole.Log("A client tried to manage talents of a character they don't control or have permission to manage"); +#endif + return false; + } + } + + return true; + } } public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c) @@ -377,8 +388,15 @@ namespace Barotrauma tempBuffer.WriteBoolean(shoot); tempBuffer.WriteBoolean(use); - tempBuffer.WriteBoolean(AnimController is HumanoidAnimController { Crouching: true }); - + if (AnimController is HumanoidAnimController humanAnim) + { + tempBuffer.WriteBoolean(humanAnim.Crouching); + } + else if (AnimController is FishAnimController fishAnim) + { + tempBuffer.WriteBoolean(fishAnim.Reverse); + } + tempBuffer.WriteBoolean(attack); Vector2 relativeCursorPos = cursorPosition - AimRefPosition; @@ -464,20 +482,17 @@ namespace Barotrauma case CharacterStatusEventData statusEventData: WriteStatus(msg, statusEventData.ForceAfflictionData); break; - case UpdateSkillsEventData _: - if (Info?.Job == null) + case UpdateSkillsEventData updateSkillsData: + if (Info?.Job is { } job) { - msg.WriteByte((byte)0); + msg.WriteIdentifier(updateSkillsData.SkillIdentifier); + msg.WriteBoolean(updateSkillsData.ForceNotification); + //don't use Character.GetSkillLevel here, because it applies all the temporary boosts from items and afflictions on the skill level + msg.WriteSingle(job.GetSkillLevel(updateSkillsData.SkillIdentifier)); } else { - var skills = Info.Job.GetSkills(); - msg.WriteByte((byte)skills.Count()); - foreach (Skill skill in skills) - { - msg.WriteIdentifier(skill.Identifier); - msg.WriteSingle(skill.Level); - } + msg.WriteIdentifier(Identifier.Empty); } break; case IAttackEventData attackEventData: @@ -555,6 +570,7 @@ namespace Barotrauma break; case UpdateExperienceEventData _: msg.WriteInt32(Info.ExperiencePoints); + msg.WriteInt32(info.AdditionalTalentPoints); break; case UpdateTalentsEventData _: msg.WriteUInt16((ushort)characterTalents.Count); @@ -565,9 +581,16 @@ namespace Barotrauma } break; case UpdateMoneyEventData _: - msg.WriteInt32(GameMain.GameSession.Campaign.GetWallet(c).Balance); + msg.WriteInt32(Wallet?.Balance ?? 0); + break; + case UpdateRefundPointsEventData when Info is { } i: + msg.WriteInt32(i.TalentRefundPoints); + break; + case ConfirmRefundEventData: + // No data break; case UpdatePermanentStatsEventData updatePermanentStatsEventData: + StatTypes statType = updatePermanentStatsEventData.StatType; if (Info == null) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 0285452fa..33c011b33 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -716,9 +716,7 @@ namespace Barotrauma ShowQuestionPrompt("Console command permissions to revoke from \"" + client.Name + "\"? You may enter multiple commands separated with a space.", (commandsStr) => { - Identifier[] splitCommands = commandsStr.Split(' ') - .Select(s => s.Trim()) - .ToIdentifiers().ToArray(); + Identifier[] splitCommands = commandsStr.ToIdentifiers(separator: " ").ToArray(); List revokedCommands = new List(); bool revokeAll = splitCommands.Length > 0 && splitCommands[0] == "all"; if (revokeAll) @@ -1351,14 +1349,14 @@ namespace Barotrauma commands.Add(new Command("mission", "mission [name]: Select the mission type for the next round.", (string[] args) => { - GameMain.NetLobbyScreen.MissionTypeName = string.Join(" ", args); - NewMessage("Set mission to " + GameMain.NetLobbyScreen.MissionTypeName, Color.Cyan); + GameMain.NetLobbyScreen.MissionTypes = args.ToIdentifiers(); + NewMessage("Set mission to " + string.Join(",", GameMain.NetLobbyScreen.MissionTypes), Color.Cyan); }, () => { return new string[][] { - Enum.GetNames(typeof(MissionType)) + MissionPrefab.GetAllMultiplayerSelectableMissionTypes().Select(id => id.Value).ToArray() }; })); @@ -1404,10 +1402,7 @@ namespace Barotrauma AssignOnExecute("respawnnow", (string[] args) => { if (GameMain.Server?.RespawnManager == null) { return; } - if (GameMain.Server.RespawnManager.CurrentState != RespawnManager.State.Transporting) - { - GameMain.Server.RespawnManager.ForceRespawn(); - } + GameMain.Server.RespawnManager.ForceRespawn(); }); commands.Add(new Command("startgame|startround|start", "start/startgame/startround: Start a new round.", (string[] args) => @@ -1416,7 +1411,7 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath, client: null); + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.DataPath, client: null); } else { @@ -1425,7 +1420,9 @@ namespace Barotrauma MultiPlayerCampaign.StartCampaignSetup(); return; } - if (!GameMain.Server.TryStartGame()) { NewMessage("Failed to start a new round", Color.Yellow); } + + var result = GameMain.Server.TryStartGame(); + if (result != GameServer.TryStartGameResult.Success) { NewMessage($"Failed to start a new round: {TextManager.Get($"TryStartGameError.{result}")}", Color.Yellow); } } })); @@ -1897,23 +1894,28 @@ namespace Barotrauma } } ); + + AssignOnClientRequestExecute( + "healme", + (Client client, Vector2 cursorWorldPos, string[] args) => + { + bool healAll = args.Length > 0 && args[0].Equals("all", StringComparison.OrdinalIgnoreCase); + if (client.Character != null) + { + HealCharacter(client.Character, healAll); + } + } + ); AssignOnClientRequestExecute( "heal", (Client client, Vector2 cursorWorldPos, string[] args) => { bool healAll = args.Length > 1 && args[1].Equals("all", StringComparison.OrdinalIgnoreCase); - Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(healAll ? args.Take(args.Length - 1).ToArray() : args); + Character healedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(healAll ? args.Take(args.Length - 1).ToArray() : args); if (healedCharacter != null) { - healedCharacter.SetAllDamage(0.0f, 0.0f, 0.0f); - healedCharacter.Oxygen = 100.0f; - healedCharacter.Bloodloss = 0.0f; - healedCharacter.SetStun(0.0f, true); - if (healAll) - { - healedCharacter.CharacterHealth.RemoveAllAfflictions(); - } + HealCharacter(healedCharacter, healAll); } } ); @@ -1934,7 +1936,7 @@ namespace Barotrauma // If killed in ironman mode, the character has been wiped from the save mid-round, so its // original data needs to be restored to the save file (without making a backup of the dead character) - if (GameMain.Server.ServerSettings.IronmanMode && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + if (GameMain.Server.ServerSettings is { IronmanModeActive: true } && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) { if (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore) { @@ -2546,6 +2548,7 @@ namespace Barotrauma { foreach (Skill skill in character.Info.Job.GetSkills()) { + GameMain.NetworkMember.CreateEntityEvent(character, new Character.UpdateSkillsEventData(skill.Identifier, forceNotification: true)); character.Info.SetSkillLevel(skill.Identifier, level); } GameMain.Server.SendConsoleMessage($"Set all {character.Name}'s skills to {level}", senderClient); @@ -2553,10 +2556,10 @@ namespace Barotrauma else { character.Info.SetSkillLevel(skillIdentifier, level); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.UpdateSkillsEventData(skillIdentifier, forceNotification: true)); GameMain.Server.SendConsoleMessage($"Set {character.Name}'s {skillIdentifier} level to {level}", senderClient); } - GameMain.NetworkMember.CreateEntityEvent(character, new Character.UpdateSkillsEventData()); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index f7656922a..e027c8ff2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -1,20 +1,51 @@ -using System.Collections.Generic; +#nullable enable +using Barotrauma.Extensions; +using Barotrauma.Networking; +using System.Collections.Generic; using System.Linq; namespace Barotrauma { partial class CombatMission { + class KillCount + { + public readonly Character Victim; + public readonly Client? VictimClient; + public readonly Character? Killer; + public readonly Client? KillerClient; + public KillCount(Character victim, Character? killer) + { + Victim = victim; + VictimClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => victim.IsClientOwner(c)); + Killer = killer; + if (killer != null) + { + KillerClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => killer.IsClientOwner(c)); + } + } + } + const float RoundEndDuration = 5.0f; private readonly bool[] teamDead = new bool[2]; + /// + /// Lists of characters currently alive in the teams + /// private List[] crews; + /// + /// List of all kills (of the characters in either team) during the round + /// + private readonly List kills = new List(); + private bool initialized = false; private float roundEndTimer; + private float timeInTargetSubmarineTimer; + public override LocalizedString Description { get @@ -28,51 +59,16 @@ namespace Barotrauma protected override void UpdateMissionSpecific(float deltaTime) { - if (!initialized) - { - crews[0].Clear(); - crews[1].Clear(); - foreach (Character character in Character.CharacterList) - { - if (character.TeamID == CharacterTeamType.Team1) - { - crews[0].Add(character); - } - else if (character.TeamID == CharacterTeamType.Team2) - { - crews[1].Add(character); - } - } - - initialized = true; - } - - if (crews[0].Count == 0 || crews[1].Count == 0) - { - //if there are no characters in either crew, end the round - teamDead[0] = teamDead[1] = true; - state = 1; - } - else - { - teamDead[0] = crews[0].All(c => c.IsDead || c.IsIncapacitated); - teamDead[1] = crews[1].All(c => c.IsDead || c.IsIncapacitated); - if (teamDead[0] && teamDead[1]) { state = 1; } - } + CheckTeamCharacters(); if (state == 0) { + CheckWinCondition(deltaTime); for (int i = 0; i < teamDead.Length; i++) { if (!teamDead[i] && teamDead[1 - i]) { - //make sure nobody in the other team can be revived because that would be pretty weird - crews[1 - i].ForEach(c => { if (!c.IsDead) c.Kill(CauseOfDeathType.Unknown, null); }); - - GameMain.GameSession.WinningTeam = i == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2; - - //state 1 = team 1 won, 2 = team 2 won - State = i + 1; + SetWinningTeam(i); break; } } @@ -85,7 +81,7 @@ namespace Barotrauma if (teamDead[0] && teamDead[1]) { GameMain.GameSession.WinningTeam = CharacterTeamType.None; - if (GameMain.Server != null) { GameMain.Server.EndGame(); } + GameMain.Server?.EndGame(); } else if (GameMain.GameSession.WinningTeam != CharacterTeamType.None) { @@ -93,5 +89,168 @@ namespace Barotrauma } } } + + private void CheckTeamCharacters() + { + if (!allowRespawning && initialized) + { + //if no respawns are allowed, we only need to check the characters once + return; + } + + for (int i = 0; i < crews.Length; i++) + { + foreach (var character in crews[i]) + { + if (character.IsDead) + { + AddKill(character); + } + } + } + + crews[0].Clear(); + crews[1].Clear(); + foreach (Character character in Character.CharacterList) + { + if (character.IsDead) { continue; } + if (character.TeamID == CharacterTeamType.Team1) + { + crews[0].Add(character); + } + else if (character.TeamID == CharacterTeamType.Team2) + { + crews[1].Add(character); + } + if (character.IsBot && character.AIController is HumanAIController humanAi) + { + if (!humanAi.ObjectiveManager.HasOrder(o => o.TargetCharactersInOtherSubs) && + OrderPrefab.Prefabs.TryGet(Tags.AssaultEnemyOrder, out OrderPrefab? assaultOrder)) + { + character.SetOrder(assaultOrder.CreateInstance( + OrderPrefab.OrderTargetType.Entity, orderGiver: null).WithManualPriority(CharacterInfo.HighestManualOrderPriority), + isNewOrder: true, speak: false); + } + } + } + + initialized = true; + } + + private void CheckWinCondition(float deltaTime) + { + switch (winCondition) + { + case WinCondition.LastManStanding: + if (crews[0].Count == 0 || crews[1].Count == 0) + { + //if there are no characters in either crew, end the round + teamDead[0] = teamDead[1] = true; + state = 1; + } + else + { + teamDead[0] = crews[0].All(c => c.IsDead || c.IsIncapacitated); + teamDead[1] = crews[1].All(c => c.IsDead || c.IsIncapacitated); + if (teamDead[0] && teamDead[1]) { state = 1; } + } + break; + case WinCondition.KillCount: + //no need to do anything, kills are counted in AddKill + break; + case WinCondition.ControlSubmarine: + CheckTargetSubmarineControl(deltaTime); + break; + } + CheckScore(); + } + + private void CheckScore() + { + for (int i = 0; i < crews.Length; i++) + { + if (Scores[i] >= WinScore) + { + SetWinningTeam(i); + break; + } + } + } + + private void CheckTargetSubmarineControl(float deltaTime) + { + if (targetSubmarine == null) { return; } + + //score updates at 1 second intervals, so the score represents the time in seconds + timeInTargetSubmarineTimer += deltaTime; + if (timeInTargetSubmarineTimer < 1.0f) + { + return; + } + timeInTargetSubmarineTimer = 0.0f; + + bool crew1InSubmarine = crews[0].Any(c => c.Submarine == targetSubmarine); + bool crew2InSubmarine = crews[1].Any(c => c.Submarine == targetSubmarine); + + for (int i = 0; i < crews.Length; i++) + { + if (crews[i].Any(c => c.Submarine == targetSubmarine) && + crews[1 - i].None(c => c.Submarine == targetSubmarine)) + { + Scores[i]++; + GameMain.Server?.UpdateMissionState(this); + } + } + } + + private void AddKill(Character character) + { + kills.Add(new KillCount(character, character.CauseOfDeath?.Killer)); + if (winCondition == WinCondition.KillCount) + { + Scores[character.TeamID == CharacterTeamType.Team1 ? 1 : 0] += PointsPerKill; + } + GameMain.Server?.UpdateMissionState(this); + } + + private void SetWinningTeam(int teamIndex) + { + //state 1 = team 1 won, 2 = team 2 won + State = teamIndex + 1; + GameMain.GameSession.WinningTeam = teamIndex == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2; + } + + public override void ServerWrite(IWriteMessage msg) + { + base.ServerWrite(msg); + msg.WriteUInt16((ushort)Scores[0]); + msg.WriteUInt16((ushort)Scores[1]); + + IEnumerable uniqueClients = kills + .Select(k => k.VictimClient) + .Union(kills.Select(k => k.KillerClient)) + .NotNull(); + msg.WriteVariableUInt32((uint)uniqueClients.Count()); + foreach (Client client in uniqueClients) + { + msg.WriteByte(client.SessionId); + msg.WriteVariableUInt32((uint)kills.Count(k => k.VictimClient == client)); + msg.WriteVariableUInt32((uint)kills.Count(k => k.KillerClient == client)); + } + + IEnumerable uniqueBots = kills + .Select(k => k.Killer) + .Union(kills.Select(k => k.Victim)) + .NotNull() + .Where(c => c.Info != null && c.IsBot) + .Select(c => c.Info); + msg.WriteVariableUInt32((uint)uniqueBots.Count()); + foreach (CharacterInfo botInfo in uniqueBots) + { + msg.WriteUInt16(botInfo.ID); + msg.WriteVariableUInt32((uint)kills.Count(k => k.Victim?.Info == botInfo)); + msg.WriteVariableUInt32((uint)kills.Count(k => k.Killer?.Info == botInfo)); + } + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index c62784177..74f492db9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -241,7 +241,7 @@ namespace Barotrauma maxPlayers, ownerKey, ownerEndpoint); - Server.StartServer(); + Server.StartServer(registerToServerList: true); for (int i = 0; i < CommandLineArgs.Length; i++) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index f9404cc87..0a0d45a07 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -143,6 +143,7 @@ namespace Barotrauma { Reset(); CharacterInfo.PermanentlyDead = true; + GameMain.GameSession?.IncrementPermadeath(AccountId); DebugConsole.NewMessage($"Permadeath applied on {Name}'s CharacterCampaignData.CharacterInfo."); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index f153d82d1..95d6df05c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -121,21 +121,21 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(savePath)) { return; } - GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), savePath, GameModePreset.MultiPlayerCampaign, startingSettings, seed); + GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), Option.None, CampaignDataPath.CreateRegular(savePath), GameModePreset.MultiPlayerCampaign, startingSettings, seed); GameMain.NetLobbyScreen.ToggleCampaignMode(true); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + SaveUtil.SaveGame(GameMain.GameSession.DataPath); DebugConsole.NewMessage("Campaign started!", Color.Cyan); DebugConsole.NewMessage("Current location: " + GameMain.GameSession.Map.CurrentLocation.DisplayName, Color.Cyan); ((MultiPlayerCampaign)GameMain.GameSession.GameMode).LoadInitialLevel(); } - public static void LoadCampaign(string selectedSave, Client client) + public static void LoadCampaign(CampaignDataPath path, Client client) { GameMain.NetLobbyScreen.ToggleCampaignMode(true); try { - SaveUtil.LoadGame(selectedSave); + SaveUtil.LoadGame(path); if (GameMain.GameSession.GameMode is MultiPlayerCampaign mpCampaign) { mpCampaign.LastSaveID++; @@ -148,7 +148,7 @@ namespace Barotrauma } catch (Exception e) { - string errorMsg = $"Error while loading the save {selectedSave}"; + string errorMsg = $"Error while loading the save {path.LoadPath}"; if (client != null) { GameMain.Server?.SendDirectChatMessage($"{errorMsg}: {e.Message}\n{e.StackTrace}", client, ChatMessageType.Error); @@ -209,7 +209,7 @@ namespace Barotrauma { try { - LoadCampaign(saveFiles[saveIndex].FilePath, client: null); + LoadCampaign(CampaignDataPath.CreateRegular(saveFiles[saveIndex].FilePath), client: null); } catch (Exception ex) { @@ -289,7 +289,8 @@ namespace Barotrauma data.Refresh(character, refreshHealthData: character.CauseOfDeath?.Type != CauseOfDeathType.Disconnected); characterData.Add(data); } - else + //check the cause of death in the CharacterInfo too (the character instance may have despawned, so we can't just rely on that) + else if (data.CharacterInfo.CauseOfDeath is not { Type: CauseOfDeathType.Disconnected }) { //character dead or removed -> reduce skills, remove items, health data, etc data.CharacterInfo.ApplyDeathEffects(); @@ -406,13 +407,13 @@ namespace Barotrauma LeaveUnconnectedSubs(leavingSub); NextLevel = newLevel; GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + SaveUtil.SaveGame(GameMain.GameSession.DataPath); } else { PendingSubmarineSwitch = null; GameMain.Server.EndGame(TransitionType.None, wasSaved: false); - LoadCampaign(GameMain.GameSession.SavePath, client: null); + LoadCampaign(GameMain.GameSession.DataPath, client: null); LastSaveID++; IncrementAllLastUpdateIds(); yield return CoroutineStatus.Success; @@ -1229,7 +1230,7 @@ namespace Barotrauma if (renameCharacter) { renamedIdentifier = msg.ReadUInt16(); - newName = msg.ReadString(); + newName = Client.SanitizeName(msg.ReadString()); existingCrewMember = msg.ReadBoolean(); if (!GameMain.Server.IsNameValid(sender, newName)) { @@ -1466,7 +1467,16 @@ namespace Barotrauma return wallet.Balance + Bank.Balance; } - public override void Save(XElement element) + /// + /// Serializes the campaign and character data to XML. + /// + /// Game session element to save the campaign data to. + /// + /// Whether the save is being done during loading to ensure the campaign ID matches the one in the save file. + /// Used to work around some quirks with the backup save system. + /// See: + /// + public override void Save(XElement element, bool isSavingOnLoading) { element.Add(new XAttribute("campaignid", CampaignID)); XElement modeElement = new XElement("MultiPlayerCampaign", @@ -1516,8 +1526,16 @@ namespace Barotrauma element.Add(modeElement); - //save character data to a separate file - string characterDataPath = GetCharacterDataSavePath(); + // save character data to a separate file + + // When loading a campaign in multiplayer, we save the campaign to ensure the campaign ID that gets assigned + // matches the one in the save file, this is a problem with the backup save system since this causes the + // character data to save too, and we don't want to overwrite the main save file's character data. + // So we instead save over the load path in this case, which in backup saves is the backup file + // which we don't mind getting overriden since the data should be the same + string characterDataPath = isSavingOnLoading + ? GetCharacterDataPathForLoading() + : GetCharacterDataPathForSaving(); XDocument characterDataDoc = new XDocument(new XElement("CharacterData")); foreach (CharacterCampaignData cd in characterData) { @@ -1525,6 +1543,7 @@ namespace Barotrauma } try { + SaveUtil.DeleteIfExists(characterDataPath); characterDataDoc.SaveSafe(characterDataPath); } catch (Exception e) @@ -1544,7 +1563,7 @@ namespace Barotrauma /// eg. when using this method to save a character itself restored from the backup. public void SaveSingleCharacter(CharacterCampaignData newData, bool skipBackup = false) { - string characterDataPath = GetCharacterDataSavePath(); + string characterDataPath = GetCharacterDataPathForSaving(); if (!File.Exists(characterDataPath)) { DebugConsole.ThrowError($"Failed to load the character data for the campaign. Could not find the file \"{characterDataPath}\"."); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index 81610eb59..ea29665f7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components private string lastSentText; private float sendStateTimer; - [Serialize("", IsPropertySaveable.Yes, description: "The text to display on the label.", alwaysUseInstanceValues: true), Editable(100)] + [Serialize("", IsPropertySaveable.Yes, description: "The text to display on the label.", alwaysUseInstanceValues: true), Editable(MaxLength = 100)] public string Text { get; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index f618c0a72..3ca6dae01 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components if (!AutoPilot) { steeringInput = newSteeringInput; - steeringAdjustSpeed = MathHelper.Lerp(0.2f, 1.0f, c.Character.GetSkillLevel("helm") / 100.0f); + steeringAdjustSpeed = MathHelper.Lerp(0.2f, 1.0f, c.Character.GetSkillLevel(Tags.HelmSkill) / 100.0f); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs index f3dbfeb14..9cc01bcbc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -310,7 +310,13 @@ namespace Barotrauma.Items.Components return wire.BackingWire.TryUnwrap(out var backingWire) ? backingWire.Name : "a wire"; } - bool CanAccessAndUnlocked(Client client) => item.CanClientAccess(client) && !Locked; + bool CanAccessAndUnlocked(Client client) => + !IsLocked() && + item.CanClientAccess(client) && + ClientHasRequiredItems(client); + + bool ClientHasRequiredItems(Client client) => + client.Character is { } chara && HasRequiredItems(chara, addMessage: false); } /// diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs index b103e8e7f..acea2c8d2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs @@ -11,13 +11,17 @@ namespace Barotrauma.Items.Components string[] elementValues = new string[customInterfaceElementList.Count]; for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].HasPropertyName) + var element = customInterfaceElementList[i]; + switch (element.InputType) { - elementValues[i] = msg.ReadString(); - } - else - { - elementStates[i] = msg.ReadBoolean(); + case CustomInterfaceElement.InputTypeOption.Number: + case CustomInterfaceElement.InputTypeOption.Text: + elementValues[i] = msg.ReadString(); + break; + case CustomInterfaceElement.InputTypeOption.Button: + case CustomInterfaceElement.InputTypeOption.TickBox: + elementStates[i] = msg.ReadBoolean(); + break; } } @@ -26,15 +30,10 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < customInterfaceElementList.Count; i++) { - var element = customInterfaceElementList[i]; - if (element.HasPropertyName) + var element = customInterfaceElementList[i]; + switch (element.InputType) { - if (!element.IsNumberInput) - { - TextChanged(element, elementValues[i]); - } - else - { + case CustomInterfaceElement.InputTypeOption.Number: switch (element.NumberType) { case NumberType.Int when int.TryParse(elementValues[i], out int value): @@ -44,16 +43,20 @@ namespace Barotrauma.Items.Components ValueChanged(element, value); break; } - } - } - else if (element.ContinuousSignal) - { - TickBoxToggled(element, elementStates[i]); - } - else if (elementStates[i]) - { - clickedButton = element; - ButtonClicked(element); + break; + case CustomInterfaceElement.InputTypeOption.Text: + TextChanged(element, elementValues[i]); + break; + case CustomInterfaceElement.InputTypeOption.TickBox: + TickBoxToggled(element, elementStates[i]); + break; + case CustomInterfaceElement.InputTypeOption.Button: + if (elementStates[i]) + { + clickedButton = element; + ButtonClicked(element); + } + break; } } } @@ -70,17 +73,19 @@ namespace Barotrauma.Items.Components for (int i = 0; i < customInterfaceElementList.Count; i++) { var element = customInterfaceElementList[i]; - if (element.HasPropertyName) + + switch (element.InputType) { - msg.WriteString(element.Signal); - } - else if(element.ContinuousSignal) - { - msg.WriteBoolean(element.State); - } - else - { - msg.WriteBoolean(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); + case CustomInterfaceElement.InputTypeOption.Number: + case CustomInterfaceElement.InputTypeOption.Text: + msg.WriteString(element.Signal); + break; + case CustomInterfaceElement.InputTypeOption.TickBox: + msg.WriteBoolean(element.State); + break; + case CustomInterfaceElement.InputTypeOption.Button: + msg.WriteBoolean(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); + break; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 62c2477a1..91d95e6a2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -175,6 +175,10 @@ namespace Barotrauma } } break; + case SwapItemEventData swapItemEventData: + msg.WriteUInt16(swapItemEventData.NewId); + msg.WriteUInt32(swapItemEventData.NewItem.UintIdentifier); + break; default: throw error($"Unsupported event type {itemEventData.GetType().Name}"); } @@ -317,7 +321,7 @@ namespace Barotrauma msg.WriteBoolean(tagsChanged); if (tagsChanged) { - IEnumerable splitTags = Tags.Split(',').ToIdentifiers(); + IEnumerable splitTags = Tags.ToIdentifiers(); msg.WriteString(string.Join(',', splitTags.Where(t => !base.Prefab.Tags.Contains(t)))); msg.WriteString(string.Join(',', base.Prefab.Tags.Where(t => !splitTags.Contains(t)))); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 73e844d25..300853f29 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -49,7 +49,7 @@ namespace Barotrauma.Networking string[] lines; try { - lines = File.ReadAllLines(LegacySavePath); + lines = File.ReadAllLines(LegacySavePath, catchUnauthorizedAccessExceptions: false); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index f95371efe..5a804f9bc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -125,7 +125,7 @@ namespace Barotrauma.Networking } else { - GameMain.Server.SendChatMessage(txt, senderClient: c, chatMode: chatMode); + GameMain.Server.SendChatMessage(txt, senderClient: c, chatMode: chatMode, type: type == ChatMessageType.Team ? type : null); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index e7f4cd5d6..16245aa68 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -102,13 +102,19 @@ namespace Barotrauma.Networking { get { - if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled) { return 100.0f; } + if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled || GameMain.GameSession?.GameMode is PvPMode) + { + return 100.0f; + } if (HasPermission(ClientPermissions.KarmaImmunity)) { return 100.0f; } return karma; } set { - if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled) { return; } + if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled || GameMain.GameSession?.GameMode is PvPMode) + { + return; + } karma = Math.Min(Math.Max(value, 0.0f), 100.0f); if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index 9b8ab528d..d82f95fc3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -94,7 +94,7 @@ namespace Barotrauma.Networking { try { - Data = File.ReadAllBytes(filePath); + Data = File.ReadAllBytes(filePath, catchUnauthorizedAccessExceptions: false); } catch (System.IO.IOException e) { @@ -400,7 +400,7 @@ namespace Barotrauma.Networking if (GameMain.GameSession != null && !ActiveTransfers.Any(t => t.Connection == inc.Sender && t.FileType == FileTransferType.CampaignSave)) { - StartTransfer(inc.Sender, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); + StartTransfer(inc.Sender, FileTransferType.CampaignSave, GameMain.GameSession.DataPath.LoadPath); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { client.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index d221ecedf..e1d124a06 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -11,6 +11,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Xml.Linq; +using Barotrauma.PerkBehaviors; namespace Barotrauma.Networking { @@ -35,8 +36,10 @@ namespace Barotrauma.Networking private readonly List connectedClients = new List(); - //for keeping track of disconnected clients in case the reconnect shortly after - private readonly List disconnectedClients = new List(); + /// + /// For keeping track of disconnected clients in case they reconnect shortly after. + /// + private readonly List clientsAttemptingToReconnectSoon = new List(); //keeps track of players who've previously been playing on the server //so kick votes persist during the session and the server can let the clients know what name this client used previously @@ -63,6 +66,11 @@ namespace Barotrauma.Networking public float EndRoundTimeRemaining => EndRoundTimer > 0 ? EndRoundDelay - EndRoundTimer : 0; + private const int PvpAutoBalanceCountdown = 10; + private static float pvpAutoBalanceCountdownRemaining = -1; + private int Team1Count => GetPlayingClients().Count(static c => c.TeamID == CharacterTeamType.Team1); + private int Team2Count => GetPlayingClients().Count(static c => c.TeamID == CharacterTeamType.Team2); + /// /// Chat messages that get sent to the owner of the server when the owner is determined /// @@ -126,6 +134,38 @@ namespace Barotrauma.Networking private readonly Option ownerKey; private readonly Option ownerEndpoint; + public void ClearRecentlyDisconnectedClients() + { + lock (clientsAttemptingToReconnectSoon) + { + clientsAttemptingToReconnectSoon.Clear(); + } + } + + public bool FindAndRemoveRecentlyDisconnectedConnection(NetworkConnection conn) + { + lock (clientsAttemptingToReconnectSoon) + { + Client found = null; + foreach (var client in clientsAttemptingToReconnectSoon) + { + if (conn.AddressMatches(client.Connection)) + { + found = client; + break; + } + } + + if (found is not null) + { + clientsAttemptingToReconnectSoon.Remove(found); + return true; + } + } + + return false; + } + public GameServer( string name, int port, @@ -158,7 +198,7 @@ namespace Barotrauma.Networking entityEventManager = new ServerEntityEventManager(this); } - public void StartServer() + public void StartServer(bool registerToServerList) { Log("Starting the server...", ServerLog.MessageType.ServerMessage); @@ -178,8 +218,11 @@ namespace Barotrauma.Networking { Log("Using Lidgren networking. Manual port forwarding may be required. If players cannot connect to the server, you may want to use the in-game hosting menu (which uses Steamworks and EOS networking and does not require port forwarding).", ServerLog.MessageType.ServerMessage); serverPeer = new LidgrenServerPeer(ownerKey, ServerSettings, callbacks); - registeredToSteamMaster = SteamManager.CreateServer(this, ServerSettings.IsPublic); - Eos.EosSessionManager.UpdateOwnedSession(Option.None, ServerSettings); + if (registerToServerList) + { + registeredToSteamMaster = SteamManager.CreateServer(this, ServerSettings.IsPublic); + Eos.EosSessionManager.UpdateOwnedSession(Option.None, ServerSettings); + } } FileSender = new FileSender(serverPeer, MsgConstants.MTU); @@ -464,7 +507,8 @@ namespace Barotrauma.Networking EndRoundDelay = 5.0f; EndRoundTimer += deltaTime; } - else if (isCrewDown && (RespawnManager == null || !RespawnManager.CanRespawnAgain)) + else if (isCrewDown && + (RespawnManager == null || (!RespawnManager.CanRespawnAgain(CharacterTeamType.Team1) && !RespawnManager.CanRespawnAgain(CharacterTeamType.Team2)))) { #if !DEBUG if (EndRoundTimer <= 0.0f) @@ -579,18 +623,14 @@ namespace Barotrauma.Networking wasReadyToStartAutomatically = readyToStartAutomatically; } - for (int i = disconnectedClients.Count - 1; i >= 0; i--) + lock (clientsAttemptingToReconnectSoon) { - disconnectedClients[i].DeleteDisconnectedTimer -= deltaTime; - if (disconnectedClients[i].DeleteDisconnectedTimer > 0.0f) continue; - - if (GameStarted && disconnectedClients[i].Character != null) + foreach (var client in clientsAttemptingToReconnectSoon) { - disconnectedClients[i].Character.Kill(CauseOfDeathType.Disconnected, null); - disconnectedClients[i].Character = null; + client.DeleteDisconnectedTimer -= deltaTime; } - disconnectedClients.RemoveAt(i); + clientsAttemptingToReconnectSoon.RemoveAll(static c => c.DeleteDisconnectedTimer < 0f); } foreach (Client c in connectedClients) @@ -606,6 +646,37 @@ namespace Barotrauma.Networking } } + if (pvpAutoBalanceCountdownRemaining > 0) + { + if (GameStarted || initiatedStartGame || Screen.Selected != GameMain.NetLobbyScreen || + ServerSettings.PvpTeamSelectionMode == PvpTeamSelectionMode.PlayerPreference || ServerSettings.PvpAutoBalanceThreshold == 0) + { + StopAutoBalanceCountdown(); + } + else + { + float prevTimeRemaining = pvpAutoBalanceCountdownRemaining; + pvpAutoBalanceCountdownRemaining -= deltaTime; + if (pvpAutoBalanceCountdownRemaining <= 0) + { + pvpAutoBalanceCountdownRemaining = -1; + RefreshPvpTeamAssignments(autoBalanceNow: true); + } + else + { + // Send a chat message about the countdown every 5 seconds the countdown is running, but not when + // it (=its integer part, which gets printed out) is still at the starting value, or zero + int currentTimeRemainingInteger = (int)Math.Ceiling(pvpAutoBalanceCountdownRemaining); + if (Math.Ceiling(prevTimeRemaining) > currentTimeRemainingInteger && currentTimeRemainingInteger % 5 == 0) + { + SendChatMessage( + TextManager.GetWithVariable("AutoBalance.CountdownRemaining", "[number]", currentTimeRemainingInteger.ToString()).Value, + ChatMessageType.Server); + } + } + } + } + if (connectedClients.Any(c => c.KickAFKTimer >= ServerSettings.KickAFKTime)) { IEnumerable kickAFK = connectedClients.FindAll(c => @@ -761,6 +832,18 @@ namespace Barotrauma.Networking } } break; + case ClientPacketHeader.RESPONSE_CANCEL_STARTGAME: + if (isRoundStartWarningActive) + { + foreach (Client c in connectedClients) + { + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.CANCEL_STARTGAME); + serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable); + } + } + + AbortStartGameIfWarningActive(); + break; case ClientPacketHeader.REQUEST_STARTGAMEFINALIZE: if (connectedClient == null) { @@ -822,7 +905,11 @@ namespace Barotrauma.Networking } else { - string saveName = inc.ReadString(); + string savePath = inc.ReadString(); + bool isBackup = inc.ReadBoolean(); + inc.ReadPadBits(); + uint backupIndex = isBackup ? inc.ReadUInt32() : uint.MinValue; + if (GameStarted) { SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); @@ -832,7 +919,18 @@ namespace Barotrauma.Networking { using (dosProtection.Pause(connectedClient)) { - MultiPlayerCampaign.LoadCampaign(saveName, connectedClient); + CampaignDataPath dataPath; + if (isBackup) + { + string backupPath = SaveUtil.GetBackupPath(savePath, backupIndex); + dataPath = new CampaignDataPath(loadPath: backupPath, savePath: savePath); + } + else + { + dataPath = CampaignDataPath.CreateRegular(savePath); + } + + MultiPlayerCampaign.LoadCampaign(dataPath, connectedClient); } } } @@ -855,6 +953,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.SERVER_SETTINGS: ServerSettings.ServerRead(inc, connectedClient); break; + case ClientPacketHeader.SERVER_SETTINGS_PERKS: + ServerSettings.ReadPerks(inc, connectedClient); + break; case ClientPacketHeader.SERVER_COMMAND: ClientReadServerCommand(inc); break; @@ -897,12 +998,27 @@ namespace Barotrauma.Networking case ClientPacketHeader.UPDATE_CHARACTERINFO: UpdateCharacterInfo(inc, connectedClient); break; + case ClientPacketHeader.REQUEST_BACKUP_INDICES: + SendBackupIndices(inc, connectedClient); + break; case ClientPacketHeader.ERROR: HandleClientError(inc, connectedClient); break; } } + private void SendBackupIndices(IReadMessage inc, Client connectedClient) + { + string savePath = inc.ReadString(); + + var indexData = SaveUtil.GetIndexData(savePath); + + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.SEND_BACKUP_INDICES); + msg.WriteString(savePath); + msg.WriteNetSerializableStruct(indexData.ToNetCollection()); + serverPeer?.Send(msg, connectedClient.Connection, DeliveryMethod.Reliable); + } + private void HandleClientError(IReadMessage inc, Client c) { string errorStr = "Unhandled error report"; @@ -1013,9 +1129,9 @@ namespace Barotrauma.Networking { errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Info.Name); } - if (GameMain.NetworkMember?.RespawnManager?.RespawnShuttle != null) + if (GameMain.NetworkMember?.RespawnManager is { } respawnManager) { - errorLines.Add("Respawn shuttle: " + GameMain.NetworkMember.RespawnManager.RespawnShuttle.Info.Name); + errorLines.Add("Respawn shuttles: " + string.Join(", ", respawnManager.RespawnShuttles.Select(s => s.Info.Name))); } if (Level.Loaded != null) { @@ -1175,6 +1291,10 @@ namespace Barotrauma.Networking { mpCampaign.SendCrewState(); } + else if (GameMain.GameSession.GameMode is PvPMode && c.TeamID == CharacterTeamType.None) + { + AssignClientToPvpTeamMidgame(c); + } c.InGame = true; } } @@ -1386,7 +1506,7 @@ namespace Barotrauma.Networking UInt16 botId = inc.ReadUInt16(); if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign) { return; } - if (ServerSettings.IronmanMode) + if (ServerSettings.IronmanModeActive) { DebugConsole.ThrowError($"Client {sender.Name} has requested to take over a bot in Ironman mode!"); return; @@ -1576,7 +1696,7 @@ namespace Barotrauma.Networking mpCampaign.SavePlayers(); mpCampaign.HandleSaveAndQuit(); GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + SaveUtil.SaveGame(GameMain.GameSession.DataPath); } else { @@ -1609,7 +1729,7 @@ namespace Barotrauma.Networking { using (dosProtection.Pause(sender)) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath, sender); + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.DataPath, sender); } } } @@ -1618,7 +1738,11 @@ namespace Barotrauma.Networking using (dosProtection.Pause(sender)) { Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); - TryStartGame(); + var result = TryStartGame(); + if (result != TryStartGameResult.Success) + { + SendDirectChatMessage(TextManager.Get($"TryStartGameError.{result}").Value, sender, ChatMessageType.Error); + } } } else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))) @@ -1660,24 +1784,27 @@ namespace Barotrauma.Networking } break; case ClientPermissions.SelectSub: - bool isShuttle = inc.ReadBoolean(); - inc.ReadPadBits(); + SelectedSubType subType = (SelectedSubType)inc.ReadByte(); string subHash = inc.ReadString(); var subList = GameMain.NetLobbyScreen.GetSubList(); - var sub = GameMain.NetLobbyScreen.GetSubList().FirstOrDefault(s => s.MD5Hash.StringRepresentation == subHash); + var sub = subList.FirstOrDefault(s => s.MD5Hash.StringRepresentation == subHash); if (sub == null) { DebugConsole.NewMessage($"Client \"{ClientLogName(sender)}\" attempted to select a sub, could not find a sub with the MD5 hash \"{subHash}\".", Color.Red); } else { - if (isShuttle) + switch (subType) { - GameMain.NetLobbyScreen.SelectedShuttle = sub; - } - else - { - GameMain.NetLobbyScreen.SelectedSub = sub; + case SelectedSubType.Shuttle: + GameMain.NetLobbyScreen.SelectedShuttle = sub; + break; + case SelectedSubType.Sub: + GameMain.NetLobbyScreen.SelectedSub = sub; + break; + case SelectedSubType.EnemySub: + GameMain.NetLobbyScreen.SelectedEnemySub = sub; + break; } } break; @@ -1777,7 +1904,7 @@ namespace Barotrauma.Networking if (!FileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave)) { - FileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); + FileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.DataPath.SavePath); c.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)NetTime.Now); } } @@ -2033,6 +2160,9 @@ namespace Barotrauma.Networking segmentTable.StartNewSegment(ServerNetSegment.ClientList); outmsg.WriteUInt16(LastClientListUpdateID); + outmsg.WriteByte((byte)Team1Count); + outmsg.WriteByte((byte)Team2Count); + outmsg.WriteByte((byte)connectedClients.Count); foreach (Client client in connectedClients) { @@ -2046,6 +2176,7 @@ namespace Barotrauma.Networking ? client.Character.Info.Job.Prefab.Identifier : client.PreferredJob, PreferredTeam = client.PreferredTeam, + TeamID = client.TeamID, CharacterId = client.Character == null || !GameStarted ? (ushort)0 : client.Character.ID, Karma = c.HasPermission(ClientPermissions.ServerLog) ? client.Karma : 100.0f, Muted = client.Muted, @@ -2103,9 +2234,21 @@ namespace Barotrauma.Networking } outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.Name); outmsg.WriteString(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); + + if (GameMain.NetLobbyScreen.SelectedEnemySub is { } enemySub) + { + outmsg.WriteBoolean(true); + outmsg.WriteString(enemySub.Name); + outmsg.WriteString(enemySub.MD5Hash.ToString()); + } + else + { + outmsg.WriteBoolean(false); + } + outmsg.WriteBoolean(IsUsingRespawnShuttle()); var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ? - RespawnManager.RespawnShuttle.Info : + RespawnManager.RespawnShuttles.First().Info : GameMain.NetLobbyScreen.SelectedShuttle; outmsg.WriteString(selectedShuttle.Name); outmsg.WriteString(selectedShuttle.MD5Hash.ToString()); @@ -2120,7 +2263,11 @@ namespace Barotrauma.Networking outmsg.WriteSingle(ServerSettings.TraitorProbability); outmsg.WriteRangedInteger(ServerSettings.TraitorDangerLevel, TraitorEventPrefab.MinDangerLevel, TraitorEventPrefab.MaxDangerLevel); - outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All); + outmsg.WriteVariableUInt32((uint)GameMain.NetLobbyScreen.MissionTypes.Count()); + foreach (var missionType in GameMain.NetLobbyScreen.MissionTypes) + { + outmsg.WriteIdentifier(missionType); + } outmsg.WriteByte((byte)GameMain.NetLobbyScreen.SelectedModeIndex); outmsg.WriteString(GameMain.NetLobbyScreen.LevelSeed); @@ -2243,15 +2390,25 @@ namespace Barotrauma.Networking } } - public bool TryStartGame() + public enum TryStartGameResult { - if (initiatedStartGame || GameStarted) { return false; } + Success, + GameAlreadyStarted, + PerksExceedAllowance, + SubmarineNotFound, + GameModeNotSelected, + CannotStartMultiplayerCampaign, + } - GameModePreset selectedMode = + public TryStartGameResult TryStartGame() + { + if (initiatedStartGame || GameStarted) { return TryStartGameResult.GameAlreadyStarted; } + + GameModePreset selectedMode = Voting.HighestVoted(VoteType.Mode, connectedClients) ?? GameMain.NetLobbyScreen.SelectedMode; if (selectedMode == null) { - return false; + return TryStartGameResult.GameModeNotSelected; } if (selectedMode == GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is not MultiPlayerCampaign) { @@ -2260,37 +2417,174 @@ namespace Barotrauma.Networking { GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModePreset.MultiPlayerCampaign.Identifier; } - return false; + return TryStartGameResult.CannotStartMultiplayerCampaign; + } + + bool applyPerks = GameSession.ShouldApplyDisembarkPoints(selectedMode); + if (applyPerks) + { + if (!GameSession.ValidatedDisembarkPoints(selectedMode, GameMain.NetLobbyScreen.MissionTypes)) + { + return TryStartGameResult.PerksExceedAllowance; + } } Log("Starting a new round...", ServerLog.MessageType.ServerMessage); SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; SubmarineInfo selectedSub; + Option selectedEnemySub = Option.None; + if (ServerSettings.AllowSubVoting) { - selectedSub = Voting.HighestVoted(VoteType.Sub, connectedClients); - if (selectedSub == null) { selectedSub = GameMain.NetLobbyScreen.SelectedSub; } + if (selectedMode == GameModePreset.PvP) + { + var team1Voters = connectedClients.Where(static c => c.PreferredTeam == CharacterTeamType.Team1); + var team2Voters = connectedClients.Where(static c => c.PreferredTeam == CharacterTeamType.Team2); + + SubmarineInfo team1Sub = Voting.HighestVoted(VoteType.Sub, team1Voters, out int team1VoteCount); + SubmarineInfo team2Sub = Voting.HighestVoted(VoteType.Sub, team2Voters, out int team2VoteCount); + + // check if anyone on coalition voted for a sub + if (team1VoteCount > 0) + { + // use the most voted one + selectedSub = team1Sub; + } + else + { + selectedSub = team2VoteCount > 0 + ? team2Sub // only separatists voted for a sub, use theirs + : GameMain.NetLobbyScreen.SelectedSub; // nobody voted for a sub so use the default one + } + + // check if separatists voted for a sub + if (team2VoteCount > 0 && team2Sub != null) + { + selectedEnemySub = Option.Some(team2Sub); + } + // no reason to fall back to coalition sub, + // since not selecting an enemy submarine automatically selects the coalition sub + // deeper in the code + } + else + { + selectedSub = Voting.HighestVoted(VoteType.Sub, connectedClients) ?? GameMain.NetLobbyScreen.SelectedSub; + } } else { selectedSub = GameMain.NetLobbyScreen.SelectedSub; + SubmarineInfo enemySub = GameMain.NetLobbyScreen.SelectedEnemySub ?? GameMain.NetLobbyScreen.SelectedSub; + + // Option throws an exception if the value is null, prevent that + if (enemySub != null) + { + selectedEnemySub = Option.Some(enemySub); + } } if (selectedSub == null || selectedShuttle == null) { - return false; + return TryStartGameResult.SubmarineNotFound; + } + + if (applyPerks && CheckIfAnyPerksAreIncompatible(selectedSub, selectedEnemySub.Fallback(selectedSub), selectedMode, out var incompatiblePerks)) + { + CoroutineManager.StartCoroutine(WarnAndDelayStartGame(incompatiblePerks, selectedSub, selectedEnemySub, selectedShuttle, selectedMode), nameof(WarnAndDelayStartGame)); + return TryStartGameResult.Success; } initiatedStartGame = true; - startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedShuttle, selectedMode), "InitiateStartGame"); + startGameCoroutine = CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedEnemySub, selectedShuttle, selectedMode), "InitiateStartGame"); - return true; + return TryStartGameResult.Success; } - - private IEnumerable InitiateStartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode) + private bool CheckIfAnyPerksAreIncompatible(SubmarineInfo team1Sub, SubmarineInfo team2Sub, GameModePreset preset, out PerkCollection incompatiblePerks) { + var incompatibleTeam1Perks = ImmutableArray.CreateBuilder(); + var incompatibleTeam2Perks = ImmutableArray.CreateBuilder(); + bool hasIncompatiblePerks = false; + PerkCollection perks = GameSession.GetPerks(); + + bool ignorePerksThatCanNotApplyWithoutSubmarine = GameSession.ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(preset, GameMain.NetLobbyScreen.MissionTypes); + + foreach (DisembarkPerkPrefab perk in perks.Team1Perks) + { + if (ignorePerksThatCanNotApplyWithoutSubmarine && perk.PerkBehaviors.Any(static p => !p.CanApplyWithoutSubmarine())) { continue; } + bool anyCanNotApply = perk.PerkBehaviors.Any(p => !p.CanApply(team1Sub)); + + if (anyCanNotApply) + { + incompatibleTeam1Perks.Add(perk); + hasIncompatiblePerks = true; + } + } + + if (preset == GameModePreset.PvP) + { + foreach (DisembarkPerkPrefab perk in perks.Team2Perks) + { + if (ignorePerksThatCanNotApplyWithoutSubmarine && perk.PerkBehaviors.Any(static p => !p.CanApplyWithoutSubmarine())) { continue; } + + bool anyCanNotApply = perk.PerkBehaviors.Any(p => !p.CanApply(team2Sub)); + + if (anyCanNotApply) + { + incompatibleTeam2Perks.Add(perk); + hasIncompatiblePerks = true; + } + } + } + + incompatiblePerks = new PerkCollection(incompatibleTeam1Perks.ToImmutable(), incompatibleTeam2Perks.ToImmutable()); + return hasIncompatiblePerks; + } + + private bool isRoundStartWarningActive; + + private void AbortStartGameIfWarningActive() + { + isRoundStartWarningActive = false; + CoroutineManager.StopCoroutines(nameof(WarnAndDelayStartGame)); + } + + private IEnumerable WarnAndDelayStartGame(PerkCollection incompatiblePerks, SubmarineInfo selectedSub, Option selectedEnemySub, SubmarineInfo selectedShuttle, GameModePreset selectedMode) + { + isRoundStartWarningActive = true; + const float warningDuration = 15.0f; + + SerializableDateTime waitUntilTime = SerializableDateTime.UtcNow + TimeSpan.FromSeconds(warningDuration); + if (connectedClients.Any()) + { + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.WARN_STARTGAME); + INetSerializableStruct warnData = new RoundStartWarningData( + RoundStartsAnywaysTimeInSeconds: warningDuration, + Team1Sub: selectedSub.Name, + Team1IncompatiblePerks: ToolBox.PrefabCollectionToUintIdentifierArray(incompatiblePerks.Team1Perks), + Team2Sub: selectedEnemySub.Fallback(selectedSub).Name, + Team2IncompatiblePerks: ToolBox.PrefabCollectionToUintIdentifierArray(incompatiblePerks.Team2Perks)); + msg.WriteNetSerializableStruct(warnData); + + foreach (Client c in connectedClients) + { + serverPeer.Send(msg, c.Connection, DeliveryMethod.Reliable); + } + } + + while (waitUntilTime > SerializableDateTime.UtcNow) + { + yield return CoroutineStatus.Running; + } + + CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedEnemySub, selectedShuttle, selectedMode), "InitiateStartGame"); + yield return CoroutineStatus.Success; + } + + private IEnumerable InitiateStartGame(SubmarineInfo selectedSub, Option selectedEnemySub, SubmarineInfo selectedShuttle, GameModePreset selectedMode) + { + isRoundStartWarningActive = false; initiatedStartGame = true; if (connectedClients.Any()) @@ -2301,6 +2595,17 @@ namespace Barotrauma.Networking msg.WriteString(selectedSub.Name); msg.WriteString(selectedSub.MD5Hash.StringRepresentation); + if (selectedEnemySub.TryUnwrap(out var enemySub)) + { + msg.WriteBoolean(true); + msg.WriteString(enemySub.Name); + msg.WriteString(enemySub.MD5Hash.StringRepresentation); + } + else + { + msg.WriteBoolean(false); + } + msg.WriteBoolean(IsUsingRespawnShuttle()); msg.WriteString(selectedShuttle.Name); msg.WriteString(selectedShuttle.MD5Hash.StringRepresentation); @@ -2339,19 +2644,27 @@ namespace Barotrauma.Networking } } - startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedMode, CampaignSettings.Empty), false); + startGameCoroutine = GameMain.Instance.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedEnemySub, selectedMode, CampaignSettings.Empty), false); yield return CoroutineStatus.Success; } - private IEnumerable StartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, GameModePreset selectedMode, CampaignSettings settings) + private IEnumerable StartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, Option selectedEnemySub, GameModePreset selectedMode, CampaignSettings settings) { + PerkCollection perkCollection = PerkCollection.Empty; + + if (GameSession.ShouldApplyDisembarkPoints(selectedMode)) + { + perkCollection = GameSession.GetPerks(); + } + entityEventManager.Clear(); roundStartSeed = DateTime.Now.Millisecond; Rand.SetSyncedSeed(roundStartSeed); int teamCount = 1; + bool isPvP = selectedMode == GameModePreset.PvP; MultiPlayerCampaign campaign = selectedMode == GameMain.GameSession?.GameMode.Preset ? GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; @@ -2374,31 +2687,38 @@ namespace Barotrauma.Networking if (campaign == null || GameMain.GameSession == null) { traitorManager = new TraitorManager(this); - GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, settings, GameMain.NetLobbyScreen.LevelSeed, missionType: GameMain.NetLobbyScreen.MissionType); + GameMain.GameSession = new GameSession(selectedSub, selectedEnemySub, CampaignDataPath.Empty, selectedMode, settings, GameMain.NetLobbyScreen.LevelSeed, missionTypes: GameMain.NetLobbyScreen.MissionTypes); } else { initialSuppliesSpawned = GameMain.GameSession.SubmarineInfo is { InitialSuppliesSpawned: true }; } - List playingClients = new List(connectedClients); - if (ServerSettings.AllowSpectating) - { - playingClients.RemoveAll(c => c.SpectateOnly); - } - //always allow the server owner to spectate even if it's disallowed in server settings - playingClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); - if (GameMain.GameSession.GameMode is PvPMode pvpMode) { - pvpMode.AssignTeamIDs(playingClients); teamCount = 2; + + // In Player Preference mode, team assignments are handled only at this point, and in Player Choice mode, + // everyone should already have chosen a team, ie. players can no longer make choices now and we should + // finalize all the team assignments without further delay. + RefreshPvpTeamAssignments(assignUnassignedNow: true, autoBalanceNow: true); } else { connectedClients.ForEach(c => c.TeamID = CharacterTeamType.Team1); } + bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || missionMode.Missions.All(m => m.AllowRespawning); + foreach (var mission in GameMain.GameSession.GameMode.Missions) + { + if (mission.Prefab.ForceRespawnMode.HasValue) + { + ServerSettings.RespawnMode = mission.Prefab.ForceRespawnMode.Value; + } + } + + + List playingClients = GetPlayingClients(); if (campaign != null) { if (campaign.Map == null) @@ -2428,7 +2748,7 @@ namespace Barotrauma.Networking else { SendStartMessage(roundStartSeed, GameMain.NetLobbyScreen.LevelSeed, GameMain.GameSession, connectedClients, false); - GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, ServerSettings.SelectedLevelDifficulty); + GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, ServerSettings.SelectedLevelDifficulty, forceBiome: ServerSettings.Biome); Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage); Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); @@ -2447,9 +2767,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Failure; } - bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn); bool isOutpost = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; - if (ServerSettings.RespawnMode != RespawnMode.BetweenRounds && missionAllowRespawn) { RespawnManager = new RespawnManager(this, ServerSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null); @@ -2461,34 +2779,44 @@ namespace Barotrauma.Networking if (isOutpost) { campaign.SendCrewState(); } } - Level.Loaded?.SpawnNPCs(); + if (GameMain.GameSession.Missions.None(m => !m.Prefab.AllowOutpostNPCs)) + { + Level.Loaded?.SpawnNPCs(); + } Level.Loaded?.SpawnCorpses(); Level.Loaded?.PrepareBeaconStation(); AutoItemPlacer.SpawnItems(campaign?.Settings.StartItemSet); - CrewManager crewManager = campaign?.CrewManager; + CrewManager crewManager = GameMain.GameSession.CrewManager; bool hadBots = true; + List team1Characters = new(), + team2Characters = new(); + //assign jobs and spawnpoints separately for each team for (int n = 0; n < teamCount; n++) { var teamID = n == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2; - Submarine.MainSubs[n].TeamID = teamID; - foreach (Item item in Item.ItemList) + Submarine teamSub = Submarine.MainSubs[n]; + if (teamSub != null) { - 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()) + teamSub.TeamID = teamID; + foreach (Item item in Item.ItemList) { - wifiComponent.TeamID = Submarine.MainSubs[n].TeamID; + if (item.Submarine == null) { continue; } + if (item.Submarine != teamSub && !teamSub.DockedTo.Contains(item.Submarine)) { continue; } + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = teamSub.TeamID; + } + } + foreach (Submarine sub in teamSub.DockedTo) + { + if (sub.Info.Type != SubmarineType.Player) { continue; } + sub.TeamID = teamID; } - } - foreach (Submarine sub in Submarine.MainSubs[n].DockedTo) - { - if (sub.Info.Type != SubmarineType.Player) { continue; } - sub.TeamID = teamID; } //find the clients in this team @@ -2522,16 +2850,18 @@ namespace Barotrauma.Networking client.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, client.Name); } characterInfos.Add(client.CharacterInfo); - if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.Prefab) + if (client.CharacterInfo.Job == null || + client.CharacterInfo.Job.Prefab != client.AssignedJob.Prefab || + //always recreate the job to reset the skills in non-campaign modes + campaign == null) { - client.CharacterInfo.Job = new Job(client.AssignedJob.Prefab, Rand.RandSync.Unsynced, client.AssignedJob.Variant); + client.CharacterInfo.Job = new Job(client.AssignedJob.Prefab, isPvP, Rand.RandSync.Unsynced, client.AssignedJob.Variant); } } List bots = new List(); - // do not load new bots if we already have them - if (crewManager == null || !crewManager.HasBots) + if (!crewManager.HasBots || campaign == null) { int botsToSpawn = ServerSettings.BotSpawnMode == BotSpawnMode.Fill ? ServerSettings.BotCount - characterInfos.Count : ServerSettings.BotCount; for (int i = 0; i < botsToSpawn; i++) @@ -2544,32 +2874,21 @@ namespace Barotrauma.Networking bots.Add(botInfo); } - AssignBotJobs(bots, teamID); - if (campaign != null) + AssignBotJobs(bots, teamID, isPvP); + foreach (CharacterInfo bot in bots) { - foreach (CharacterInfo bot in bots) - { - crewManager?.AddCharacterInfo(bot); - } + crewManager.AddCharacterInfo(bot); } - if (crewManager != null) - { - crewManager.HasBots = true; - hadBots = false; - } + crewManager.HasBots = true; + hadBots = false; } List spawnWaypoints = null; - List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[n]).ToList(); + List mainSubWaypoints = teamSub != null ? WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[n]).ToList() : null; if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost()) { - spawnWaypoints = WayPoint.WayPointList.FindAll(wp => - wp.SpawnType == SpawnType.Human && - wp.Submarine == Level.Loaded.StartOutpost && - wp.CurrentHull?.OutpostModuleTags != null && - wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); - + spawnWaypoints = WayPoint.GetOutpostSpawnPoints(teamID); while (spawnWaypoints.Count > characterInfos.Count) { spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count)); @@ -2579,14 +2898,21 @@ namespace Barotrauma.Networking spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]); } } - if (spawnWaypoints == null || !spawnWaypoints.Any()) + if (teamSub != null) { - spawnWaypoints = mainSubWaypoints; + if (spawnWaypoints == null || !spawnWaypoints.Any()) + { + spawnWaypoints = mainSubWaypoints; + } + Debug.Assert(spawnWaypoints.Count == mainSubWaypoints.Count); } - Debug.Assert(spawnWaypoints.Count == mainSubWaypoints.Count); for (int i = 0; i < teamClients.Count; i++) { + //if there's a main sub waypoint available (= the spawnpoint the character would've spawned at, if they'd spawned in the main sub instead of the outpost), + //give the job items based on that spawnpoint + WayPoint jobItemSpawnPoint = mainSubWaypoints != null ? mainSubWaypoints[i] : spawnWaypoints[i]; + Character spawnedCharacter = Character.Create(teamClients[i].CharacterInfo, spawnWaypoints[i].WorldPosition, teamClients[i].CharacterInfo.Name, isRemotePlayer: true, hasAi: false); spawnedCharacter.AnimController.Frozen = true; spawnedCharacter.TeamID = teamID; @@ -2594,7 +2920,7 @@ namespace Barotrauma.Networking var characterData = campaign?.GetClientCharacterData(teamClients[i]); if (characterData == null) { - spawnedCharacter.GiveJobItems(mainSubWaypoints[i]); + spawnedCharacter.GiveJobItems(GameMain.GameSession.GameMode is PvPMode, jobItemSpawnPoint); if (campaign != null) { characterData = campaign.SetClientCharacterData(teamClients[i]); @@ -2606,7 +2932,7 @@ namespace Barotrauma.Networking if (!characterData.HasItemData && !characterData.CharacterInfo.StartItemsGiven) { //clients who've chosen to spawn with the respawn penalty can have CharacterData without inventory data - spawnedCharacter.GiveJobItems(mainSubWaypoints[i]); + spawnedCharacter.GiveJobItems(GameMain.GameSession.GameMode is PvPMode, jobItemSpawnPoint); } else { @@ -2615,7 +2941,7 @@ namespace Barotrauma.Networking characterData.ApplyHealthData(spawnedCharacter); characterData.ApplyOrderData(spawnedCharacter); characterData.ApplyWalletData(spawnedCharacter); - spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); + spawnedCharacter.GiveIdCardTags(jobItemSpawnPoint); spawnedCharacter.LoadTalents(); characterData.HasSpawned = true; } @@ -2631,22 +2957,38 @@ namespace Barotrauma.Networking } spawnedCharacter.SetOwnerClient(teamClients[i]); + AddCharacterToList(teamID, spawnedCharacter); } for (int i = teamClients.Count; i < teamClients.Count + bots.Count; i++) { + WayPoint jobItemSpawnPoint = mainSubWaypoints != null ? mainSubWaypoints[i] : spawnWaypoints[i]; Character spawnedCharacter = Character.Create(characterInfos[i], spawnWaypoints[i].WorldPosition, characterInfos[i].Name, isRemotePlayer: false, hasAi: true); spawnedCharacter.TeamID = teamID; - spawnedCharacter.GiveJobItems(mainSubWaypoints[i]); - spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); + spawnedCharacter.GiveJobItems(GameMain.GameSession.GameMode is PvPMode, jobItemSpawnPoint); + spawnedCharacter.GiveIdCardTags(jobItemSpawnPoint); spawnedCharacter.Info.InventoryData = new XElement("inventory"); spawnedCharacter.Info.StartItemsGiven = true; spawnedCharacter.SaveInventory(); spawnedCharacter.LoadTalents(); + AddCharacterToList(teamID, spawnedCharacter); + } + + void AddCharacterToList(CharacterTeamType team, Character character) + { + switch (team) + { + case CharacterTeamType.Team1: + team1Characters.Add(character); + break; + case CharacterTeamType.Team2: + team2Characters.Add(character); + break; + } } } - if (crewManager != null && crewManager.HasBots) + if (campaign != null && crewManager.HasBots) { if (hadBots) { @@ -2656,7 +2998,7 @@ namespace Barotrauma.Networking else { //created new bots -> save them - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + SaveUtil.SaveGame(GameMain.GameSession.DataPath); } } @@ -2684,10 +3026,12 @@ namespace Barotrauma.Networking GameAnalyticsManager.AddDesignEvent("Traitors:" + (TraitorManager == null ? "Disabled" : "Enabled")); + perkCollection.ApplyAll(team1Characters, team2Characters); + yield return CoroutineStatus.Running; Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false); - + GameMain.GameScreen.Select(); Log("Round started.", ServerLog.MessageType.ServerMessage); @@ -2718,7 +3062,7 @@ namespace Barotrauma.Networking msg.WriteByte((byte)ServerPacketHeader.STARTGAME); msg.WriteInt32(seed); msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier); - bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn); + bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawning); msg.WriteBoolean(ServerSettings.RespawnMode != RespawnMode.BetweenRounds && missionAllowRespawn); msg.WriteBoolean(ServerSettings.AllowDisguises); msg.WriteBoolean(ServerSettings.AllowRewiring); @@ -2728,6 +3072,7 @@ namespace Barotrauma.Networking msg.WriteBoolean(ServerSettings.LockAllDefaultWires); msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat); msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest); + msg.WriteByte((byte)ServerSettings.RespawnMode); msg.WriteBoolean(IsUsingRespawnShuttle()); msg.WriteByte((byte)ServerSettings.LosMode); msg.WriteByte((byte)ServerSettings.ShowEnemyHealthBars); @@ -2742,9 +3087,21 @@ namespace Barotrauma.Networking msg.WriteString(gameSession.SubmarineInfo.Name); msg.WriteString(gameSession.SubmarineInfo.MD5Hash.StringRepresentation); var selectedShuttle = GameStarted && RespawnManager != null && RespawnManager.UsingShuttle ? - RespawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; + RespawnManager.RespawnShuttles.First().Info : GameMain.NetLobbyScreen.SelectedShuttle; msg.WriteString(selectedShuttle.Name); msg.WriteString(selectedShuttle.MD5Hash.StringRepresentation); + + if (gameSession.EnemySubmarineInfo is { } enemySub) + { + msg.WriteBoolean(true); + msg.WriteString(enemySub.Name); + msg.WriteString(enemySub.MD5Hash.StringRepresentation); + } + else + { + msg.WriteBoolean(false); + } + msg.WriteByte((byte)GameMain.GameSession.GameMode.Missions.Count()); foreach (Mission mission in GameMain.GameSession.GameMode.Missions) { @@ -2830,6 +3187,8 @@ namespace Barotrauma.Networking } msg.WriteBoolean(GameMain.GameSession.CrewManager != null); GameMain.GameSession.CrewManager?.ServerWriteActiveOrders(msg); + + msg.WriteBoolean(GameSession.ShouldApplyDisembarkPoints(GameMain.GameSession.GameMode?.Preset)); } public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false, IEnumerable missions = null) @@ -2978,7 +3337,12 @@ namespace Barotrauma.Networking c.NameId = nameId; if (newName == c.Name && newJob == c.PreferredJob && newTeam == c.PreferredTeam) { return false; } c.PreferredJob = newJob; - c.PreferredTeam = newTeam; + + if (newTeam != c.PreferredTeam) + { + c.PreferredTeam = newTeam; + RefreshPvpTeamAssignments(); + } return TryChangeClientName(c, newName); } @@ -3007,8 +3371,6 @@ namespace Barotrauma.Networking public bool IsNameValid(Client c, string newName) { - newName = Client.SanitizeName(newName); - if (c.Connection != OwnerConnection) { if (!Client.IsValidName(newName, ServerSettings)) @@ -3190,6 +3552,16 @@ namespace Barotrauma.Networking previousPlayer = new PreviousPlayer(client); previousPlayers.Add(previousPlayer); } + + if (peerDisconnectPacket.ShouldAttemptReconnect) + { + lock (clientsAttemptingToReconnectSoon) + { + client.DeleteDisconnectedTimer = ServerSettings.KillDisconnectedTime; + clientsAttemptingToReconnectSoon.Add(client); + } + } + previousPlayer.Name = client.Name; previousPlayer.Karma = client.Karma; previousPlayer.KarmaKickCount = client.KarmaKickCount; @@ -3204,6 +3576,12 @@ namespace Barotrauma.Networking serverPeer.Disconnect(client.Connection, peerDisconnectPacket); KarmaManager.OnClientDisconnected(client); + + // A player disconnecting might impact PvP team assignments if still in the lobby + if (!GameStarted) + { + RefreshPvpTeamAssignments(); + } UpdateVoteStatus(); @@ -3376,8 +3754,8 @@ namespace Barotrauma.Networking } else //msg sent by a client { - //game not started -> clients can only send normal and private chatmessages - if (type != ChatMessageType.Private) type = ChatMessageType.Default; + //game not started -> clients can only send normal, private, and team chatmessages + if (type != ChatMessageType.Private && type != ChatMessageType.Team) type = ChatMessageType.Default; senderName = senderClient.Name; } } @@ -3442,6 +3820,10 @@ namespace Barotrauma.Networking //private msg sent to someone else than this client -> don't send if (client != targetClient && client != senderClient) { continue; } break; + case ChatMessageType.Team: + // No need to relay team messages at all to clients in opposing teams (or without a team) + if (client.TeamID == CharacterTeamType.None || client.TeamID != senderClient.TeamID) { continue; } + break; } var chatMsg = ChatMessage.Create( @@ -4040,12 +4422,12 @@ namespace Barotrauma.Networking } } - public void AssignBotJobs(List bots, CharacterTeamType teamID) + public void AssignBotJobs(List bots, CharacterTeamType teamID, bool isPvP) { //shuffle first so the parts where we go through the prefabs //and find ones there's too few of don't always pick the same job List shuffledPrefabs = JobPrefab.Prefabs.Where(static jp => !jp.HiddenJob).ToList(); - shuffledPrefabs.Shuffle(); + shuffledPrefabs.Shuffle(Rand.RandSync.Unsynced); Dictionary assignedPlayerCount = new Dictionary(); foreach (JobPrefab jp in shuffledPrefabs) @@ -4114,7 +4496,7 @@ namespace Barotrauma.Networking void AssignJob(CharacterInfo bot, JobPrefab job) { int variant = Rand.Range(0, job.Variants); - bot.Job = new Job(job, Rand.RandSync.Unsynced, variant); + bot.Job = new Job(job, isPvP, Rand.RandSync.Unsynced, variant); assignedPlayerCount[bot.Job.Prefab]++; unassignedBots.Remove(bot); } @@ -4207,6 +4589,201 @@ namespace Barotrauma.Networking SteamManager.CloseServer(); } } + + private void UpdateClientLobbies() + { + // Triggers a call to WriteClientList(), which causes clients to call GameClient.ReadClientList() + LastClientListUpdateID++; + } + + private List GetPlayingClients() + { + List playingClients = new List(connectedClients); + if (ServerSettings.AllowSpectating) + { + playingClients.RemoveAll(static c => c.SpectateOnly); + } + // Always allow the server owner to spectate even if it's disallowed in server settings + playingClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); + return playingClients; + } + + /// + /// Assigns currently playing clients into PvP teams according to current server settings. + /// + /// Should players without team preference be randomized into teams or given time to choose? + /// Should auto-balance be applied immediately? Otherwise, only the auto-balance countdown is started (in case of imbalance). + public void RefreshPvpTeamAssignments(bool assignUnassignedNow = false, bool autoBalanceNow = false) + { + List team1 = new List(); + List team2 = new List(); + List playingClients = GetPlayingClients(); + + // First assign clients with a team preference/choice into the teams they want (applies in both team selection modes) + List unassignedClients = new List(playingClients); + for (int i = 0; i < unassignedClients.Count; i++) + { + if (unassignedClients[i].PreferredTeam == CharacterTeamType.Team1 || + unassignedClients[i].PreferredTeam == CharacterTeamType.Team2) + { + assignTeam(unassignedClients[i], unassignedClients[i].PreferredTeam); + i--; + } + } + + // Should unassigned players be forced into teams now? (eg. at round start when the time to make choices is over) + if (assignUnassignedNow) + { + if (unassignedClients.Any()) + { + SendChatMessage(TextManager.Get("PvP.WithoutTeamWillBeRandomlyAssigned").Value, ChatMessageType.Server); + } + + // Assign to the team that has the least players + while (unassignedClients.Any()) + { + var randomClient = unassignedClients.GetRandom(Rand.RandSync.Unsynced); + assignTeam(randomClient, team1.Count < team2.Count ? CharacterTeamType.Team1 : CharacterTeamType.Team2); + } + } + + if (ServerSettings.PvpAutoBalanceThreshold > 0) + { + // Deal with team size balance as necessary + int sizeDifference = Math.Abs(team1.Count - team2.Count); + if (sizeDifference > ServerSettings.PvpAutoBalanceThreshold) + { + if (autoBalanceNow) + { + SendChatMessage(TextManager.Get("AutoBalance.Activating").Value, ChatMessageType.Server); + + // Assign a random player from the bigger team into the smaller team until the teams are no longer too imbalanced + while (Math.Abs(team1.Count - team2.Count) > ServerSettings.PvpAutoBalanceThreshold) + { + // Note: team size difference never 0 at this point + var biggerTeam = GetPlayingClients().Where( + c => team1.Count > team2.Count ? + c.TeamID == CharacterTeamType.Team1 : + c.TeamID == CharacterTeamType.Team2) + .ToList(); + switchTeam(biggerTeam.GetRandom(Rand.RandSync.Unsynced), team1.Count < team2.Count ? CharacterTeamType.Team1 : CharacterTeamType.Team2); + } + } + else if (ServerSettings.PvpTeamSelectionMode != PvpTeamSelectionMode.PlayerPreference) + { + // Start a countdown (if not already running) to auto-balancing, so players have a chance to manually rebalance the team before that + if (pvpAutoBalanceCountdownRemaining == -1) + { + SendChatMessage(TextManager.GetWithVariables( + "AutoBalance.CountdownStarted", + ("[teamname]", TextManager.Get(team1.Count > team2.Count ? "teampreference.team1" : "teampreference.team2")), + ("[numberplayers]", (sizeDifference - ServerSettings.PvpAutoBalanceThreshold).ToString()), + ("[numberseconds]", PvpAutoBalanceCountdown.ToString()) + ).Value, ChatMessageType.Server); + pvpAutoBalanceCountdownRemaining = PvpAutoBalanceCountdown; + } + } + } + else + { + // Stop countdown if there was one + StopAutoBalanceCountdown(); + } + } + else + { + // Stop countdown if there was one (eg. if the settings were changed during countdown) + StopAutoBalanceCountdown(); + } + + // Finally, push the assignments to the clients + UpdateClientLobbies(); + + void assignTeam(Client client, CharacterTeamType newTeam) + { + client.TeamID = newTeam; + unassignedClients.Remove(client); + if (newTeam == CharacterTeamType.Team1) + { + team1.Add(client); + } + else if (newTeam == CharacterTeamType.Team2) + { + team2.Add(client); + } + } + + void switchTeam(Client client, CharacterTeamType newTeam) + { + string teamNameVariable = ""; + if (newTeam == CharacterTeamType.Team1) + { + team2.Remove(client); + team1.Add(client); + teamNameVariable = "teampreference.team1"; + } + else if (newTeam == CharacterTeamType.Team2) + { + team1.Remove(client); + team2.Add(client); + teamNameVariable = "teampreference.team2"; + } + SendChatMessage(TextManager.GetWithVariables( + "AutoBalance.PlayerMoved", + ("[clientname]", client.Name), + ("[teamname]", TextManager.Get(teamNameVariable)) + ).Value, ChatMessageType.Server); + client.TeamID = newTeam; + client.PreferredTeam = newTeam; + } + } + + /// + /// Assign a team for single clients who join the server when a round is already running. + /// + public void AssignClientToPvpTeamMidgame(Client client) + { + if (client.PreferredTeam == CharacterTeamType.None) + { + // If teams are currently even, assign the preference-less new player into a random team + if (Team1Count == Team2Count) + { + client.TeamID = Rand.Value() > 0.5f ? CharacterTeamType.Team1 : CharacterTeamType.Team2; + } + else // Otherwise, just assign them to the smaller team + { + client.TeamID = Team1Count < Team2Count ? CharacterTeamType.Team1 : CharacterTeamType.Team2; + } + } + else if (ServerSettings.PvpAutoBalanceThreshold > 0) // Check if the player can be put into their preferred team + { + int newTeam1Count = Team1Count + (client.PreferredTeam == CharacterTeamType.Team1 ? 1 : 0); + int newTeam2Count = Team2Count + (client.PreferredTeam == CharacterTeamType.Team2 ? 1 : 0); + + // Threshold won't be crossed by assigning the player to their preferred team, so do it + if (Math.Abs(newTeam1Count - newTeam2Count) <= ServerSettings.PvpAutoBalanceThreshold) + { + client.TeamID = client.PreferredTeam; + } + else // Preferred team would go against balance threshold, assing the player to the smaller team + { + client.TeamID = Team1Count < Team2Count ? CharacterTeamType.Team1 : CharacterTeamType.Team2; + } + } + else // Nothing stopping us from assigning the player into their preferred team + { + client.TeamID = client.PreferredTeam; + } + } + + private void StopAutoBalanceCountdown() + { + if (pvpAutoBalanceCountdownRemaining != -1) + { + SendChatMessage(TextManager.Get("AutoBalance.CountdownCancelled").Value, ChatMessageType.Server); + } + pvpAutoBalanceCountdownRemaining = -1; + } } class PreviousPlayer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 05cbbc7db..87878e9c4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -269,10 +269,15 @@ namespace Barotrauma.Networking { if (netServer == null) { return; } - switch (inc.SenderConnection.Status) + NetConnectionStatus status = inc.ReadHeader(); + switch (status) { case NetConnectionStatus.Disconnected: LidgrenConnection? conn = connectedClients.Select(c => c.Connection).FirstOrDefault(c => c.NetConnection == inc.SenderConnection); + + string disconnectMsg = inc.ReadString(); + var peerDisconnectPacket = + PeerDisconnectPacket.FromLidgrenStringRepresentation(disconnectMsg).Fallback(PeerDisconnectPacket.WithReason(DisconnectReason.Unknown)); if (conn != null) { if (conn == OwnerConnection) @@ -283,7 +288,7 @@ namespace Barotrauma.Networking } else { - Disconnect(conn, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); + Disconnect(conn, peerDisconnectPacket); } } else @@ -291,7 +296,7 @@ namespace Barotrauma.Networking PendingClient? pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == inc.SenderConnection); if (pendingClient != null) { - RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected)); + RemovePendingClient(pendingClient, peerDisconnectPacket); } } @@ -332,7 +337,9 @@ namespace Barotrauma.Networking if (status == Steamworks.AuthResponse.OK) { pendingClient.Connection.SetAccountInfo(new AccountInfo(new SteamId(steamId), new SteamId(ownerId))); - pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; + pendingClient.InitializationStep = ShouldAskForPassword(serverSettings, pendingClient.Connection) + ? ConnectionInitialization.Password + : ConnectionInitialization.ContentPackageOrder; pendingClient.UpdateTime = Timing.TotalTime; } else @@ -450,7 +457,9 @@ namespace Barotrauma.Networking pendingClient.Connection.SetAccountInfo(accountInfo); pendingClient.Name = packet.Name; pendingClient.OwnerKey = packet.OwnerKey; - pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; + pendingClient.InitializationStep = ShouldAskForPassword(serverSettings, pendingClient.Connection) + ? ConnectionInitialization.Password + : ConnectionInitialization.ContentPackageOrder; } void rejectClient() @@ -470,7 +479,7 @@ namespace Barotrauma.Networking if (authenticators is null || !packet.AuthTicket.TryUnwrap(out var authTicket) || !authenticators.TryGetValue(authTicket.Kind, out var authenticator)) - { + { #if DEBUG DebugConsole.NewMessage("Debug server accepts unauthenticated connections", Microsoft.Xna.Framework.Color.Yellow); acceptClient(new AccountInfo(new UnauthenticatedAccountId(packet.Name))); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs index 4781c5e0a..3b317e7a7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/P2PServerPeer.cs @@ -344,7 +344,9 @@ namespace Barotrauma.Networking { // Do nothing with the auth ticket because that should be handled by the owner peer, // just assume that authentication succeeded - pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; + pendingClient.InitializationStep = ShouldAskForPassword(serverSettings, pendingClient.Connection) + ? ConnectionInitialization.Password + : ConnectionInitialization.ContentPackageOrder; pendingClient.Name = packet.Name; pendingClient.AuthSessionStarted = true; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index c9329cf3e..6a19c93ad 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -359,5 +359,18 @@ namespace Barotrauma.Networking } protected static void LogMalformedMessage() => DebugConsole.ThrowError("Received malformed message from remote peer."); + + protected bool ShouldAskForPassword(ServerSettings serverSettings, NetworkConnection connection) + { + if (!serverSettings.HasPassword) { return false; } + + if (GameMain.Server is { } server && server.FindAndRemoveRecentlyDisconnectedConnection(connection)) + { + // do not ask passwords from clients that have recently disconnected + return false; + } + + return true; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index f0b0e7a80..922c5a079 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -8,16 +9,11 @@ namespace Barotrauma.Networking { partial class RespawnManager : Entity, IServerSerializable { - private DateTime despawnTime; - private float shuttleEmptyTimer; - private int pendingRespawnCount, requiredRespawnCount; - private int prevPendingRespawnCount, prevRequiredRespawnCount; + public bool IsShuttleInsideLevel => RespawnShuttles.Any(s => s.WorldPosition.Y < Level.Loaded.Size.Y); - public bool IsShuttleInsideLevel => RespawnShuttle != null && RespawnShuttle.WorldPosition.Y < Level.Loaded.Size.Y; - - private IEnumerable GetClientsToRespawn() + private IEnumerable GetClientsToRespawn(CharacterTeamType teamId) { MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; foreach (Client c in networkMember.ConnectedClients) @@ -25,6 +21,10 @@ namespace Barotrauma.Networking if (!c.InGame) { continue; } if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { continue; } if (c.Character != null && !c.Character.IsDead) { continue; } + if (c.TeamID != CharacterTeamType.None && c.TeamID != teamId) + { + continue; + } var matchingData = campaign?.GetClientCharacterData(c); @@ -80,36 +80,42 @@ namespace Barotrauma.Networking return false; } - private static List GetBotsToRespawn() + private static List GetBotsToRespawn(CharacterTeamType teamId) { + //this works under the assumption that GetCharacterInfos only returns bots in MP + var botInfos = GameMain.GameSession.CrewManager.GetCharacterInfos() + .Where(botInfo => botInfo.TeamID == teamId) + //filter out players in case a player has been given control of a bot using console commands + .Where(botInfo => GameMain.Server.ConnectedClients.None(c => c.CharacterInfo == botInfo)) + .ToList(); + if (GameMain.Server.ServerSettings.BotSpawnMode == BotSpawnMode.Normal) { - return Character.CharacterList - .FindAll(c => c.TeamID == CharacterTeamType.Team1 && c.AIController != null && c.Info != null && c.IsDead) - .Select(c => c.Info) - .ToList(); + return botInfos.Where(ci => ci.Character == null || ci.Character.IsDead).ToList(); } int currPlayerCount = GameMain.Server.ConnectedClients.Count(c => c.InGame && (!c.SpectateOnly || (!GameMain.Server.ServerSettings.AllowSpectating && GameMain.Server.OwnerConnection != c.Connection))); - var existingBots = Character.CharacterList - .FindAll(c => c.TeamID == CharacterTeamType.Team1 && c.AIController != null && c.Info != null); - + var existingBots = Character.CharacterList.FindAll(c => c.IsBot && !c.IsDead && c.TeamID == teamId); int requiredBots = GameMain.Server.ServerSettings.BotCount - currPlayerCount; requiredBots -= existingBots.Count(b => !b.IsDead); List botsToRespawn = new List(); for (int i = 0; i < requiredBots; i++) { - CharacterInfo botToRespawn = existingBots.Find(b => b.IsDead)?.Info; + CharacterInfo botToRespawn = botInfos.FirstOrDefault(b => b.Character == null || b.Character.IsDead); if (botToRespawn == null) { - botToRespawn = new CharacterInfo(CharacterPrefab.HumanSpeciesName); + botToRespawn = new CharacterInfo(CharacterPrefab.HumanSpeciesName) + { + TeamID = teamId + }; } else { + botInfos.Remove(botToRespawn); existingBots.Remove(botToRespawn.Character); } botsToRespawn.Add(botToRespawn); @@ -117,9 +123,27 @@ namespace Barotrauma.Networking return botsToRespawn; } - private bool ShouldStartRespawnCountdown() + private string GetRespawnShuttleText(CharacterTeamType team) { - int characterToRespawnCount = GetClientsToRespawn().Count(); + if (teamSpecificStates.Count == 1) + { + return "respawn shuttle"; + } + return team == CharacterTeamType.Team1 ? "respawn shuttle (team 1)" : "respawn shuttle (team 2)"; + } + private string GetTeamNameText(CharacterTeamType team) + { + if (teamSpecificStates.Count == 1) + { + return "everyone"; + } + return team == CharacterTeamType.Team1 ? "team 1" : "team 2"; + } + + + private bool ShouldStartRespawnCountdown(TeamSpecificState teamSpecificState) + { + int characterToRespawnCount = GetClientsToRespawn(teamSpecificState.TeamID).Count(); return ShouldStartRespawnCountdown(characterToRespawnCount); } @@ -133,108 +157,91 @@ namespace Barotrauma.Networking return characterToRespawnCount >= GetMinCharactersToRespawn(); } - partial void UpdateWaiting(float _) + partial void UpdateWaiting(TeamSpecificState teamSpecificState) { - if (RespawnShuttle != null) + //no respawns in the first minute of the round - otherwise it can be that bots + //are respawned to "fill" the spots of players who are taking a long time to load in + if (GameMain.GameSession is { RoundDuration: < 60 }) { - RespawnShuttle.Velocity = Vector2.Zero; + return; } - pendingRespawnCount = GetClientsToRespawn().Count(); - requiredRespawnCount = GetMinCharactersToRespawn(); - if (pendingRespawnCount != prevPendingRespawnCount || - requiredRespawnCount != prevRequiredRespawnCount) + var teamId = teamSpecificState.TeamID; + var respawnShuttle = GetShuttle(teamId); + if (respawnShuttle != null) { - prevPendingRespawnCount = pendingRespawnCount; - prevRequiredRespawnCount = requiredRespawnCount; + respawnShuttle.Velocity = Vector2.Zero; + } + + teamSpecificState.PendingRespawnCount = GetClientsToRespawn(teamId).Count(); + if (GameMain.GameSession?.Campaign == null) + { + teamSpecificState.PendingRespawnCount += GetBotsToRespawn(teamId).Count; + } + teamSpecificState.RequiredRespawnCount = GetMinCharactersToRespawn(); + if (teamSpecificState.PendingRespawnCount != teamSpecificState.PrevPendingRespawnCount || + teamSpecificState.RequiredRespawnCount != teamSpecificState.PrevRequiredRespawnCount) + { + teamSpecificState.PrevPendingRespawnCount = teamSpecificState.PendingRespawnCount; + teamSpecificState.PrevRequiredRespawnCount = teamSpecificState.RequiredRespawnCount; GameMain.Server.CreateEntityEvent(this); } - if (RespawnCountdownStarted) + if (teamSpecificState.RespawnCountdownStarted) { - if (pendingRespawnCount == 0) + if (teamSpecificState.PendingRespawnCount == 0) { - RespawnCountdownStarted = false; + teamSpecificState.RespawnCountdownStarted = false; GameMain.Server.CreateEntityEvent(this); } } else { - bool shouldStartCountdown = ShouldStartRespawnCountdown(pendingRespawnCount); + bool shouldStartCountdown = ShouldStartRespawnCountdown(teamSpecificState.PendingRespawnCount); if (shouldStartCountdown) { - RespawnCountdownStarted = true; - if (RespawnTime < DateTime.Now) + teamSpecificState.RespawnCountdownStarted = true; + if (teamSpecificState.RespawnTime < DateTime.Now) { - RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, (int)(GameMain.Server.ServerSettings.RespawnInterval * 1000.0f)); + teamSpecificState.RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, (int)(GameMain.Server.ServerSettings.RespawnInterval * 1000.0f)); } GameMain.Server.CreateEntityEvent(this); - } + } } - if (RespawnCountdownStarted && DateTime.Now > RespawnTime) + if (teamSpecificState.RespawnCountdownStarted && DateTime.Now > teamSpecificState.RespawnTime) { - DispatchShuttle(); - RespawnCountdownStarted = false; + DispatchShuttle(teamSpecificState); + teamSpecificState.RespawnCountdownStarted = false; } } - private void DispatchShuttle() + private void DispatchShuttle(TeamSpecificState teamSpecificState) { - if (RespawnShuttle != null) + if (RespawnShuttles.Any()) { - CurrentState = State.Transporting; + ResetShuttle(teamSpecificState); + teamSpecificState.CurrentState = State.Transporting; GameMain.Server.CreateEntityEvent(this); - - ResetShuttle(); - - if (shuttleSteering != null) - { - shuttleSteering.TargetVelocity = Vector2.Zero; - } - - Vector2 spawnPos = FindSpawnPos(); - RespawnCharacters(spawnPos, out bool anyCharacterSpawnedInShuttle); - if (anyCharacterSpawnedInShuttle) - { - GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning); - CoroutineManager.StopCoroutines("forcepos"); - if (spawnPos.Y > Level.Loaded.Size.Y) - { - CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos"); - } - else - { - RespawnShuttle.SetPosition(spawnPos); - RespawnShuttle.Velocity = Vector2.Zero; - RespawnShuttle.NeutralizeBallast(); - RespawnShuttle.EnableMaintainPosition(); - } - } - else - { - GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning); - } } else { - CurrentState = State.Waiting; - GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning); + teamSpecificState.CurrentState = State.Waiting; + GameServer.Log($"Respawning {GetTeamNameText(teamSpecificState.TeamID)} in the main sub.", ServerLog.MessageType.Spawning); GameMain.Server.CreateEntityEvent(this); - - RespawnCharacters(shuttlePos: null, out _); } + RespawnCharacters(teamSpecificState); } - partial void UpdateReturningProjSpecific(float deltaTime) + partial void UpdateReturningProjSpecific(TeamSpecificState teamSpecificState, float deltaTime) { //speed up despawning if there's no-one inside the shuttle - if (despawnTime > DateTime.Now + new TimeSpan(0, 0, seconds: 30) && CheckShuttleEmpty(deltaTime)) + if (teamSpecificState.DespawnTime > DateTime.Now + new TimeSpan(0, 0, seconds: 30) && CheckShuttleEmpty(deltaTime)) { - despawnTime = DateTime.Now + new TimeSpan(0, 0, seconds: 30); + teamSpecificState.DespawnTime = DateTime.Now + new TimeSpan(0, 0, seconds: 30); } - foreach (Door door in shuttleDoors) + foreach (Door door in shuttleDoors[teamSpecificState.TeamID]) { if (door.IsOpen) { @@ -242,87 +249,71 @@ namespace Barotrauma.Networking } } - var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == RespawnShuttle && g.ConnectedWall != null); + var shuttleGaps = Gap.GapList.FindAll(g => RespawnShuttles.Contains(g.Submarine) && g.ConnectedWall != null); shuttleGaps.ForEach(g => Spawner.AddEntityToRemoveQueue(g)); - var dockingPorts = Item.ItemList.FindAll(i => i.Submarine == RespawnShuttle && i.GetComponent() != null); + var dockingPorts = Item.ItemList.FindAll(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent() != null); dockingPorts.ForEach(d => d.GetComponent().Undock()); - //shuttle has returned if the path has been traversed or the shuttle is close enough to the exit - if (!CoroutineManager.IsCoroutineRunning("forcepos")) + if (!IsShuttleInsideLevel || DateTime.Now > teamSpecificState.DespawnTime) { - if ((shuttleSteering?.SteeringPath != null && shuttleSteering.SteeringPath.Finished) - || (RespawnShuttle.WorldPosition.Y + RespawnShuttle.Borders.Y > Level.Loaded.StartPosition.Y - Level.ShaftHeight && - Math.Abs(Level.Loaded.StartPosition.X - RespawnShuttle.WorldPosition.X) < 1000.0f)) - { - CoroutineManager.StopCoroutines("forcepos"); - CoroutineManager.StartCoroutine( - ForceShuttleToPos(new Vector2(Level.Loaded.StartPosition.X, Level.Loaded.Size.Y + 1000.0f), 100.0f), "forcepos"); + ResetShuttle(teamSpecificState); - } - } - - if (!IsShuttleInsideLevel || DateTime.Now > despawnTime) - { - CoroutineManager.StopCoroutines("forcepos"); - - ResetShuttle(); - - CurrentState = State.Waiting; - GameServer.Log("The respawn shuttle has left.", ServerLog.MessageType.Spawning); + teamSpecificState.CurrentState = State.Waiting; + GameServer.Log($"The {GetRespawnShuttleText(teamSpecificState.TeamID)} has left.", ServerLog.MessageType.Spawning); GameMain.Server.CreateEntityEvent(this); - RespawnCountdownStarted = false; - ReturnCountdownStarted = false; + teamSpecificState.RespawnCountdownStarted = false; + teamSpecificState.ReturnCountdownStarted = false; } } - partial void UpdateTransportingProjSpecific(float deltaTime) + partial void UpdateTransportingProjSpecific(TeamSpecificState teamSpecificState, float deltaTime) { - if (!ReturnCountdownStarted) + if (!teamSpecificState.ReturnCountdownStarted) { //if there are no living chracters inside, transporting can be stopped immediately if (CheckShuttleEmpty(deltaTime)) { - ReturnTime = DateTime.Now; - ReturnCountdownStarted = true; + teamSpecificState.ReturnTime = DateTime.Now; + teamSpecificState.ReturnCountdownStarted = true; } - else if (!ShouldStartRespawnCountdown()) + else if (!ShouldStartRespawnCountdown(teamSpecificState)) { //don't start counting down until someone else needs to respawn - ReturnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(maxTransportTime * 1000)); - despawnTime = ReturnTime + new TimeSpan(0, 0, seconds: 30); + teamSpecificState.ReturnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(maxTransportTime * 1000)); + teamSpecificState.DespawnTime = teamSpecificState.ReturnTime + new TimeSpan(0, 0, seconds: 30); return; } else { - ReturnCountdownStarted = true; + teamSpecificState.ReturnCountdownStarted = true; GameMain.Server.CreateEntityEvent(this); } } else if (CheckShuttleEmpty(deltaTime)) { - ReturnTime = DateTime.Now; + teamSpecificState.ReturnTime = DateTime.Now; } - if (DateTime.Now > ReturnTime) + if (DateTime.Now > teamSpecificState.ReturnTime) { if (IsShuttleInsideLevel) { - GameServer.Log("The respawn shuttle is leaving.", ServerLog.MessageType.ServerMessage); + GameServer.Log($"The {GetRespawnShuttleText(teamSpecificState.TeamID)} is leaving.", ServerLog.MessageType.ServerMessage); } - CurrentState = State.Returning; + teamSpecificState.CurrentState = State.Returning; GameMain.Server.CreateEntityEvent(this); - RespawnCountdownStarted = false; + teamSpecificState.RespawnCountdownStarted = false; maxTransportTime = GameMain.Server.ServerSettings.MaxTransportTime; } } private bool CheckShuttleEmpty(float deltaTime) { - if (!Character.CharacterList.Any(c => c.Submarine == RespawnShuttle && !c.IsDead)) + if (RespawnShuttles.All(respawnShuttle => Character.CharacterList.None(c => c.Submarine == respawnShuttle && !c.IsDead))) { shuttleEmptyTimer += deltaTime; } @@ -333,15 +324,18 @@ namespace Barotrauma.Networking return shuttleEmptyTimer > 1.0f; } - private void RespawnCharacters(Vector2? shuttlePos, out bool anyCharacterSpawnedInShuttle) + private void RespawnCharacters(TeamSpecificState teamSpecificState) { - respawnedCharacters.Clear(); - - var respawnSub = RespawnShuttle ?? Submarine.MainSub; + var teamID = teamSpecificState.TeamID; + int teamIndex = teamID == CharacterTeamType.Team1 ? 0 : 1; + bool anyCharacterSpawnedInShuttle = false; + teamSpecificState.RespawnedCharacters.Clear(); MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; + bool isPvPMode = GameMain.GameSession.GameMode is PvPMode; + int teamCount = isPvPMode ? 2 : 1; - var clients = GetClientsToRespawn().ToList(); + var clients = GetClientsToRespawn(teamID).ToList(); foreach (Client c in clients) { // Get rid of the existing character @@ -355,18 +349,31 @@ namespace Barotrauma.Networking c.CharacterInfo = matchingData.CharacterInfo; } - //all characters are in Team 1 in game modes/missions with only one team. - //if at some point we add a game mode with multiple teams where respawning is possible, this needs to be reworked - c.TeamID = CharacterTeamType.Team1; c.CharacterInfo ??= new CharacterInfo(CharacterPrefab.HumanSpeciesName, c.Name); + + //force everyone to team 1 if there's just one team + if (teamCount == 1) + { + c.TeamID = teamID; + } + else if (isPvPMode && c.TeamID == CharacterTeamType.None) + { + GameMain.Server.AssignClientToPvpTeamMidgame(c); + } + c.CharacterInfo.TeamID = c.TeamID; } List characterInfos = clients.Select(c => c.CharacterInfo).ToList(); //bots don't respawn in the campaign + var botsToSpawn = GetBotsToRespawn(teamID); if (campaign == null) { - var botsToSpawn = GetBotsToRespawn(); characterInfos.AddRange(botsToSpawn); + foreach (var bot in botsToSpawn) + { + // Get rid of the existing bots' corpses + if (bot.Character is Character character) { character.DespawnNow(); } + } } GameMain.Server.AssignJobs(clients); @@ -374,38 +381,61 @@ namespace Barotrauma.Networking { if (campaign?.GetClientCharacterData(c) == null || c.CharacterInfo.Job == null) { - c.CharacterInfo.Job = new Job(c.AssignedJob.Prefab, Rand.RandSync.Unsynced, c.AssignedJob.Variant); + c.CharacterInfo.Job = new Job(c.AssignedJob.Prefab, isPvPMode, Rand.RandSync.Unsynced, c.AssignedJob.Variant); } } - //the spawnpoints where the characters will spawn - var shuttleSpawnPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, respawnSub); - //the spawnpoints where they would spawn if they were spawned inside the main sub - //(in order to give them appropriate ID card tags) - var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub); + System.Diagnostics.Debug.Assert(characterInfos.All(c => c.TeamID == teamID), + "List of characters to respawn contained characters from the wrong team."); + + Submarine mainSub = Submarine.MainSubs[teamIndex]; + Submarine respawnSub = null; + + Submarine respawnShuttle = GetShuttle(teamID); + Vector2? shuttlePos = null; + if (respawnShuttle != null) + { + respawnSub = respawnShuttle; + shuttlePos = FindSpawnPos(respawnShuttle, mainSub); + } + + respawnSub ??= mainSub ?? Level.Loaded.StartOutpost; ItemPrefab divingSuitPrefab = null; if ((shuttlePos != null && Level.Loaded.GetRealWorldDepth(shuttlePos.Value.Y) > Level.DefaultRealWorldCrushDepth) || - Level.Loaded.GetRealWorldDepth(Submarine.MainSub.WorldPosition.Y) > Level.DefaultRealWorldCrushDepth) + (mainSub != null && Level.Loaded.GetRealWorldDepth(mainSub.WorldPosition.Y) > Level.DefaultRealWorldCrushDepth)) { divingSuitPrefab = ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuitdeep")); } - divingSuitPrefab ??= + divingSuitPrefab ??= ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuit")) ?? ItemPrefab.Find(null, "divingsuit".ToIdentifier()); ItemPrefab oxyPrefab = ItemPrefab.Find(null, "oxygentank".ToIdentifier()); ItemPrefab scooterPrefab = ItemPrefab.Find(null, "underwaterscooter".ToIdentifier()); ItemPrefab batteryPrefab = ItemPrefab.Find(null, "batterycell".ToIdentifier()); - var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo); + //the spawnpoints where the characters will spawn + var selectedSpawnPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, respawnSub); + if (isPvPMode && Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost()) + { + var spawnWaypoints = WayPoint.GetOutpostSpawnPoints(teamID); + for (int i = 0; i < characterInfos.Count; i++) + { + selectedSpawnPoints[i] = spawnWaypoints.GetRandomUnsynced(); + } + } - anyCharacterSpawnedInShuttle = false; + //the spawnpoints where they would spawn if they were spawned inside the main sub + //(in order to give them appropriate ID card tags) + var mainSubSpawnPoints = mainSub != null ? WayPoint.SelectCrewSpawnPoints(characterInfos, mainSub) : null; + var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo); for (int i = 0; i < characterInfos.Count; i++) { - bool bot = i >= clients.Count; + var characterInfo = characterInfos[i]; - characterInfos[i].ClearCurrentOrders(); + bool bot = botsToSpawn.Contains(characterInfo); + characterInfo.ClearCurrentOrders(); CharacterCampaignData characterCampaignData = null; bool forceSpawnInMainSub = false; @@ -413,7 +443,7 @@ namespace Barotrauma.Networking { //the client has opted to change the name of their new character //when the character spawns, set the client's name to match - if (clients[i].PendingName == characterInfos[i].Name) + if (clients[i].PendingName == characterInfo.Name) { GameMain.Server?.TryChangeClientName(clients[i], clients[i].PendingName); clients[i].PendingName = null; @@ -428,32 +458,31 @@ namespace Barotrauma.Networking } else { - ReduceCharacterSkillsOnDeath(characterInfos[i]); - characterInfos[i].RemoveSavedStatValuesOnDeath(); - characterInfos[i].CauseOfDeath = null; + ReduceCharacterSkillsOnDeath(characterInfo); + characterInfo.RemoveSavedStatValuesOnDeath(); + characterInfo.CauseOfDeath = null; } } } - if (!forceSpawnInMainSub) + if (!forceSpawnInMainSub && respawnShuttle != null) { - anyCharacterSpawnedInShuttle = true; + anyCharacterSpawnedInShuttle = true; } - var character = Character.Create(characterInfos[i], (forceSpawnInMainSub ? mainSubSpawnPoints[i] : shuttleSpawnPoints[i]).WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot); + var character = Character.Create(characterInfo, (forceSpawnInMainSub ? mainSubSpawnPoints[i] : selectedSpawnPoints[i]).WorldPosition, characterInfo.Name, isRemotePlayer: !bot, hasAi: bot); characterCampaignData?.ApplyWalletData(character); - character.TeamID = CharacterTeamType.Team1; character.LoadTalents(); - if (characterInfos[i].LastRewardDistribution.TryUnwrap(out int salary)) + if (characterInfo.LastRewardDistribution.TryUnwrap(out int salary)) { character.Wallet.SetRewardDistribution(salary); } - respawnedCharacters.Add(character); + teamSpecificState.RespawnedCharacters.Add(character); if (bot) { - GameServer.Log(string.Format("Respawning bot {0} as {1}", character.Info.Name, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning); + GameServer.Log(string.Format("Respawning bot {0} as {1}.", character.Info.Name, characterInfo.Job.Name), ServerLog.MessageType.Spawning); } else { @@ -475,11 +504,18 @@ namespace Barotrauma.Networking clients[i].Character = character; character.SetOwnerClient(clients[i]); GameServer.Log( - $"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning); + $"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfo.Job.Name}.", ServerLog.MessageType.Spawning); } - if (RespawnShuttle != null && anyCharacterSpawnedInShuttle) + if (respawnShuttle != null && anyCharacterSpawnedInShuttle) { + GameServer.Log($"Dispatching the {GetRespawnShuttleText(teamID)}.", ServerLog.MessageType.Spawning); + respawnShuttle.SetPosition(shuttlePos.Value); + respawnShuttle.Velocity = Vector2.Zero; + respawnShuttle.NeutralizeBallast(); + respawnShuttle.EnableMaintainPosition(); + shuttleSteering[teamID].ForEach(s => s.TargetVelocity = Vector2.Zero); + List newRespawnItems = new List(); Vector2 pos = cargoSp?.Position ?? character.Position; if (divingSuitPrefab != null) @@ -513,25 +549,29 @@ namespace Barotrauma.Networking } } } - if (respawnContainer != null) - { - AutoItemPlacer.RegenerateLoot(RespawnShuttle, respawnContainer); - } - //try to put the items in containers in the shuttle foreach (var respawnItem in newRespawnItems) { System.Diagnostics.Debug.Assert(!respawnItem.Removed); - foreach (Item shuttleItem in RespawnShuttle.GetItems(alsoFromConnectedSubs: false)) + //already in a container (a battery we just placed in a scooter?) -> don't move to a cabinet + if (respawnItem.Container == null) { - if (shuttleItem.NonInteractable || shuttleItem.NonPlayerTeamInteractable) { continue; } - var container = shuttleItem.GetComponent(); - if (container != null && container.Inventory.TryPutItem(respawnItem, user: null)) + foreach (Item shuttleItem in respawnShuttle.GetItems(alsoFromConnectedSubs: false)) { - break; + if (shuttleItem.NonInteractable || shuttleItem.NonPlayerTeamInteractable) { continue; } + var container = shuttleItem.GetComponent(); + if (container != null && container.Inventory.TryPutItem(respawnItem, user: null)) + { + break; + } } } - respawnItems.Add(respawnItem); + teamSpecificState.RespawnItems.Add(respawnItem); + } + + foreach (var respawnContainer in respawnContainers[teamID]) + { + teamSpecificState.RespawnItems.AddRange(AutoItemPlacer.RegenerateLoot(respawnShuttle, respawnContainer)); } } @@ -541,10 +581,11 @@ namespace Barotrauma.Networking { ReduceCharacterSkillsOnDeath(characterInfos[i], applyExtraSkillLoss: true); } + WayPoint jobItemSpawnPoint = mainSubSpawnPoints != null ? mainSubSpawnPoints[i] : selectedSpawnPoints[i]; if (characterData == null || characterData.HasSpawned) { //give the character the items they would've gotten if they had spawned in the main sub - character.GiveJobItems(mainSubSpawnPoints[i]); + character.GiveJobItems(isPvPMode, jobItemSpawnPoint); if (campaign != null) { characterData = campaign.SetClientCharacterData(clients[i]); @@ -559,16 +600,17 @@ namespace Barotrauma.Networking } else { - character.GiveJobItems(mainSubSpawnPoints[i]); + character.GiveJobItems(isPvPMode, jobItemSpawnPoint); } characterData.ApplyHealthData(character); - character.GiveIdCardTags(mainSubSpawnPoints[i]); + character.GiveIdCardTags(jobItemSpawnPoint); characterData.HasSpawned = true; } //add the ID card tags they should've gotten when spawning in the shuttle - character.GiveIdCardTags(shuttleSpawnPoints[i], createNetworkEvent: true); + character.GiveIdCardTags(selectedSpawnPoints[i], createNetworkEvent: true); } + } /// @@ -605,24 +647,29 @@ namespace Barotrauma.Networking public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.WriteRangedInteger((int)CurrentState, 0, Enum.GetNames(typeof(State)).Length); - - switch (CurrentState) + msg.WriteByte((byte)c.TeamID); + foreach (var teamSpecificState in teamSpecificStates.Values) { - case State.Transporting: - msg.WriteBoolean(ReturnCountdownStarted); - msg.WriteSingle(GameMain.Server.ServerSettings.MaxTransportTime); - msg.WriteSingle((float)(ReturnTime - DateTime.Now).TotalSeconds); - break; - case State.Waiting: - msg.WriteUInt16((ushort)pendingRespawnCount); - msg.WriteUInt16((ushort)requiredRespawnCount); - msg.WriteBoolean(IsRespawnDecisionPendingForClient(c)); - msg.WriteBoolean(RespawnCountdownStarted); - msg.WriteSingle((float)(RespawnTime - DateTime.Now).TotalSeconds); - break; - case State.Returning: - break; + msg.WriteByte((byte)teamSpecificState.TeamID); + msg.WriteRangedInteger((int)teamSpecificState.CurrentState, 0, Enum.GetNames(typeof(State)).Length); + + switch (teamSpecificState.CurrentState) + { + case State.Transporting: + msg.WriteBoolean(teamSpecificState.ReturnCountdownStarted); + msg.WriteSingle(GameMain.Server.ServerSettings.MaxTransportTime); + msg.WriteSingle((float)(teamSpecificState.ReturnTime - DateTime.Now).TotalSeconds); + break; + case State.Waiting: + msg.WriteUInt16((ushort)teamSpecificState.PendingRespawnCount); + msg.WriteUInt16((ushort)teamSpecificState.RequiredRespawnCount); + msg.WriteBoolean(IsRespawnDecisionPendingForClient(c)); + msg.WriteBoolean(teamSpecificState.RespawnCountdownStarted); + msg.WriteSingle((float)(teamSpecificState.RespawnTime - DateTime.Now).TotalSeconds); + break; + case State.Returning: + break; + } } msg.WritePadBits(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index b474f5548..610206882 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -2,9 +2,9 @@ using Barotrauma.IO; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.Steam; namespace Barotrauma.Networking { @@ -106,6 +106,7 @@ namespace Barotrauma.Networking if (requiredFlags.HasFlag(NetFlags.Properties)) { WriteExtraCargo(outMsg); + WritePerks(outMsg); } if (requiredFlags.HasFlag(NetFlags.HiddenSubs)) @@ -129,6 +130,40 @@ namespace Barotrauma.Networking } } + public void ReadPerks(IReadMessage incMsg, Client c) + { + if (!HasPermissionToChangePerks(c)) return; + + bool changed = ReadPerks(incMsg); + if (!changed) { return; } + + UpdateFlag(NetFlags.Properties); + SaveSettings(); + GameMain.NetLobbyScreen.LastUpdateID++; + + static bool HasPermissionToChangePerks(Client client) + { + if (client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return true; } + + bool isPvP = GameMain.NetLobbyScreen?.SelectedMode == GameModePreset.PvP; + bool hasSelectedTeam = client.PreferredTeam is CharacterTeamType.Team1 or CharacterTeamType.Team2; + var otherClients = GameMain.NetworkMember?.ConnectedClients?.Where(c => c != client).ToImmutableArray() ?? ImmutableArray.Empty; + + if (isPvP) + { + if (!hasSelectedTeam) { return false; } + + return !otherClients + .Where(c => c.PreferredTeam == client.PreferredTeam) + .Any(static c => c.HasPermission(Networking.ClientPermissions.ManageSettings)); + } + else + { + return !otherClients.Any(static c => c.HasPermission(Networking.ClientPermissions.ManageSettings)); + } + } + } + public void ServerRead(IReadMessage incMsg, Client c) { if (!c.HasPermission(Networking.ClientPermissions.ManageSettings)) return; @@ -136,7 +171,7 @@ namespace Barotrauma.Networking NetFlags flags = (NetFlags)incMsg.ReadByte(); bool changed = false; - + if (flags.HasFlag(NetFlags.Properties)) { bool propertiesChanged = ReadExtraCargo(incMsg); @@ -176,6 +211,7 @@ namespace Barotrauma.Networking if (propertiesChanged) { UpdateFlag(NetFlags.Properties); + GameMain.Server.RefreshPvpTeamAssignments(); // the changed settings might be relevant to team logic, so refresh } changed |= propertiesChanged; } @@ -189,9 +225,18 @@ namespace Barotrauma.Networking if (flags.HasFlag(NetFlags.Misc)) { - int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; - int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; - GameMain.NetLobbyScreen.MissionType = (MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits); + List missionTypes = new List(GameMain.NetLobbyScreen.MissionTypes); + Identifier addedMissionType = incMsg.ReadIdentifier(); + Identifier removedMissionType = incMsg.ReadIdentifier(); + if (!addedMissionType.IsEmpty) + { + missionTypes.Add(addedMissionType); + } + if (!removedMissionType.IsEmpty) + { + missionTypes.Remove(removedMissionType); + } + GameMain.NetLobbyScreen.MissionTypes = missionTypes; //the byte indicates the direction we're changing the value, subtract one to get negative values from a byte TraitorDangerLevel = TraitorDangerLevel + incMsg.ReadByte() - 1; @@ -361,28 +406,20 @@ namespace Barotrauma.Networking if (min > -1 && max > -1) { AllowedClientNameChars.Add(new Range(min, max)); } } - AllowedRandomMissionTypes = new List(); - string[] allowedMissionTypeNames = doc.Root.GetAttributeStringArray( - "AllowedRandomMissionTypes", Enum.GetValues(typeof(MissionType)).Cast().Select(m => m.ToString()).ToArray()); - foreach (string missionTypeName in allowedMissionTypeNames) - { - if (Enum.TryParse(missionTypeName, out MissionType missionType)) - { - if (missionType == Barotrauma.MissionType.None) { continue; } - if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) { continue; } - AllowedRandomMissionTypes.Add(missionType); - } - } + AllowedRandomMissionTypes = doc.Root.GetAttributeIdentifierArray( + "AllowedRandomMissionTypes", MissionPrefab.GetAllMultiplayerSelectableMissionTypes().ToArray()).ToList(); ServerName = doc.Root.GetAttributeString("name", ""); if (ServerName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); } ServerMessageText = doc.Root.GetAttributeString("ServerMessage", ""); GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModeIdentifier; - //handle Random as the mission type, which is no longer a valid setting - //MissionType.All offers equivalent functionality - if (MissionType == "Random") { MissionType = "All"; } - GameMain.NetLobbyScreen.MissionTypeName = MissionType; + if (AllowedRandomMissionTypes.Contains(Tags.MissionTypeAll)) + { + AllowedRandomMissionTypes = MissionPrefab.GetAllMultiplayerSelectableMissionTypes().ToList(); + } + AllowedRandomMissionTypes = AllowedRandomMissionTypes.Distinct().ToList(); + ValidateMissionTypes(); GameMain.NetLobbyScreen.SetBotSpawnMode(BotSpawnMode); GameMain.NetLobbyScreen.SetBotCount(BotCount); @@ -404,6 +441,20 @@ namespace Barotrauma.Networking CampaignSettings = new CampaignSettings(element); } } + + HashSet selectedCoalitionPerks = SelectedCoalitionPerks.ToHashSet(); + HashSet selectedSeparatistsPerks = SelectedSeparatistsPerks.ToHashSet(); + foreach (DisembarkPerkPrefab prefab in DisembarkPerkPrefab.Prefabs) + { + if (prefab.Cost == 0) + { + selectedSeparatistsPerks.Add(prefab.Identifier); + selectedCoalitionPerks.Add(prefab.Identifier); + } + } + + SelectedCoalitionPerks = selectedCoalitionPerks.ToArray(); + SelectedSeparatistsPerks = selectedSeparatistsPerks.ToArray(); } public string SelectNonHiddenSubmarine(string current = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 81c472caf..9979d522a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -374,13 +374,32 @@ namespace Barotrauma msg.WriteBoolean(GameMain.Server.ServerSettings.AllowSubVoting); if (GameMain.Server.ServerSettings.AllowSubVoting) { - IReadOnlyDictionary voteList = GetVoteCounts(VoteType.Sub, GameMain.Server.ConnectedClients); + bool isMultiSub = GameMain.NetLobbyScreen.SelectedMode == GameModePreset.PvP; + msg.WriteBoolean(isMultiSub); + + var subVoters = isMultiSub ? + GameMain.Server.ConnectedClients.Where(static c => c.PreferredTeam is CharacterTeamType.Team1) : + GameMain.Server.ConnectedClients; + + IReadOnlyDictionary voteList = GetVoteCounts(VoteType.Sub, subVoters); msg.WriteByte((byte)voteList.Count); foreach (KeyValuePair vote in voteList) { msg.WriteByte((byte)vote.Value); msg.WriteString(vote.Key.Name); } + + if (isMultiSub) + { + var separatistsVotes = GetVoteCounts(VoteType.Sub, GameMain.Server.ConnectedClients.Where(static c => c.PreferredTeam is CharacterTeamType.Team2)); + msg.WriteByte((byte)separatistsVotes.Count); + + foreach (var (info, amount) in separatistsVotes) + { + msg.WriteByte((byte)amount); + msg.WriteString(info.Name); + } + } } msg.WriteBoolean(GameMain.Server.ServerSettings.AllowModeVoting); if (GameMain.Server.ServerSettings.AllowModeVoting) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index 554930461..4c08108ad 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -1,7 +1,9 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Barotrauma @@ -9,6 +11,7 @@ namespace Barotrauma partial class NetLobbyScreen : Screen { private SubmarineInfo selectedSub; + private SubmarineInfo selectedEnemySub; private SubmarineInfo selectedShuttle; public bool RadiationEnabled = true; @@ -26,6 +29,18 @@ namespace Barotrauma } } } + + [MaybeNull, AllowNull] + public SubmarineInfo SelectedEnemySub + { + get => selectedEnemySub; + set + { + selectedEnemySub = value; + lastUpdateID++; + } + } + public SubmarineInfo SelectedShuttle { get { return selectedShuttle; } @@ -81,31 +96,19 @@ namespace Barotrauma get { return GameModes[SelectedModeIndex]; } } - private MissionType missionType; - public MissionType MissionType + public IEnumerable MissionTypes { - get { return missionType; } + get { return GameMain.NetworkMember.ServerSettings.AllowedRandomMissionTypes; } set { lastUpdateID++; - missionType = value; if (GameMain.NetworkMember?.ServerSettings != null) { - GameMain.NetworkMember.ServerSettings.MissionType = missionType.ToString(); + GameMain.NetworkMember.ServerSettings.MissionTypes = string.Join(",", value.Select(t => t.ToIdentifier())); } } } - public string MissionTypeName - { - get { return missionType.ToString(); } - set - { - Enum.TryParse(value, out MissionType type); - MissionType = type; - } - } - public NetLobbyScreen() { LevelSeed = ToolBox.RandomSeed(8); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index c9c691212..ab1e682dd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Barotrauma.Extensions; using Barotrauma.Networking; @@ -273,8 +273,11 @@ namespace Barotrauma for (int i = 0; i < amountToChoose; i++) { var traitor = viableTraitors.GetRandomUnsynced(); - viableTraitors.Remove(traitor); - traitors.Add(traitor); + if (traitor != null) + { + viableTraitors.Remove(traitor); + traitors.Add(traitor); + } } return traitors; } @@ -388,7 +391,7 @@ namespace Barotrauma { if (level?.LevelData is { Type: LevelData.LevelType.LocationConnection }) { - if (Submarine.MainSub.WorldPosition.X > level.Size.X / 2) + if (Submarine.MainSub != null && Submarine.MainSub.WorldPosition.X > level.Size.X / 2) { //try starting ASAP if the submarine is already half-way through the level //(brief delay regardless, because otherwise we might retry every frame if finding a suitable event fails below) diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 826f83962..8e9ebbe98 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.5.9.2 + 1.6.17.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs index 2cf5ccbb5..f2914465c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/AchievementManager.cs @@ -42,7 +42,7 @@ namespace Barotrauma private static PathFinder pathFinder; private static readonly Dictionary cachedDistances = new Dictionary(); - public static void OnStartRound() + public static void OnStartRound(Biome biome = null) { roundData = new RoundData(); foreach (Item item in Item.ItemList) @@ -53,12 +53,32 @@ namespace Barotrauma } pathFinder = new PathFinder(WayPoint.WayPointList, false); cachedDistances.Clear(); + +#if CLIENT + // If this is a multiplayer game, the client should let the server handle achievements + if (GameMain.Client != null) { return; } +#endif + + if (biome != null && GameMain.GameSession?.GameMode is CampaignMode) + { + string shortBiomeIdentifier = biome.Identifier.Value.Replace(" ", ""); + UnlockAchievement($"discover{shortBiomeIdentifier}".ToIdentifier(), unlockClients: true); + + // Just got out of Cold Caverns + if (shortBiomeIdentifier == "europanridge".ToIdentifier() && + GameMain.NetworkMember?.ServerSettings?.RespawnMode == RespawnMode.Permadeath) + { + UnlockAchievement("getoutalive".ToIdentifier(), unlockClients: true, + clientConditions: static client => GameMain.GameSession.PermadeathCountForAccount(client.AccountId) <= 0); + } + } } public static void Update(float deltaTime) { if (GameMain.GameSession == null) { return; } #if CLIENT + // If this is a multiplayer game, the client should let the server handle achievements if (GameMain.Client != null) { return; } #endif @@ -73,7 +93,7 @@ namespace Barotrauma UnlockAchievement( identifier: "maxintensity".ToIdentifier(), unlockClients: true, - conditions: static c => c is { IsDead: false, IsUnconscious: false }); + characterConditions: static c => c is { IsDead: false, IsUnconscious: false }); } foreach (Character c in Character.CharacterList) @@ -221,11 +241,6 @@ namespace Barotrauma return false; } - public static void OnBiomeDiscovered(Biome biome) - { - UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier()); - } - public static void OnCampaignMetadataSet(Identifier identifier, object value, bool unlockClients = false) { if (identifier.IsEmpty || value is null) { return; } @@ -235,6 +250,7 @@ namespace Barotrauma public static void OnItemRepaired(Item item, Character fixer) { #if CLIENT + // If this is a multiplayer game, the client should let the server handle achievements if (GameMain.Client != null) { return; } #endif if (fixer == null) { return; } @@ -242,11 +258,27 @@ namespace Barotrauma UnlockAchievement(fixer, "repairdevice".ToIdentifier()); UnlockAchievement(fixer, $"repair{item.Prefab.Identifier}".ToIdentifier()); } + + public static void OnButtonTerminalSignal(Item item, Character user) + { + if (item == null || user == null) { return; } + +#if CLIENT + // If this is a multiplayer game, the client should let the server handle achievements + if (GameMain.Client != null) { return; } +#endif + if ((item.Prefab.Identifier == "alienterminal" || item.Prefab.Identifier == "alienterminal_new") && + item.Condition <= 0) + { + UnlockAchievement(user, "ancientnovelty".ToIdentifier()); + } + } public static void OnAfflictionReceived(Affliction affliction, Character character) { if (affliction.Prefab.AchievementOnReceived.IsEmpty) { return; } #if CLIENT + // If this is a multiplayer game, the client should let the server handle achievements if (GameMain.Client != null) { return; } #endif UnlockAchievement(character, affliction.Prefab.AchievementOnReceived); @@ -257,6 +289,7 @@ namespace Barotrauma if (affliction.Prefab.AchievementOnRemoved.IsEmpty) { return; } #if CLIENT + // If this is a multiplayer game, the client should let the server handle achievements if (GameMain.Client != null) { return; } #endif UnlockAchievement(character, affliction.Prefab.AchievementOnRemoved); @@ -265,6 +298,7 @@ namespace Barotrauma public static void OnCharacterRevived(Character character, Character reviver) { #if CLIENT + // If this is a multiplayer game, the client should let the server handle achievements if (GameMain.Client != null) { return; } #endif if (reviver == null) { return; } @@ -274,6 +308,7 @@ namespace Barotrauma public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath) { #if CLIENT + // If this is a multiplayer game, the client should let the server handle achievements if (GameMain.Client != null || GameMain.GameSession == null) { return; } if (character != Character.Controlled && @@ -310,6 +345,17 @@ namespace Barotrauma UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}indoors".ToIdentifier()); } } +#if SERVER + if (character.SpeciesName == "Jove" && + GameMain.GameSession.Campaign is MultiPlayerCampaign && + GameMain.Server?.ServerSettings is { IronmanModeActive: true }) + { + UnlockAchievement( + identifier: "europasfinest".ToIdentifier(), + unlockClients: true, + characterConditions: static c => c is { IsDead: false }); + } +#endif if (character.HasEquippedItem("clownmask".ToIdentifier()) && character.HasEquippedItem("clowncostume".ToIdentifier()) && @@ -317,6 +363,12 @@ namespace Barotrauma { UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier()); } + + if (character.CharacterHealth?.GetAffliction("psychoclown") != null && + character.CurrentHull?.Submarine.Info is { Type: SubmarineType.BeaconStation }) + { + UnlockAchievement(causeOfDeath.Killer, "whatsmirksbelow".ToIdentifier()); + } // TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest. if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null) @@ -344,8 +396,13 @@ namespace Barotrauma } } } - + #if SERVER + if (GameMain.Server?.ServerSettings?.RespawnMode == RespawnMode.Permadeath) + { + UnlockAchievement(character, "abyssbeckons".ToIdentifier()); + } + if (GameMain.Server?.TraitorManager != null) { if (GameMain.Server.TraitorManager.IsTraitor(character)) @@ -359,6 +416,7 @@ namespace Barotrauma public static void OnTraitorWin(Character character) { #if CLIENT + // If this is a multiplayer game, the client should let the server handle achievements if (GameMain.Client != null || GameMain.GameSession == null) { return; } #endif UnlockAchievement(character, "traitorwin".ToIdentifier()); @@ -400,16 +458,21 @@ namespace Barotrauma foreach (Mission mission in gameSession.Missions) { - if (mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue) + // For PvP missions, all characters on the winning team that are still alive get achievements (if available) + if (mission is CombatMission && GameMain.GameSession.WinningTeam.HasValue) { - //all characters that are alive and in the winning team get an achievement + // Attempt unlocking team-specific achievement (if one has been set in the achievement backend) var achvIdentifier = $"{mission.Prefab.AchievementIdentifier}{(int) GameMain.GameSession.WinningTeam}" .ToIdentifier(); UnlockAchievement(achvIdentifier, true, - c => c != null && !c.IsDead && !c.IsUnconscious && combatMission.IsInWinningTeam(c)); + c => c != null && !c.IsDead && !c.IsUnconscious && CombatMission.IsInWinningTeam(c)); + + // Attempt unlocking mission-specific achievement (if one has been set in the achievement backend) + UnlockAchievement(mission.Prefab.AchievementIdentifier, true, + c => c != null && !c.IsDead && !c.IsUnconscious && CombatMission.IsInWinningTeam(c)); } - else if (mission.Completed) + else if (mission is not CombatMission && mission.Completed) { //all characters get an achievement if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) @@ -424,7 +487,7 @@ namespace Barotrauma } //made it to the destination - if (gameSession.Submarine.AtEndExit) + if (gameSession.Submarine != null && gameSession.Submarine.AtEndExit) { bool noDamageRun = !roundData.SubWasDamaged && !gameSession.Casualties.Any(); @@ -454,7 +517,7 @@ namespace Barotrauma if (charactersInSub.Count == 1) { - //there must be some casualties to get the last mant standing achievement + //there must be some casualties to get the last man standing achievement if (gameSession.Casualties.Any()) { UnlockAchievement(charactersInSub[0], "lastmanstanding".ToIdentifier()); @@ -517,7 +580,7 @@ namespace Barotrauma #endif } - public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func conditions = null) + public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func characterConditions = null, Func clientConditions = null) { if (CheatsEnabled) { return; } if (Screen.Selected is { IsEditor: true }) { return; } @@ -527,16 +590,17 @@ namespace Barotrauma #if SERVER if (unlockClients && GameMain.Server != null) { - foreach (Client c in GameMain.Server.ConnectedClients) + foreach (Client client in GameMain.Server.ConnectedClients) { - if (conditions != null && !conditions(c.Character)) { continue; } - GameMain.Server.GiveAchievement(c, identifier); + if (clientConditions != null && !clientConditions(client)) { continue; } + if (characterConditions != null && !characterConditions(client.Character)) { continue; } + GameMain.Server.GiveAchievement(client, identifier); } } #endif #if CLIENT - if (conditions != null && !conditions(Character.Controlled)) { return; } + if (characterConditions != null && !characterConditions(Character.Controlled)) { return; } #endif UnlockAchievementsOnPlatforms(identifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/CachedDistance.cs b/Barotrauma/BarotraumaShared/SharedSource/CachedDistance.cs index 068a4d320..e8e603387 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CachedDistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CachedDistance.cs @@ -25,4 +25,6 @@ namespace Barotrauma Vector2.DistanceSquared(EndWorldPos, currentEndWorldPos) > minDistSquared; } } + + public readonly record struct CachedLocation(Vector2 Location, double RecalculationTime); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index 852d07842..6501c1333 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -191,7 +191,7 @@ namespace Barotrauma { if (InDetectable) { return true; } if (Entity == null) { return true; } - if (Level.Loaded != null && WorldPosition.Y > Level.Loaded.Size.Y) + if (Level.IsPositionAboveLevel(WorldPosition)) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 37ef8f1b9..fc4b5fbad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -11,7 +11,7 @@ using System.Linq; namespace Barotrauma { - public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow, FleeTo, Patrol } + public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow, FleeTo, Patrol, PlayDead, HideTo, Hiding } public enum AttackPattern { Straight, Sweep, Circle } @@ -23,10 +23,21 @@ namespace Barotrauma Heading = 0x2, Steering = 0x4 } + + [Flags] + public enum EnemyTargetingRestrictions + { + None = 0x0, + PlayerCharacters = 0x1, + PlayerSubmarines = 0x2 + } partial class EnemyAIController : AIController { public static bool DisableEnemyAI; + + public static EnemyTargetingRestrictions TargetingRestrictions = EnemyTargetingRestrictions.None; + private EnemyTargetingRestrictions previousTargetingRestrictions; private AIState _state; public AIState State @@ -35,15 +46,14 @@ namespace Barotrauma set { if (_state == value) { return; } + if (_state == AIState.PlayDead && value == AIState.Idle) + { + // Don't allow to switch to Idle from PlayDead. + return; + } PreviousState = _state; OnStateChanged(_state, value); _state = value; - if (_state == AIState.Attack) - { -#if CLIENT - Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); -#endif - } } } @@ -70,8 +80,25 @@ namespace Barotrauma private bool IsAttackRunning => AttackLimb != null && AttackLimb.attack.IsRunning; private bool IsCoolDownRunning => AttackLimb != null && AttackLimb.attack.CoolDownTimer > 0 || _previousAttackLimb != null && _previousAttackLimb.attack.CoolDownTimer > 0; public float CombatStrength => AIParams.CombatStrength; - private float Sight => AIParams.Sight; - private float Hearing => AIParams.Hearing; + private float Sight => GetPerceptionRange(AIParams.Sight); + private float Hearing => GetPerceptionRange(AIParams.Hearing); + + private float GetPerceptionRange(float range) + { + // TODO: make adjustable + if (State is AIState.PlayDead or AIState.Hiding) + { + // Intentionally constant + return 0.2f; + } + if (PreviousState is AIState.PlayDead or AIState.Hiding) + { + // Significantly buffed + return range * 1.5f; + } + return range; + } + private float FleeHealthThreshold => AIParams.FleeHealthThreshold; private bool IsAggressiveBoarder => AIParams.AggressiveBoarding; @@ -87,11 +114,22 @@ namespace Barotrauma if (_attackLimb != value) { _previousAttackLimb = _attackLimb; - if (_previousAttackLimb != null && _previousAttackLimb.attack.SnapRopeOnNewAttack) { _previousAttackLimb.AttachedRope?.Snap(); } + if (_previousAttackLimb != null) + { + Character.DeselectCharacter(); + if (_previousAttackLimb.attack.SnapRopeOnNewAttack) + { + _previousAttackLimb.AttachedRope?.Snap(); + } + } } else if (_attackLimb != null && _attackLimb.attack.CoolDownTimer <= 0) { - if (_attackLimb != null && _attackLimb.attack.SnapRopeOnNewAttack) { _attackLimb.AttachedRope?.Snap(); } + Character.DeselectCharacter(); + if (_attackLimb.attack.SnapRopeOnNewAttack) + { + _attackLimb.AttachedRope?.Snap(); + } } _attackLimb = value; attackVector = null; @@ -116,10 +154,10 @@ namespace Barotrauma } } - public AITargetMemory SelectedTargetMemory => selectedTargetMemory; - private AITargetMemory selectedTargetMemory; + public AITargetMemory CurrentTargetMemory => currentTargetMemory; + private AITargetMemory currentTargetMemory; private float targetValue; - private CharacterParams.TargetParams selectedTargetingParams; + private CharacterParams.TargetParams currentTargetingParams; private Dictionary targetMemories; @@ -127,6 +165,7 @@ namespace Barotrauma private bool canAttackWalls; public bool CanAttackDoors => canAttackDoors; private bool canAttackDoors; + private bool canAttackItems; private bool canAttackCharacters; public float PriorityFearIncrement => priorityFearIncreasement; @@ -147,6 +186,12 @@ namespace Barotrauma private float aggressionIntensity; private CirclePhase CirclePhase; private float currentAttackIntensity; + + private float playDeadTimer; + /// + /// How long the character has to idle without a target before it can start playing dead (again). + /// + private const float PlayDeadCoolDown = 60; private CoroutineHandle disableTailCoroutine; @@ -156,14 +201,13 @@ namespace Barotrauma public SwarmBehavior SwarmBehavior { get; private set; } public PetBehavior PetBehavior { get; private set; } - public CharacterParams.TargetParams SelectedTargetingParams { get { return selectedTargetingParams; } } + public CharacterParams.TargetParams CurrentTargetingParams => currentTargetingParams; public bool AttackHumans { get { - var target = GetTargetParams(CharacterPrefab.HumanSpeciesName); - return target != null && target.Priority > 0.0f && (target.State == AIState.Attack || target.State == AIState.Aggressive); + return GetTargetParams(Tags.Human).Any(static tp => tp is { Priority: > 0.0f, State: AIState.Attack or AIState.Aggressive }); } } @@ -171,8 +215,7 @@ namespace Barotrauma { get { - var target = GetTargetParams("room"); - return target != null && target.Priority > 0.0f && (target.State == AIState.Attack || target.State == AIState.Aggressive); + return GetTargetParams(Tags.Room).Any(static tp => tp is { Priority: > 0.0f, State: AIState.Attack or AIState.Aggressive }); } } @@ -208,11 +251,11 @@ namespace Barotrauma } = new HashSet(); public static bool IsTargetBeingChasedBy(Character target, Character character) - => character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && (enemyAI.State == AIState.Attack || enemyAI.State == AIState.Aggressive); + => character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && enemyAI.State is AIState.Attack or AIState.Aggressive; public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c); private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character); - private static bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam; + private static bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character { IsOnPlayerTeam: true }; private bool IsAttackingOwner(Character other) => PetBehavior != null && PetBehavior.Owner != null && @@ -230,7 +273,7 @@ namespace Barotrauma reverse = value; if (FishAnimController != null) { - FishAnimController.reverse = reverse; + FishAnimController.Reverse = reverse; } } } @@ -250,7 +293,10 @@ namespace Barotrauma targetMemories = new Dictionary(); steeringManager = outsideSteering; //allow targeting outposts and outpost NPCs in outpost levels - TargetOutposts = Level.Loaded != null && Level.Loaded.Type == LevelData.LevelType.Outpost; + TargetOutposts = + (Level.Loaded != null && Level.Loaded.Type == LevelData.LevelType.Outpost) || + //the main sub can be an outpost in the editor + Submarine.MainSub is { Info.Type: SubmarineType.Outpost }; List aiElements = new List(); List aiCommonness = new List(); @@ -319,6 +365,10 @@ namespace Barotrauma requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); myBodies.Add(Character.AnimController.Collider.FarseerBody); + if (AIParams.PlayDeadProbability > 0) + { + Character.EvaluatePlayDeadProbability(); + } CreatureMetrics.UnlockInEditor(Character.SpeciesName); } @@ -345,60 +395,72 @@ namespace Barotrauma return _aiParams; } } - private CharacterParams.TargetParams GetTargetParams(string targetTag) => GetTargetParams(targetTag.ToIdentifier()); - private CharacterParams.TargetParams GetTargetParams(Identifier targetTag) => AIParams.GetTarget(targetTag, false); - private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget)); - private Identifier GetTargetingTag(AITarget aiTarget) + private IEnumerable GetTargetParams(Identifier targetTag) => AIParams.GetTargets(targetTag); + + private IEnumerable GetTargetParams(IEnumerable targetingTags) { - if (aiTarget?.Entity == null) { return Identifier.Empty; } - Identifier targetingTag = Identifier.Empty; + foreach (Identifier tag in targetingTags) + { + foreach (var tp in GetTargetParams(tag)) + { + yield return tp; + } + } + } + + private readonly List _targetingTags = new List(); + + private IEnumerable GetTargetingTags(AITarget aiTarget) + { + _targetingTags.Clear(); + if (aiTarget?.Entity == null) { return _targetingTags; } if (aiTarget.Entity is Character targetCharacter) { if (targetCharacter.IsDead) { - targetingTag = "dead".ToIdentifier(); + _targetingTags.Add(Tags.Dead); } - else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >= Character.GetDamageDoneByAttacker(targetCharacter)) + else if (AIParams.TryGetHighestPriorityTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >= Character.GetDamageDoneByAttacker(targetCharacter)) { - targetingTag = tp.Tag; + _targetingTags.Add(tp.Tag); } else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner) { - targetingTag = "owner".ToIdentifier(); + _targetingTags.Add(Tags.Owner); } else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter))) { - targetingTag = "hostile".ToIdentifier(); + _targetingTags.Add(Tags.Hostile); } - else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP)) + else if (AIParams.TryGetHighestPriorityTarget(targetCharacter, out CharacterParams.TargetParams tP)) { - targetingTag = tP.Tag; + _targetingTags.Add(tP.Tag); } else if (targetCharacter.AIController is EnemyAIController enemy) { - if (enemy.PetBehavior != null && (PetBehavior != null || AIParams.HasTag("pet"))) + if (enemy.PetBehavior != null && (PetBehavior != null || AIParams.HasTag(Tags.Pet))) { // Pets see other pets as pets by default. // Monsters see them only as pet only when they have a matching ai target. Otherwise they use the other tags, specified below. - targetingTag = "pet".ToIdentifier(); + _targetingTags.Add(Tags.Pet); } - else if (targetCharacter.IsHusk && AIParams.HasTag("husk")) + else if (targetCharacter.IsHusk && AIParams.HasTag(Tags.Husk)) { - targetingTag = "husk".ToIdentifier(); + _targetingTags.Add(Tags.Husk); } else if (!Character.IsSameSpeciesOrGroup(targetCharacter)) { if (enemy.CombatStrength > CombatStrength) { - targetingTag = "stronger".ToIdentifier(); + _targetingTags.Add(Tags.Stronger); } else if (enemy.CombatStrength < CombatStrength) { - targetingTag = "weaker".ToIdentifier(); + _targetingTags.Add(Tags.Weaker); } else { - targetingTag = "equal".ToIdentifier(); + _targetingTags.Add(Tags.Equal); } } } @@ -409,31 +471,30 @@ namespace Barotrauma { if (targetItem.HasTag(prio.Tag)) { - targetingTag = prio.Tag; - break; + _targetingTags.Add(prio.Tag); } } - if (targetingTag.IsEmpty) + if (_targetingTags.None()) { if (targetItem.GetComponent() != null) { - targetingTag = "sonar".ToIdentifier(); + _targetingTags.Add(Tags.Sonar); } if (targetItem.GetComponent() != null) { - targetingTag = "door".ToIdentifier(); + _targetingTags.Add(Tags.Door); } } } else if (aiTarget.Entity is Structure) { - targetingTag = "wall".ToIdentifier(); + _targetingTags.Add(Tags.Wall); } else if (aiTarget.Entity is Hull) { - targetingTag = "room".ToIdentifier(); + _targetingTags.Add(Tags.Room); } - return targetingTag; + return _targetingTags; } public override void SelectTarget(AITarget target) => SelectTarget(target, 100); @@ -441,8 +502,8 @@ namespace Barotrauma public void SelectTarget(AITarget target, float priority) { SelectedAiTarget = target; - selectedTargetMemory = GetTargetMemory(target, addIfNotFound: true); - selectedTargetMemory.Priority = priority; + currentTargetMemory = GetTargetMemory(target, addIfNotFound: true); + currentTargetMemory.Priority = priority; ignoredTargets.Remove(target); } @@ -456,6 +517,15 @@ namespace Barotrauma Character.HeldItems.ForEach(i => i.GetComponent()?.GetRope()?.Snap()); } } + + public void EvaluatePlayDeadProbability(float? probability = null) + { + if (probability.HasValue) + { + AIParams.PlayDeadProbability = probability.Value; + } + Character.AllowPlayDead = Rand.Value() <= AIParams.PlayDeadProbability; + } public override void Update(float deltaTime) { @@ -466,6 +536,9 @@ namespace Barotrauma IsTryingToSteerThroughGap = false; Reverse = false; + //doesn't do anything usually, but events may sometimes change monsters' (or pets' that use enemy AI) teams + Character.UpdateTeam(); + bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); if (steeringManager == insideSteering) { @@ -560,28 +633,21 @@ namespace Barotrauma } else { + if (TargetingRestrictions != previousTargetingRestrictions) + { + previousTargetingRestrictions = TargetingRestrictions; + // update targeting instantly when there's a change in targeting restrictions + updateTargetsTimer = 0; + SelectedAiTarget = null; + } + if (updateTargetsTimer > 0) { updateTargetsTimer -= deltaTime; } else if (avoidTimer <= 0 || activeTriggers.Any() && returnTimer <= 0) { - UpdateTargets(out CharacterParams.TargetParams targetingParams); - updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f); - if (SelectedAiTarget == null) - { - State = AIState.Idle; - } - else if (targetingParams != null) - { - selectedTargetingParams = targetingParams; - State = targetingParams.State; - } - if ((LatchOntoAI == null || !LatchOntoAI.IsAttached || wallTarget != null) && - (State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)) - { - UpdateWallTarget(requiredHoleCount); - } + UpdateTargets(); } } @@ -641,6 +707,9 @@ namespace Barotrauma case AIState.Idle: UpdateIdle(deltaTime); break; + case AIState.PlayDead: + Character.IsRagdolled = true; + break; case AIState.Patrol: UpdatePatrol(deltaTime); break; @@ -659,7 +728,7 @@ namespace Barotrauma case AIState.Avoid: case AIState.PassiveAggressive: case AIState.Aggressive: - if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) + if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity.Removed) { State = AIState.Idle; return; @@ -681,11 +750,11 @@ namespace Barotrauma else { bool isBeingChased = IsBeingChased; - float reactDistance = !isBeingChased && selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); + float reactDistance = !isBeingChased && currentTargetingParams is { ReactDistance: > 0 } ? currentTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); if (squaredDistance <= Math.Pow(reactDistance, 2)) { float halfReactDistance = reactDistance / 2; - float attackDistance = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDistance; + float attackDistance = currentTargetingParams is { AttackDistance: > 0 } ? currentTargetingParams.AttackDistance : halfReactDistance; if (State == AIState.Aggressive || State == AIState.PassiveAggressive && squaredDistance < Math.Pow(attackDistance, 2)) { run = true; @@ -707,7 +776,9 @@ namespace Barotrauma case AIState.Protect: case AIState.Follow: case AIState.FleeTo: - if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) + case AIState.HideTo: + case AIState.Hiding: + if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity.Removed) { State = AIState.Idle; return; @@ -728,14 +799,14 @@ namespace Barotrauma if (c.Submarine?.TeamID != Character.Submarine?.TeamID) { return false; } if (c.IsPlayer || Character.IsOnFriendlyTeam(c)) { - return a.Damage >= selectedTargetingParams.Threshold; + return a.Damage >= currentTargetingParams.Threshold; } return true; } Character attacker = targetCharacter.LastAttackers.LastOrDefault(ShouldRetaliate)?.Character; if (attacker?.AiTarget != null) { - ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); + ChangeTargetState(attacker, AIState.Attack, currentTargetingParams.Priority * 2); SelectTarget(attacker.AiTarget); State = AIState.Attack; UpdateWallTarget(requiredHoleCount); @@ -743,31 +814,48 @@ namespace Barotrauma } } } - float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); + float distX = Math.Abs(WorldPosition.X - SelectedAiTarget.WorldPosition.X); + float distY = Math.Abs(WorldPosition.Y - SelectedAiTarget.WorldPosition.Y); + if (Character.Submarine != null && distY > 50 && SelectedAiTarget.Entity is Character targetC && !VisibleHulls.Contains(targetC.CurrentHull)) + { + // Target not visible, and possibly on a different floor. + distY *= 3; + } + float dist = distX + distY; float reactDist = GetPerceivingRange(SelectedAiTarget); Vector2 offset = Vector2.Zero; - if (selectedTargetingParams != null) + if (currentTargetingParams != null) { - if (selectedTargetingParams.ReactDistance > 0) + if (currentTargetingParams.ReactDistance > 0) { - reactDist = selectedTargetingParams.ReactDistance; + reactDist = currentTargetingParams.ReactDistance; } - offset = selectedTargetingParams.Offset; + offset = currentTargetingParams.Offset; } if (offset != Vector2.Zero) { reactDist += offset.Length(); } - if (sqrDist > MathUtils.Pow2(reactDist + movementMargin)) + if (dist > reactDist + movementMargin) { - movementMargin = State == AIState.FleeTo ? 0 : reactDist; + movementMargin = State is AIState.FleeTo or AIState.HideTo or AIState.Hiding ? 0 : reactDist; + if (State == AIState.Hiding) + { + // Too far to hide. + State = AIState.HideTo; + } run = true; UpdateFollow(deltaTime); } else { + if (State == AIState.HideTo) + { + // Close enough to hide. + State = AIState.Hiding; + } movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, reactDist); - if (State == AIState.FleeTo) + if (State is AIState.FleeTo or AIState.Hiding) { SteeringManager.Reset(); Character.AnimController.TargetMovement = Vector2.Zero; @@ -795,7 +883,7 @@ namespace Barotrauma { disableTailCoroutine = CoroutineManager.Invoke(() => { - if (Character != null && !Character.Removed) + if (Character is { Removed: false }) { Character.AnimController.HideAndDisable(LimbType.Tail, ignoreCollisions: false); } @@ -816,16 +904,16 @@ namespace Barotrauma } break; case AIState.Observe: - if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) + if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity.Removed) { State = AIState.Idle; return; } run = false; - sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); - reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); + float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); + reactDist = currentTargetingParams is { ReactDistance: > 0 } ? currentTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); float halfReactDist = reactDist / 2; - float attackDist = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDist; + float attackDist = currentTargetingParams is { AttackDistance: > 0 } ? currentTargetingParams.AttackDistance : halfReactDist; if (sqrDist > Math.Pow(reactDist, 2)) { // Too far to react @@ -902,7 +990,18 @@ namespace Barotrauma private void UpdateIdle(float deltaTime, bool followLastTarget = true) { - if (AIParams.PatrolFlooded || AIParams.PatrolDry) + if (Character.AllowPlayDead && Character.Submarine != null) + { + if (playDeadTimer > 0) + { + playDeadTimer -= deltaTime; + } + else + { + State = AIState.PlayDead; + } + } + else if (AIParams.PatrolFlooded || AIParams.PatrolDry) { State = AIState.Patrol; } @@ -920,10 +1019,10 @@ namespace Barotrauma if (followLastTarget) { var target = SelectedAiTarget ?? _lastAiTarget; - if (target?.Entity != null && !target.Entity.Removed && + if (target?.Entity is { Removed: false } && PreviousState == AIState.Attack && Character.CurrentHull == null && (_previousAttackLimb?.attack == null || - _previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0))) + _previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0))) { // Keep heading to the last known position of the target var memory = GetTargetMemory(target); @@ -950,19 +1049,22 @@ namespace Barotrauma } } } - if (pathSteering != null && !Character.AnimController.InWater) + if (!Character.IsClimbing) { - // Wander around inside - pathSteering.Wander(deltaTime, Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100.0f), stayStillInTightSpace: false); - } - else - { - // Wander around outside or swimming - steeringManager.SteeringWander(avoidWanderingOutsideLevel: true); - if (Character.AnimController.InWater) + if (pathSteering != null && !Character.AnimController.InWater) { - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); + // Wander around inside + pathSteering.Wander(deltaTime, Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100.0f), stayStillInTightSpace: false); } + else + { + // Wander around outside or swimming + steeringManager.SteeringWander(avoidWanderingOutsideLevel: true); + if (Character.AnimController.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); + } + } } } @@ -1121,7 +1223,7 @@ namespace Barotrauma private void UpdateAttack(float deltaTime) { - if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) + if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity.Removed || currentTargetingParams == null) { State = AIState.Idle; return; @@ -1204,7 +1306,7 @@ namespace Barotrauma } Character targetCharacter = SelectedAiTarget.Entity as Character; IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; - bool canAttack = true; + bool canAttack = !Character.IsClimbing; bool pursue = false; if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0)) { @@ -1222,7 +1324,14 @@ namespace Barotrauma switch (activeBehavior) { case AIBehaviorAfterAttack.Eat: - UpdateEating(deltaTime); + if (currentAttackLimb.IsSevered) + { + ReleaseEatingTarget(); + } + else + { + UpdateEating(deltaTime); + } return; case AIBehaviorAfterAttack.Pursue: case AIBehaviorAfterAttack.PursueIfCanAttack: @@ -1396,6 +1505,10 @@ namespace Barotrauma if (canAttack) { + if (AttackLimb is { IsSevered: true }) + { + AttackLimb = null; + } if (AttackLimb == null || !IsValidAttack(AttackLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity)) { AttackLimb = GetAttackLimb(attackWorldPos); @@ -1493,12 +1606,32 @@ namespace Barotrauma // Check that we can reach the target distance = toTargetOffset.Length(); - canAttack = distance < AttackLimb.attack.Range; + if (canAttack) + { + canAttack = distance < AttackLimb.attack.Range; + } + if (canAttack && !Character.InWater && Character.AnimController.CanWalk) + { + // On ground, ensure that the monster is facing the target, so that they don't hit the target while standing with their back towards it. + // In water, we don't want such checks, because it's ok for the monsters to attack targets on their sides and even behind them. + if (!Character.IsFacing(attackWorldPos)) + { + canAttack = false; + } + } if (canAttack) { reachTimer = 0; + if (IsAggressiveBoarder) + { + if (SelectedAiTarget.Entity is Item i && i.GetComponent() is Door { CanBeTraversed: true }) + { + // Don't attack open doors, just steer through them. + canAttack = false; + } + } } - else if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && distance < AttackLimb.attack.Range * 5) + else if (currentTargetingParams.AttackPattern == AttackPattern.Straight && distance < AttackLimb.attack.Range * 5) { Vector2 targetVelocity = Vector2.Zero; Submarine targetSub = SelectedAiTarget.Entity.Submarine; @@ -1535,7 +1668,7 @@ namespace Barotrauma { if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range) { - humanoidAnimController.Crouching = true; + humanoidAnimController.Crouch(); } } @@ -1612,11 +1745,11 @@ namespace Barotrauma if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull) { var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; - if (door != null && !door.CanBeTraversed && !door.HasAccess(Character)) + if (door is { CanBeTraversed: false } && (!Character.IsInFriendlySub || !door.HasAccess(Character))) { if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget) { - SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority); + SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority); State = AIState.Attack; return; } @@ -1690,36 +1823,36 @@ namespace Barotrauma // Sweeping and circling doesn't work well inside if (Character.CurrentHull == null) { - switch (selectedTargetingParams.AttackPattern) + switch (currentTargetingParams.AttackPattern) { case AttackPattern.Sweep: - if (selectedTargetingParams.SweepDistance > 0) + if (currentTargetingParams.SweepDistance > 0) { if (distance <= 0) { distance = (attackWorldPos - WorldPosition).Length(); } - float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance)); + float amplitude = MathHelper.Lerp(0, currentTargetingParams.SweepStrength, MathUtils.InverseLerp(currentTargetingParams.SweepDistance, 0, distance)); if (amplitude > 0) { - sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed; + sweepTimer += deltaTime * currentTargetingParams.SweepSpeed; float sin = (float)Math.Sin(sweepTimer) * amplitude; steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, sin); } else { - sweepTimer = Rand.Range(-1000f, 1000f) * selectedTargetingParams.SweepSpeed; + sweepTimer = Rand.Range(-1000f, 1000f) * currentTargetingParams.SweepSpeed; } } break; case AttackPattern.Circle: if (IsCoolDownRunning) { break; } if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; } - if (selectedTargetingParams == null) { break; } + if (currentTargetingParams == null) { break; } var targetSub = SelectedAiTarget.Entity?.Submarine; ISpatialEntity spatialTarget = targetSub ?? SelectedAiTarget.Entity; float targetSize = 0; - if (!selectedTargetingParams.IgnoreTargetSize) + if (!currentTargetingParams.IgnoreTargetSize) { targetSize = targetSub != null ? Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2 : @@ -1737,28 +1870,28 @@ namespace Barotrauma strikeTimer = 0; blockCheckTimer = 0; breakCircling = false; - float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f; - float maxFallBackDistance = selectedTargetingParams.CircleStartDistance; - float maxRandomOffset = selectedTargetingParams.CircleMaxRandomOffset; + float minFallBackDistance = currentTargetingParams.CircleStartDistance * 0.5f; + float maxFallBackDistance = currentTargetingParams.CircleStartDistance; + float maxRandomOffset = currentTargetingParams.CircleMaxRandomOffset; // The lower the rotation speed, the slower the progression. Also the distance to the target stays longer. // So basically if the value is higher, the creature will strike the sub more quickly and with more precision. float ClampIntensity(float intensity) => MathHelper.Clamp(intensity * Rand.Range(0.9f, 1.1f), AIParams.StartAggression, AIParams.MaxAggression); if (isProgressive) { float intensity = ClampIntensity(currentAttackIntensity); - float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; - float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; + float minRotationSpeed = 0.01f * currentTargetingParams.CircleRotationSpeed; + float maxRotationSpeed = 0.5f * currentTargetingParams.CircleRotationSpeed; circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, intensity); circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, intensity); circleOffset = Rand.Vector(MathHelper.Lerp(maxRandomOffset, 0, intensity)); } else { - circleRotationSpeed = selectedTargetingParams.CircleRotationSpeed; + circleRotationSpeed = currentTargetingParams.CircleRotationSpeed; circleFallbackDistance = maxFallBackDistance; circleOffset = Rand.Vector(maxRandomOffset); } - circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor); + circleRotationSpeed *= Rand.Range(1 - currentTargetingParams.CircleRandomRotationFactor, 1 + currentTargetingParams.CircleRandomRotationFactor); aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); DisableAttacksIfLimbNotRanged(); if (targetSub is { Borders.Width: < 1000 } && AttackLimb?.attack is { Ranged: false }) @@ -1766,7 +1899,7 @@ namespace Barotrauma breakCircling = true; CirclePhase = CirclePhase.CloseIn; } - else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance)) + else if (sqrDistToTarget > MathUtils.Pow2(targetSize + currentTargetingParams.CircleStartDistance)) { CirclePhase = CirclePhase.CloseIn; } @@ -1781,8 +1914,8 @@ namespace Barotrauma break; case CirclePhase.CloseIn: Vector2 targetVelocity = GetTargetVelocity(); - float targetDistance = selectedTargetingParams.IgnoreTargetSize ? selectedTargetingParams.CircleStartDistance * 0.9f: - targetSize + selectedTargetingParams.CircleStartDistance / 2; + float targetDistance = currentTargetingParams.IgnoreTargetSize ? currentTargetingParams.CircleStartDistance * 0.9f: + targetSize + currentTargetingParams.CircleStartDistance / 2; if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetVelocity)) { strikeTimer = AttackLimb.attack.CoolDown; @@ -1811,9 +1944,9 @@ namespace Barotrauma { CirclePhase = CirclePhase.CloseIn; } - else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance * 1.2f)) + else if (sqrDistToTarget > MathUtils.Pow2(targetSize + currentTargetingParams.CircleStartDistance * 1.2f)) { - if (selectedTargetingParams.DynamicCircleRotationSpeed && circleRotationSpeed < 100) + if (currentTargetingParams.DynamicCircleRotationSpeed && circleRotationSpeed < 100) { circleRotationSpeed *= 1 + deltaTime; } @@ -1923,17 +2056,17 @@ namespace Barotrauma float GetStrikeDistanceMultiplier(Vector2 targetVelocity) { - if (selectedTargetingParams.CircleStrikeDistanceMultiplier < 1) { return 0; } + if (currentTargetingParams.CircleStrikeDistanceMultiplier < 1) { return 0; } float requiredDistMultiplier = 2; bool isHeading = Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; if (isHeading) { - requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier; + requiredDistMultiplier = currentTargetingParams.CircleStrikeDistanceMultiplier; float targetVelocityHorizontal = Math.Abs(targetVelocity.X); if (targetVelocityHorizontal > 1) { // Reduce the required distance if the target is moving. - requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(targetVelocityHorizontal / 10, 0, 1)); + requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(currentTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(targetVelocityHorizontal / 10, 0, 1)); if (requiredDistMultiplier < 2) { requiredDistMultiplier = 2; @@ -1968,7 +2101,7 @@ namespace Barotrauma } if (updateSteering) { - if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) + if (currentTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) { bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f; bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f); @@ -2069,7 +2202,16 @@ namespace Barotrauma if (attack.CoolDownTimer > 0) { return false; } if (!attack.IsValidContext(currentContexts)) { return false; } if (!attack.IsValidTarget(target)) { return false; } - if (target is ISerializableEntity se && target is Character) + if (!attackingLimb.attack.Ranged) + { + switch (target) + { + case Item when attackingLimb.attack.ItemDamage <= 0: + case Structure when attackingLimb.attack.StructureDamage <= 0: + return false; + } + } + if (target is ISerializableEntity se and Barotrauma.Character) { if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; } } @@ -2085,6 +2227,21 @@ namespace Barotrauma float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget)); if (angle > attack.RequiredAngle) { return false; } } + if (attack.RootForceWorldEnd.LengthSquared() > 1) + { + // Don't allow root motion attacks, if we are not on the same level with the target, because it can cause warping. + switch (target) + { + case Character targetCharacter when Character.CurrentHull != targetCharacter.CurrentHull || targetCharacter.IsKnockedDown: + case Item targetItem when Character.CurrentHull != targetItem.CurrentHull: + return false; + } + float verticalDistance = Math.Abs(attackWorldPos.Y - Character.WorldPosition.Y); + if (verticalDistance > 50) + { + return false; + } + } return true; } @@ -2150,14 +2307,15 @@ namespace Barotrauma { float reactionTime = Rand.Range(0.1f, 0.3f); updateTargetsTimer = Math.Min(updateTargetsTimer, reactionTime); - bool wasLatched = IsLatchedOnSub; Character.AnimController.ReleaseStuckLimbs(); if (attackResult.Damage > 0) { LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); } - if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } + if (attacker?.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } + AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true, keepAlive: true); + targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt; if (attackResult.Damage >= AIParams.DamageThreshold) { ReleaseDragTargets(); @@ -2181,14 +2339,33 @@ namespace Barotrauma } return; } + if (!isFriendly && attackResult.Damage > 0.0f) { bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls; - if (AIParams.AttackWhenProvoked && canAttack && !ignoredTargets.Contains(attacker.AiTarget)) + if (canAttack) { + switch (State) + { + case AIState.PlayDead when Rand.Value() < 0.5f: + // 50% chance of not reacting when playing dead. + return; + case AIState.PlayDead: + case AIState.Hiding: + SelectTarget(attacker.AiTarget); + State = AIState.Attack; + break; + } + } + if (AIParams.AttackWhenProvoked && canAttack) + { + if (ignoredTargets.Contains(attacker.AiTarget)) + { + ignoredTargets.Remove(attacker.AiTarget); + } if (attacker.IsHusk) { - ChangeTargetState("husk", AIState.Attack, 100); + ChangeTargetState(Tags.Husk, AIState.Attack, 100); } else { @@ -2199,7 +2376,7 @@ namespace Barotrauma { if (attacker.IsHusk) { - ChangeTargetState("husk", canAttack ? AIState.Attack : AIState.Escape, 100); + ChangeTargetState(Tags.Husk, canAttack ? AIState.Attack : AIState.Escape, 100); } else if (attacker.AIController is EnemyAIController enemyAI) { @@ -2227,18 +2404,23 @@ namespace Barotrauma ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100); } } - else if (canAttack && attacker.IsHuman && AIParams.TryGetTarget(attacker, out CharacterParams.TargetParams targetingParams)) + else if (canAttack && attacker.IsHuman && + AIParams.TryGetTargets(attacker, out IEnumerable targetingParams)) { - if (targetingParams.State == AIState.Aggressive || targetingParams.State == AIState.PassiveAggressive) + //use a temporary list, because changing the state may change the targetingParams returned by TryGetTargets, + //which would cause a "collection was modified" exception + tempParamsList.Clear(); + tempParamsList.AddRange(targetingParams); + foreach (var tp in tempParamsList) { - ChangeTargetState(attacker, AIState.Attack, 100); + if (tp.State is AIState.Aggressive or AIState.PassiveAggressive) + { + ChangeTargetState(attacker, AIState.Attack, 100); + } } } } - AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true, keepAlive: true); - targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt; - // Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine; bool avoidGunFire = AIParams.AvoidGunfire && attacker.Submarine != Character.Submarine; @@ -2391,12 +2573,12 @@ namespace Barotrauma // Halve the greed for attacking non-characters. greed /= 2; } - selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; + currentTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; } else { - selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1); - return selectedTargetMemory.Priority > 1; + currentTargetMemory.Priority -= Math.Max(currentTargetMemory.Priority / 2, 1); + return currentTargetMemory.Priority > 1; } } } @@ -2536,26 +2718,24 @@ namespace Barotrauma private void UpdateEating(float deltaTime) { - if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) + if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity.Removed) { - State = AIState.Idle; - if (Character.SelectedCharacter != null) - { - Character.DeselectCharacter(); - } + ReleaseEatingTarget(); return; } - if (SelectedAiTarget.Entity is Character || SelectedAiTarget.Entity is Item) + if (SelectedAiTarget.Entity is Barotrauma.Character or Item) { Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head); if (mouthLimb == null) { - DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)", + DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb found)", contentPackage: Character.Prefab.ContentPackage); - State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); + ReleaseEatingTarget(); + ResetAITarget(); return; } - Vector2 mouthPos = Character.AnimController.SimplePhysicsEnabled ? SimPosition : Character.AnimController.GetMouthPosition().Value; + Vector2 mouthPos = Character.AnimController.SimplePhysicsEnabled ? SimPosition : Character.AnimController.GetMouthPosition() ?? Vector2.Zero; Vector2 attackSimPosition = Character.GetRelativeSimPosition(SelectedAiTarget.Entity); Vector2 limbDiff = attackSimPosition - mouthPos; float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2); @@ -2609,10 +2789,16 @@ namespace Barotrauma else { IgnoreTarget(SelectedAiTarget); - State = AIState.Idle; + ReleaseEatingTarget(); ResetAITarget(); } } + + private void ReleaseEatingTarget() + { + State = AIState.Idle; + Character.DeselectCharacter(); + } #endregion @@ -2683,30 +2869,48 @@ namespace Barotrauma //goes through all the AItargets, evaluates how preferable it is to attack the target, //whether the Character can see/hear the target and chooses the most preferable target within //sight/hearing range - public AITarget UpdateTargets(out CharacterParams.TargetParams targetingParams) + public void UpdateTargets() { - AITarget newTarget = null; targetValue = 0; - selectedTargetMemory = null; - targetingParams = null; + AITarget newTarget = null; + CharacterParams.TargetParams selectedTargetParams = null; + AITargetMemory targetMemory = null; bool isAnyTargetClose = false; bool isBeingChased = IsBeingChased; - float maxModifier = 5; + const float priorityValueMaxModifier = 5; + bool isCharacterInside = Character.CurrentHull != null; + bool tryToGetInside = + Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True || + //characters that are aggressive boarders can partially enter the sub can attempt to push through holes + (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial && IsAggressiveBoarder); + foreach (AITarget aiTarget in AITarget.List) { if (aiTarget.ShouldBeIgnored()) { continue; } if (ignoredTargets.Contains(aiTarget)) { continue; } if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; } - if (!TargetOutposts) + if (!TargetOutposts && GameMain.GameSession.GameMode is not TestGameMode) { if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsOutpost) { continue; } } Character targetCharacter = aiTarget.Entity as Character; //ignore the aitarget if it is the Character itself if (targetCharacter == Character) { continue; } + if (TargetingRestrictions.HasFlag(EnemyTargetingRestrictions.PlayerCharacters)) + { + if (targetCharacter is { IsPlayer: true }) { continue; } - float valueModifier = 1; - Identifier targetingTag = GetTargetingTag(aiTarget); + // monsters can ignore a player character, but attack a diving suit equipped by the character + if (aiTarget.Entity is Item item && item.GetRootInventoryOwner() is Character { IsPlayer: true}) { continue; } + } + if (TargetingRestrictions.HasFlag(EnemyTargetingRestrictions.PlayerSubmarines)) + { + if (aiTarget.Entity.Submarine?.Info is { IsPlayer: true }) { continue; } + } + var targetingTags = GetTargetingTags(aiTarget); + + #region Filter out targets by entity type, based on contextual information. + Door door = null; if (targetCharacter != null) { // ignore if target is tagged to be explicitly ignored (Feign Death) @@ -2715,26 +2919,6 @@ namespace Barotrauma { continue; } - if (targetCharacter.AIController is EnemyAIController enemy) - { - if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee)) - { - if (SelectedAiTarget == aiTarget) - { - // Freightened -> hold on to the target - valueModifier *= 2; - } - if (IsBeingChasedBy(targetCharacter)) - { - valueModifier *= 2; - } - if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull)) - { - // Inside but in a different room - valueModifier /= 2; - } - } - } } else { @@ -2762,12 +2946,10 @@ namespace Barotrauma if (hull.Submarine == null) { continue; } if (hull.Submarine.Info.IsRuin) { continue; } } - - Door door = null; - if (aiTarget.Entity is Item item) + else if (aiTarget.Entity is Item item) { door = item.GetComponent(); - bool targetingFromOutsideToInside = item.CurrentHull != null && Character.CurrentHull == null; + bool targetingFromOutsideToInside = item.CurrentHull != null && !isCharacterInside; if (targetingFromOutsideToInside) { if (door != null && (!canAttackDoors && !AIParams.CanOpenDoors) || !canAttackWalls) @@ -2784,7 +2966,7 @@ namespace Barotrauma continue; } } - else if (targetingTag == "nasonov") + else if (targetingTags.Contains(Tags.Nasonov)) { if ((item.Submarine == null || !item.Submarine.Info.IsPlayer) && item.ParentInventory == null) { @@ -2793,7 +2975,7 @@ namespace Barotrauma } } // Ignore the target if it's a decoy and the character is already inside a sub - if (Character.CurrentHull != null && targetingTag == "decoy") + if (Character.CurrentHull != null && targetingTags.Contains(Tags.Decoy)) { continue; } @@ -2808,23 +2990,159 @@ namespace Barotrauma if (s.IsPlatform) { continue; } if (s.Submarine == null) { continue; } if (s.Submarine.Info.IsRuin) { continue; } - bool isCharacterInside = Character.CurrentHull != null; bool isInnerWall = s.Prefab.Tags.Contains("inner"); if (isInnerWall && !isCharacterInside) { // Ignore inner walls when outside (walltargets still work) continue; } - bool attemptToGetInside = - Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True || - //characters that are aggressive boarders can partially enter the sub can attempt to push through holes - (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial && IsAggressiveBoarder); - - if (!attemptToGetInside && IsWallDisabled(s)) + if (!tryToGetInside && IsWallDisabled(s)) { continue; } + } + if (door != null) + { + if (door.Item.Submarine == null) { continue; } + bool isOutdoor = door.LinkedGap is { FlowTargetHull: not null, IsRoomToRoom: false }; + // Ignore inner doors when outside + if (Character.CurrentHull == null && !isOutdoor) { continue; } + bool isOpen = door.CanBeTraversed; + if (!isOpen) + { + if (!canAttackDoors) { continue; } + } + else if (Character.AnimController.CanEnterSubmarine != CanEnterSubmarine.True) + { + // Ignore broken and open doors, if cannot enter submarine + // Also ignore them if the monster can only partially enter the sub: + // these monsters tend to be too large to get through doors anyway. + continue; + } + } + else if (aiTarget.Entity is IDamageable { Health: <= 0.0f }) + { + continue; + } + } + #endregion + #region Choose valid targeting params. + if (targetingTags.None()) { continue; } + CharacterParams.TargetParams matchingTargetParams = null; + foreach (var targetParams in GetTargetParams(targetingTags)) + { + if (matchingTargetParams != null) + { + if (matchingTargetParams.Priority > targetParams.Priority) + { + // Valid higher priority params already found. + continue; + } + } + if (targetParams.IgnoreInside && Character.CurrentHull != null) { continue; } + if (targetParams.IgnoreOutside && Character.CurrentHull == null) { continue; } + if (targetParams.IgnoreIncapacitated && targetCharacter is { IsIncapacitated: true }) { continue; } + if (targetParams.IgnoreTargetInside && aiTarget.Entity.Submarine != null) { continue; } + if (targetParams.IgnoreTargetOutside && aiTarget.Entity.Submarine == null) { continue; } + if (aiTarget.Entity is ISerializableEntity se) + { + if (targetParams.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { continue; } + } + if (targetParams.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; } + if (targetParams.IgnoreIfNotInSameSub) + { + if (aiTarget.Entity.Submarine != Character.Submarine) { continue; } + var targetHull = targetCharacter != null ? targetCharacter.CurrentHull : aiTarget.Entity is Item it ? it.CurrentHull : null; + if (targetHull == null != (Character.CurrentHull == null)) { continue; } + } + if (targetParams.State is AIState.Observe or AIState.Eat) + { + if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine) + { + // Never allow observing or eating characters that are inside a different submarine / outside when we are inside. + continue; + } + } + if (aiTarget.Entity is Item targetItem) + { + if (targetParams.IgnoreContained && targetItem.ParentInventory != null) { continue; } + + switch (targetParams.State) + { + case AIState.FleeTo: + { + float healthThreshold = targetParams.Threshold; + if (targetParams.ThresholdMin > 0 && targetParams.ThresholdMax > 0) + { + // If both min and max thresholds are defined, use ThresholdMax when currently fleeing to the target and ThresholdMin when doing something else. + // This is used to make the fractal guardians target the guardian repair pods with a different health threshold: when they are already targeting the pod, they will use ThresholdMax to keep them generating until (nearly) full health. + // And when the guardians are outside the pod, they use ThresholdMin, so that they don't target the pod before falling under certain level of health. + healthThreshold = currentTargetingParams == targetParams && State == AIState.FleeTo ? targetParams.ThresholdMax : targetParams.ThresholdMin; + } + if (Character.HealthPercentage > healthThreshold) + { + continue; + } + break; + } + case AIState.Attack or AIState.Aggressive: + if (!canAttackItems) + { + continue; + } + break; + } + if (targetItem.HasTag(Tags.GuardianShelter)) + { + // Ignore guardian pods completely, if they are targeted by someone else (is or will be occupied). + bool ignore = false; + foreach (Character otherCharacter in Character.CharacterList) + { + if (otherCharacter == Character) { continue; } + if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; } + if (!Character.IsFriendly(otherCharacter)) { continue; } + ignore = true; + break; + } + if (ignore) { continue; } + } + } + matchingTargetParams = targetParams; + } + if (matchingTargetParams == null) { continue; } + #endregion + + #region Modify the priority dynamically + float valueModifier = 1.0f; + if (targetCharacter != null) + { + if (targetCharacter.AIController is EnemyAIController) + { + if (matchingTargetParams.Tag == Tags.Stronger && State is AIState.Avoid or AIState.Escape or AIState.Flee) + { + if (SelectedAiTarget == aiTarget) + { + // Frightened -> hold on to the target + valueModifier *= 2; + } + if (IsBeingChasedBy(targetCharacter)) + { + valueModifier *= 2; + } + if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull)) + { + // Inside but in a different room + valueModifier /= 2; + } + } + } + } + else + { + if (aiTarget.Entity is Structure s) + { + bool isInnerWall = s.Prefab.Tags.Contains("inner"); // Prefer weaker walls (200 is the default for normal hull walls) valueModifier = 200f / s.MaxHealth; for (int i = 0; i < s.Sections.Length; i++) @@ -2832,13 +3150,13 @@ namespace Barotrauma var section = s.Sections[i]; if (section.gap == null) { continue; } bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; - if (attemptToGetInside) + if (tryToGetInside) { if (!isCharacterInside) { if (CanPassThroughHole(s, i)) { - valueModifier *= leadsInside ? (IsAggressiveBoarder ? maxModifier : 1) : 0; + valueModifier *= leadsInside ? (IsAggressiveBoarder ? priorityValueMaxModifier : 1) : 0; } else if (IsAggressiveBoarder && leadsInside && canAttackWalls) { @@ -2901,27 +3219,11 @@ namespace Barotrauma valueModifier *= 1 + section.gap.Open; } } - valueModifier = Math.Clamp(valueModifier, 0, maxModifier); + valueModifier = Math.Clamp(valueModifier, 0, priorityValueMaxModifier); } } if (door != null) { - if (door.Item.Submarine == null) { continue; } - bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom; - // Ignore inner doors when outside - if (Character.CurrentHull == null && !isOutdoor) { continue; } - bool isOpen = door.CanBeTraversed; - if (!isOpen) - { - if (!canAttackDoors) { continue; } - } - else if (Character.AnimController.CanEnterSubmarine != CanEnterSubmarine.True) - { - // Ignore broken and open doors, if cannot enter submarine - // Also ignore them if the monster can only partially enter the sub: - // these monsters tend to be too large to get through doors anyway. - continue; - } if (IsAggressiveBoarder) { if (Character.CurrentHull == null) @@ -2929,82 +3231,40 @@ namespace Barotrauma // Increase the priority if the character is outside and the door is from outside to inside if (door.CanBeTraversed) { - valueModifier = maxModifier; + valueModifier = priorityValueMaxModifier; } else if (door.LinkedGap != null) { - valueModifier = 1 + door.LinkedGap.Open * (maxModifier - 1); + valueModifier = 1 + door.LinkedGap.Open * (priorityValueMaxModifier - 1); } } else { // Inside -> ignore open doors and outer doors + bool isOpen = door.CanBeTraversed; + bool isOutdoor = door.LinkedGap is { FlowTargetHull: not null, IsRoomToRoom: false }; valueModifier = isOpen || isOutdoor ? 0 : 1; } } } - else if (aiTarget.Entity is IDamageable targetDamageable && targetDamageable.Health <= 0.0f) + else if (aiTarget.Entity is IDamageable { Health: <= 0.0f }) { continue; } } - - if (targetingTag == null) { continue; } - var targetParams = GetTargetParams(targetingTag); - if (targetParams == null) { continue; } - if (targetParams.IgnoreInside && Character.CurrentHull != null) { continue; } - if (targetParams.IgnoreOutside && Character.CurrentHull == null) { continue; } - if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; } - if (targetParams.IgnoreTargetInside && aiTarget.Entity.Submarine != null) { continue; } - if (targetParams.IgnoreTargetOutside && aiTarget.Entity.Submarine == null) { continue; } - if (aiTarget.Entity is ISerializableEntity se) - { - if (targetParams.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { continue; } - } - if (targetParams.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; } - if (targetParams.IgnoreIfNotInSameSub) - { - if (aiTarget.Entity.Submarine != Character.Submarine) { continue; } - var targetHull = targetCharacter != null ? targetCharacter.CurrentHull : aiTarget.Entity is Item it ? it.CurrentHull : null; - if ((targetHull == null) != (Character.CurrentHull == null)) { continue; } - } - if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat) - { - if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine) - { - // Never allow observing or eating characters that are inside a different submarine / outside when we are inside. - continue; - } - } - if (aiTarget.Entity is Item targetItem) - { - if (targetParams.IgnoreContained && targetItem.ParentInventory != null) { continue; } - if (targetParams.State == AIState.FleeTo) - { - float target = targetParams.Threshold; - if (targetParams.ThresholdMin > 0 && targetParams.ThresholdMax > 0) - { - target = selectedTargetingParams == targetParams && State == AIState.FleeTo ? targetParams.ThresholdMax : targetParams.ThresholdMin; - } - if (Character.HealthPercentage > target) - { - continue; - } - } - } //no need to eat if the character is already in full health (except if it's a pet - pets actually need to eat to stay alive, not just to regain health) - if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0 && !Character.IsPet) + if (matchingTargetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0 && !Character.IsPet) { valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f); } - valueModifier *= targetParams.Priority; + valueModifier *= matchingTargetParams.Priority; if (valueModifier == 0.0f) { continue; } - if (targetingTag != "decoy") + if (matchingTargetParams.Tag != Tags.Decoy) { if (SwarmBehavior != null && SwarmBehavior.Members.Any()) { // Halve the priority for each swarm mate targeting the same target -> reduces stacking - foreach (Character otherCharacter in SwarmBehavior.Members) + foreach (AICharacter otherCharacter in SwarmBehavior.Members) { if (otherCharacter == Character) { continue; } if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; } @@ -3023,6 +3283,8 @@ namespace Barotrauma } } } + #endregion + if (!aiTarget.IsWithinSector(WorldPosition)) { continue; } Vector2 toTarget = aiTarget.WorldPosition - Character.WorldPosition; float dist = toTarget.Length(); @@ -3032,32 +3294,37 @@ namespace Barotrauma { dist *= 0.9f; } - if (targetParams.PerceptionDistanceMultiplier > 0.0f) + if (matchingTargetParams.PerceptionDistanceMultiplier > 0.0f) { - dist /= targetParams.PerceptionDistanceMultiplier; + dist /= matchingTargetParams.PerceptionDistanceMultiplier; } - if (targetParams.MaxPerceptionDistance > 0.0f && - dist * dist > targetParams.MaxPerceptionDistance * targetParams.MaxPerceptionDistance) + if (matchingTargetParams.MaxPerceptionDistance > 0.0f && + dist * dist > matchingTargetParams.MaxPerceptionDistance * matchingTargetParams.MaxPerceptionDistance) { continue; } - - if (!CanPerceive(aiTarget, dist, checkVisibility: SelectedAiTarget != aiTarget)) + + if (State is AIState.PlayDead && targetCharacter == null) + { + // Only react to characters, when playing dead. + continue; + } + else if (!CanPerceive(aiTarget, dist, checkVisibility: SelectedAiTarget != aiTarget || State is AIState.PlayDead or AIState.Hiding)) { continue; } if (SelectedAiTarget == aiTarget) { - if (Character.Submarine == null && aiTarget.Entity is ISpatialEntity spatialEntity && spatialEntity.Submarine != null) + if (Character.Submarine == null && aiTarget.Entity is ISpatialEntity { Submarine: not null } spatialEntity) { - if (targetingTag == "door" || targetingTag == "wall") + if (matchingTargetParams.Tag == Tags.Door || matchingTargetParams.Tag == Tags.Wall) { Vector2 rayStart = Character.SimPosition; Vector2 rayEnd = aiTarget.SimPosition + spatialEntity.Submarine.SimPosition; Body closestBody = Submarine.PickBody(rayStart, rayEnd, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true); - if (closestBody != null && closestBody.UserData is ISpatialEntity hit) + if (closestBody is { UserData: ISpatialEntity hit }) { Vector2 hitPos = hit.SimPosition; if (closestBody.UserData is Submarine) @@ -3068,8 +3335,8 @@ namespace Barotrauma { hitPos += hit.Submarine.SimPosition; } - float subHalfWidth = spatialEntity.Submarine.Borders.Width / 2; - float subHalfHeight = spatialEntity.Submarine.Borders.Height / 2; + float subHalfWidth = spatialEntity.Submarine.Borders.Width / 2f; + float subHalfHeight = spatialEntity.Submarine.Borders.Height / 2f; Vector2 diff = ConvertUnits.ToDisplayUnits(rayEnd - hitPos); bool isOtherSideOfTheSub = Math.Abs(diff.X) > subHalfWidth || Math.Abs(diff.Y) > subHalfHeight; if (isOtherSideOfTheSub) @@ -3086,9 +3353,9 @@ namespace Barotrauma } if (!isBeingChased) { - if (targetParams.State == AIState.Avoid || targetParams.State == AIState.PassiveAggressive || targetParams.State == AIState.Aggressive) + if (matchingTargetParams.State is AIState.Avoid or AIState.PassiveAggressive or AIState.Aggressive) { - float reactDistance = targetParams.ReactDistance; + float reactDistance = matchingTargetParams.ReactDistance; if (reactDistance > 0 && reactDistance < dist) { // The target is too far and should be ignored. @@ -3100,7 +3367,7 @@ namespace Barotrauma //if the target is very close, the distance doesn't make much difference // -> just ignore the distance and target whatever has the highest priority dist = Math.Max(dist, 100.0f); - AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true, keepAlive: SelectedAiTarget != aiTarget); + targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true, keepAlive: SelectedAiTarget != aiTarget); if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null) { float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y; @@ -3113,7 +3380,7 @@ namespace Barotrauma if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && targetCharacter == null) { - if (targetParams.PrioritizeSubCenter || targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep) + if (matchingTargetParams.PrioritizeSubCenter || matchingTargetParams.AttackPattern is AttackPattern.Circle or AttackPattern.Sweep) { if (!isAnyTargetClose) { @@ -3123,7 +3390,7 @@ namespace Barotrauma float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X); dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter)); } - else if (targetParams.AttackPattern == AttackPattern.Circle) + else if (matchingTargetParams.AttackPattern == AttackPattern.Circle) { dist *= 5; } @@ -3138,16 +3405,16 @@ namespace Barotrauma } // Don't target characters that are outside of the allowed zone, unless chasing or escaping. - switch (targetParams.State) + switch (matchingTargetParams.State) { case AIState.Escape: case AIState.Avoid: break; default: - if (targetParams.State == AIState.Attack) + if (matchingTargetParams.State == AIState.Attack) { // In the attack state allow going into non-allowed zone only when chasing a target. - if (State == targetParams.State && SelectedAiTarget == aiTarget) { break; } + if (State == matchingTargetParams.State && SelectedAiTarget == aiTarget) { break; } } bool insideSameSub = aiTarget?.Entity?.Submarine != null && aiTarget.Entity.Submarine == Character.Submarine; if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _)) @@ -3186,24 +3453,20 @@ namespace Barotrauma // ignore if owner is tagged to be explicitly ignored (Feign Death) continue; } - var characterTargetingTag = GetTargetingTag(owner.AiTarget); - if (!characterTargetingTag.IsEmpty) - { - // if the enemy is configured to ignore the target character, ignore the provocative item they're holding/wearing too - var characterTargetingParams = GetTargetParams(characterTargetingTag); - if (characterTargetingParams?.State == AIState.Idle) { continue; } - } + // if the enemy is configured to ignore the target character, ignore the provocative item they're holding/wearing too + if (GetTargetParams(GetTargetingTags(owner.AiTarget)).Any(t => t.State == AIState.Idle)) { continue; } } } if (targetCharacter != null) { if (Character.CurrentHull != null && targetCharacter.CurrentHull != Character.CurrentHull) { - if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe || targetParams.State == AIState.Eat) + if (matchingTargetParams.State is AIState.Observe or AIState.Eat || + (matchingTargetParams.State is AIState.Follow or AIState.Protect && (!Character.CanClimb || !Character.CanInteract || !AIParams.CanOpenDoors || !Character.Params.UsePathFinding))) { - // Ignore targets that cannot be seen if (!VisibleHulls.Contains(targetCharacter.CurrentHull)) { + // Probably can't get to the target -> ignore. continue; } } @@ -3242,49 +3505,24 @@ namespace Barotrauma } } newTarget = aiTarget; - selectedTargetMemory = targetMemory; + selectedTargetParams = matchingTargetParams; targetValue = valueModifier; - targetingParams = targetParams; if (!isAnyTargetClose) { isAnyTargetClose = ConvertUnits.ToDisplayUnits(colliderLength) > nonModifiedDist; } } } - + + currentTargetingParams = selectedTargetParams; + currentTargetMemory = targetMemory; + State = currentTargetingParams?.State ?? AIState.Idle; SelectedAiTarget = newTarget; - if (SelectedAiTarget != _previousAiTarget) + if ((LatchOntoAI is not { IsAttached: true } || wallTarget != null) && State is AIState.Attack or AIState.Aggressive or AIState.PassiveAggressive) { - if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub) - { - if (SelectedAiTarget?.Entity is not Structure wall) - { - wall = wallTarget?.Structure; - } - // The target is not a wall or it's not the same as we are attached to -> release - bool releaseTarget = wall?.Bodies == null || (!wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB) && wall.Submarine?.PhysicsBody?.FarseerBody != LatchOntoAI.AttachJoints[0].BodyB); - if (!releaseTarget) - { - for (int i = 0; i < wall.Sections.Length; i++) - { - if (CanPassThroughHole(wall, i)) - { - releaseTarget = true; - } - } - } - if (releaseTarget) - { - wallTarget = null; - LatchOntoAI.DeattachFromBody(reset: true, cooldown: 1); - } - } - else - { - wallTarget = null; - } + UpdateWallTarget(requiredHoleCount); } - return SelectedAiTarget; + updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f); } class WallTarget @@ -3624,16 +3862,16 @@ namespace Barotrauma { if (trigger.IsTriggered) { return; } if (activeTriggers.ContainsKey(trigger)) { return; } - if (activeTriggers.ContainsValue(selectedTargetingParams)) + if (activeTriggers.ContainsValue(currentTargetingParams)) { if (!trigger.AllowToOverride) { return; } - var existingTrigger = activeTriggers.FirstOrDefault(kvp => kvp.Value == selectedTargetingParams && kvp.Key.AllowToBeOverridden); + var existingTrigger = activeTriggers.FirstOrDefault(kvp => kvp.Value == currentTargetingParams && kvp.Key.AllowToBeOverridden); if (existingTrigger.Key == null) { return; } activeTriggers.Remove(existingTrigger.Key); } trigger.Launch(); - activeTriggers.Add(trigger, selectedTargetingParams); - ChangeParams(selectedTargetingParams, trigger.State); + activeTriggers.Add(trigger, currentTargetingParams); + ChangeParams(currentTargetingParams, trigger.State); } private void UpdateTriggers(float deltaTime) @@ -3657,34 +3895,32 @@ namespace Barotrauma inactiveTriggers.Clear(); } - private bool TryResetOriginalState(string tag) => - TryResetOriginalState(tag.ToIdentifier()); - /// /// Resets the target's state to the original value defined in the xml. /// private bool TryResetOriginalState(Identifier tag) { if (!modifiedParams.ContainsKey(tag)) { return false; } - if (AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) + if (AIParams.TryGetTargets(tag, out IEnumerable matchingParams)) { - modifiedParams.Remove(tag); - if (tempParams.ContainsKey(tag)) + foreach (var targetParams in matchingParams) { - tempParams.Values.ForEach(t => AIParams.RemoveTarget(t)); - tempParams.Remove(tag); + modifiedParams.Remove(tag); + if (tempParams.ContainsKey(tag)) + { + tempParams.Values.ForEach(t => AIParams.RemoveTarget(t)); + tempParams.Remove(tag); + } + ResetParams(targetParams); + return true; } - ResetParams(targetParams); - return true; - } - else - { - return false; } + return false; } - private readonly Dictionary modifiedParams = new Dictionary(); + private readonly Dictionary> modifiedParams = new Dictionary>(); private readonly Dictionary tempParams = new Dictionary(); + private readonly List tempParamsList = new List(); private void ChangeParams(CharacterParams.TargetParams targetParams, AIState state, float? priority = null) { @@ -3699,24 +3935,22 @@ namespace Barotrauma private void ResetParams(CharacterParams.TargetParams targetParams) { targetParams?.Reset(); - if (selectedTargetingParams == targetParams || State == AIState.Idle || State == AIState.Patrol) + if (currentTargetingParams == targetParams || State is AIState.Idle or AIState.Patrol) { ResetAITarget(); State = AIState.Idle; PreviousState = AIState.Idle; } } - - private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false) - => ChangeParams(tag.ToIdentifier(), state, priority, onlyExisting); private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false, bool ignoreAttacksIfNotInSameSub = false) { - if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) + var existingTargetParams = GetTargetParams(tag); + if (existingTargetParams.None()) { if (!onlyExisting && !tempParams.ContainsKey(tag)) { - if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out targetParams)) + if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out CharacterParams.TargetParams targetParams)) { if (state == AIState.Attack) { @@ -3727,21 +3961,21 @@ namespace Barotrauma } } } - if (targetParams != null) + else { - if (priority.HasValue) + foreach (var targetParams in existingTargetParams) { - targetParams.Priority = Math.Max(targetParams.Priority, priority.Value); - } - targetParams.State = state; - if (!modifiedParams.ContainsKey(tag)) - { - modifiedParams.Add(tag, targetParams); + if (priority.HasValue) + { + targetParams.Priority = Math.Max(targetParams.Priority, priority.Value); + } + targetParams.State = state; } + modifiedParams.TryAdd(tag, existingTargetParams); } } - private void ChangeTargetState(string tag, AIState state, float? priority = null) + private void ChangeTargetState(Identifier tag, AIState state, float? priority = null) { isStateChanged = true; SetStateResetTimer(); @@ -3763,12 +3997,15 @@ namespace Barotrauma } if (target.IsHuman) { - priority = GetTargetParams("human")?.Priority; - // Target also items, because if we are blind and the target doesn't move, we can only perceive the target when it uses items - if (state == AIState.Attack || state == AIState.Escape) + if (AIParams.TryGetHighestPriorityTarget(Tags.Human, out CharacterParams.TargetParams targetParams)) { - ChangeParams("weapon", state, priority); - ChangeParams("tool", state, priority); + priority = targetParams.Priority; + } + // Target also items, because if we are blind and the target doesn't move, we can only perceive the target when it uses items + if (state is AIState.Attack or AIState.Escape) + { + ChangeParams(Tags.Weapon, state, priority); + ChangeParams(Tags.ToolItem, state, priority); } if (state == AIState.Attack) { @@ -3776,17 +4013,17 @@ namespace Barotrauma // --> Target the submarine too. if (target.Submarine != null && Character.Submarine == null && (canAttackDoors || canAttackWalls)) { - ChangeParams("room", state, priority / 2); + ChangeParams(Tags.Room, state, priority / 2); if (canAttackWalls) { - ChangeParams("wall", state, priority / 2); + ChangeParams(Tags.Wall, state, priority / 2); } if (canAttackDoors && IsAggressiveBoarder) { - ChangeParams("door", state, priority / 2); + ChangeParams(Tags.Door, state, priority / 2); } } - ChangeParams("provocative", state, priority, onlyExisting: true); + ChangeParams(Tags.Provocative, state, priority, onlyExisting: true); } } } @@ -3801,11 +4038,38 @@ namespace Barotrauma protected override void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { base.OnTargetChanged(previousTarget, newTarget); - if (newTarget == null) { return; } - var targetParams = GetTargetParams(newTarget); - if (targetParams != null) + if ((newTarget != null || wallTarget != null) && IsLatchedOnSub) { - observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f); + if (newTarget?.Entity is not Structure wall) + { + wall = wallTarget?.Structure; + } + // The target is not a wall or it's not the same as we are attached to -> release + bool releaseTarget = wall?.Bodies == null || (!wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB) && wall.Submarine?.PhysicsBody?.FarseerBody != LatchOntoAI.AttachJoints[0].BodyB); + if (!releaseTarget) + { + for (int i = 0; i < wall.Sections.Length; i++) + { + if (CanPassThroughHole(wall, i)) + { + releaseTarget = true; + } + } + } + if (releaseTarget) + { + wallTarget = null; + LatchOntoAI.DeattachFromBody(reset: true, cooldown: 1); + } + } + else + { + wallTarget = null; + } + if (newTarget == null) { return; } + if (currentTargetingParams != null) + { + observeTimer = currentTargetingParams.Timer * Rand.Range(0.75f, 1.25f); } reachTimer = 0; sinTime = 0; @@ -3820,9 +4084,17 @@ namespace Barotrauma LatchOntoAI?.DeattachFromBody(reset: true); if (disableTailCoroutine != null) { - CoroutineManager.StopCoroutines(disableTailCoroutine); - Character.AnimController.RestoreTemporarilyDisabled(); - disableTailCoroutine = null; + bool isInTransition = from is AIState.HideTo or AIState.Hiding && to is AIState.HideTo or AIState.Hiding; + if (!isInTransition) + { + CoroutineManager.StopCoroutines(disableTailCoroutine); + Character.AnimController.RestoreTemporarilyDisabled(); + disableTailCoroutine = null; + } + } + if (to is AIState.Hiding) + { + ReleaseDragTargets(); } Character.AnimController.ReleaseStuckLimbs(); AttackLimb = null; @@ -3839,6 +4111,16 @@ namespace Barotrauma { CirclePhase = CirclePhase.Start; } + if (to != AIState.Idle) + { + playDeadTimer = PlayDeadCoolDown; + } +#if CLIENT + if (to == AIState.Attack) + { + Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); + } +#endif } private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f); @@ -3857,12 +4139,14 @@ namespace Barotrauma bool insideSoundRange; if (checkVisibility) { + Submarine mySub = Character.Submarine; + Submarine targetSub = target.Entity.Submarine; // We only want to check the visibility when the target is in ruins/wreck/similiar place where sneaking should be possible. // When the monsters attack the player sub, they wall hack so that they can be more aggressive. // Pets should always check the visibility, unless the pet and the target are both outside the submarine -> shouldn't target when they can't perceive (= no wall hack) - checkVisibility = - Character.IsPet && (Character.Submarine != null || target.Entity.Submarine != null) || - target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None; + checkVisibility = + (Character.IsPet && (mySub != null || targetSub != null)) || + (mySub != null && (targetSub == null || (targetSub == mySub && !targetSub.Info.IsPlayer))); } if (dist > 0) { @@ -3918,9 +4202,10 @@ namespace Barotrauma public void ReevaluateAttacks() { - canAttackWalls = LatchOntoAI != null && LatchOntoAI.AttachToSub; + canAttackWalls = LatchOntoAI is { AttachToSub: true }; canAttackDoors = false; canAttackCharacters = false; + canAttackItems = false; foreach (var limb in Character.AnimController.Limbs) { if (limb.IsSevered) { continue; } @@ -3928,11 +4213,17 @@ namespace Barotrauma if (limb.attack == null) { continue; } if (!canAttackWalls) { - canAttackWalls = limb.attack.IsValidTarget(AttackTarget.Structure) && (limb.attack.StructureDamage > 0 || limb.attack.Ranged); + canAttackWalls = (limb.attack.StructureDamage > 0 || limb.attack.Ranged && limb.attack.IsValidTarget(AttackTarget.Structure)); } if (!canAttackDoors) { - canAttackDoors = limb.attack.IsValidTarget(AttackTarget.Structure) && (limb.attack.ItemDamage > 0 || limb.attack.Ranged); + // Doors are technically items, but intentionally treated as structures here. + canAttackDoors = (limb.attack.ItemDamage > 0 || limb.attack.Ranged) && limb.attack.IsValidTarget(AttackTarget.Structure); + } + if (!canAttackItems) + { + // AttackTarget.Structure is also accepted for backwards support. + canAttackItems = canAttackDoors || (limb.attack.ItemDamage > 0 || limb.attack.Ranged) && limb.attack.IsValidTarget(AttackTarget.Structure | AttackTarget.Item); } if (!canAttackCharacters) { @@ -4047,7 +4338,7 @@ namespace Barotrauma State = AIState.Idle; return false; } - else if (SelectedTargetMemory is AITargetMemory targetMemory && SelectedAiTarget?.Entity is Character) + else if (CurrentTargetMemory is AITargetMemory targetMemory && SelectedAiTarget?.Entity is Character) { targetMemory.Priority += deltaTime * PriorityFearIncrement; } @@ -4061,11 +4352,11 @@ namespace Barotrauma else if (canAttackDoors && HasValidPath()) { var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor; - if (door != null && !door.CanBeTraversed && !door.HasAccess(Character)) + if (door is { CanBeTraversed: false } && !door.HasAccess(Character)) { if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack) { - SelectTarget(door.Item.AiTarget, SelectedTargetMemory.Priority); + SelectTarget(door.Item.AiTarget, CurrentTargetMemory.Priority); State = AIState.Attack; return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 8dd5113ae..caf6bbfc8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -317,18 +317,21 @@ namespace Barotrauma { obstacleRaycastTimer = obstacleRaycastIntervalShort; // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). - foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) + if (Submarine.MainSub != null) { - if (connectedSub == Submarine.MainSub) { continue; } - Vector2 rayStart = SimPosition - connectedSub.SimPosition; - Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; - Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); - if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) + foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) { - PathSteering.CurrentPath.Unreachable = true; - break; + if (connectedSub == Submarine.MainSub) { continue; } + Vector2 rayStart = SimPosition - connectedSub.SimPosition; + Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; + Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); + if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) + { + PathSteering.CurrentPath.Unreachable = true; + break; + } } - } + } } } } @@ -801,36 +804,41 @@ namespace Barotrauma if (isCarrying) { return; } if (!ObjectiveManager.CurrentObjective.AllowAutomaticItemUnequipping || !ObjectiveManager.GetActiveObjective().AllowAutomaticItemUnequipping) { return; } - if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem) + if (Character.Submarine?.TeamID == Character.TeamID && findItemState is FindItemState.None or FindItemState.OtherItem) { + // Only unequip other items inside a friendly sub. foreach (Item item in Character.HeldItems) { if (item == null || !item.IsInteractable(Character)) { continue; } - - if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID) + if (Character.TryPutItemInAnySlot(item)) { continue; } + if (Character.TryPutItemInBag(item)) { continue; } + if (item.HasTag(Tags.Weapon)) { - if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Bag })) { continue; } - findItemState = FindItemState.OtherItem; - if (FindSuitableContainer(item, out Item targetContainer)) + // Don't decontain weapons, because it could be that we are holding a weapon that cannot be placed on back (if we have a toolbelt) nor in the any slot, such as an HMG. + // Could check that we only ignore weapons when we've had an order to find a weapon, but it could also be that we picked the weapon for self-defence, on ad-hoc basis. + // And I don't think it would make sense to decontain those weapons either. + continue; + } + findItemState = FindItemState.OtherItem; + if (FindSuitableContainer(item, out Item targetContainer)) + { + findItemState = FindItemState.None; + itemIndex = 0; + if (targetContainer != null) { - findItemState = FindItemState.None; - itemIndex = 0; - if (targetContainer != null) + var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent()); + decontainObjective.Abandoned += () => { - var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => - { - ReequipUnequipped(); - IgnoredItems.Add(targetContainer); - }; - ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); - return; - } - else - { - item.Drop(Character); - HandleRelocation(item); - } + ReequipUnequipped(); + IgnoredItems.Add(targetContainer); + }; + ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); + return; + } + else + { + item.Drop(Character); + HandleRelocation(item); } } } @@ -842,7 +850,7 @@ namespace Barotrauma public void HandleRelocation(Item item) { if (item.SpawnedInCurrentOutpost) { return; } - if (item.Submarine == null) { return; } + if (item.Submarine == null || Submarine.MainSub == null) { return; } // Only affects bots in the player team if (!Character.IsOnPlayerTeam) { return; } // Don't relocate if the item is on a sub of the same team @@ -869,6 +877,7 @@ namespace Barotrauma if (item == null || item.Removed) { return; } if (!itemsToRelocate.Contains(item)) { return; } var mainSub = Submarine.MainSub; + if (mainSub == null) { return; } Entity owner = item.GetRootInventoryOwner(); if (owner != null) { @@ -1295,7 +1304,13 @@ namespace Barotrauma //if (Character.LastDamageSource == null) { return; } //AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced)); } - if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID)) + + bool sameTeam = + attacker.TeamID == Character.TeamID || + // consider escorted characters to be in the same team (otherwise accidental damage or side-effects from healing trigger them too easily) + (attacker.TeamID == CharacterTeamType.Team1 && Character.IsEscorted); + + if (realDamage <= 0 && (attacker.IsBot || sameTeam)) { // Don't react to damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player return; @@ -1307,9 +1322,9 @@ namespace Barotrauma } bool isAttackerInfected = false; bool isAttackerFightingEnemy = false; - float minorDamageThreshold = 1; + float minorDamageThreshold = 5; float majorDamageThreshold = 20; - if (attacker.TeamID == Character.TeamID && !attacker.IsInstigator) + if (sameTeam && !attacker.IsInstigator) { minorDamageThreshold = 10; majorDamageThreshold = 40; @@ -2168,15 +2183,15 @@ namespace Barotrauma float fireFactor = 1; if (!ignoreFire) { - static float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X; + static float CalculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X; // Even the smallest fire reduces the safety by 50% - float fire = visibleHulls == null ? calculateFire(hull) : visibleHulls.Sum(h => calculateFire(h)); + float fire = visibleHulls?.Sum(CalculateFire) ?? CalculateFire(hull); fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1)); } float enemyFactor = 1; if (!ignoreEnemies) { - int enemyCount = 0; + int enemyCount = 0; foreach (Character c in Character.CharacterList) { if (visibleHulls == null) @@ -2476,7 +2491,7 @@ namespace Barotrauma { other = null; if (target?.Item == null) { return false; } - bool isOrder = IsOrderedToOperateThis(Character.AIController); + bool isOrder = IsOrderedToOperateTarget(this); foreach (Character c in Character.CharacterList) { if (!IsActive(c)) { continue; } @@ -2491,14 +2506,14 @@ namespace Barotrauma break; } } - else if (c.AIController is HumanAIController operatingAI) + else if (c.AIController is HumanAIController otherAI) { - if (operatingAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item)) + if (otherAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item)) { // Not targeting the same item. continue; } - bool isTargetOrdered = IsOrderedToOperateThis(c.AIController); + bool isTargetOrdered = IsOrderedToOperateTarget(otherAI); if (!isOrder && isTargetOrdered) { // If the other bot is ordered to operate the item, let him do it, unless we are ordered too @@ -2514,15 +2529,15 @@ namespace Barotrauma } else { - if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective) + if (!IsOperatingTarget(otherAI)) { - // The other bot is ordered to do something else + // The other bot is doing something else -> stick to the target. continue; } if (target is Steering) { // Steering is hard-coded -> cannot use the required skills collection defined in the xml - if (Character.GetSkillLevel("helm") <= c.GetSkillLevel("helm")) + if (Character.GetSkillLevel(Tags.HelmSkill) <= c.GetSkillLevel(Tags.HelmSkill)) { other = c; break; @@ -2538,7 +2553,8 @@ namespace Barotrauma } } return other != null; - bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item; + bool IsOrderedToOperateTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item; + bool IsOperatingTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item; } public bool IsItemRepairedByAnother(Item target, out Character other) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 5ab7b6829..7ece54d71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -320,8 +320,7 @@ namespace Barotrauma Vector2 pos = host.WorldPosition; Vector2 diff = currentPath.CurrentNode.WorldPosition - pos; bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; - // Only humanoids can climb ladders - bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands; + bool canClimb = character.CanClimb; Ladder currentLadder = GetCurrentLadder(); Ladder nextLadder = GetNextLadder(); var ladders = currentLadder ?? nextLadder; @@ -559,26 +558,41 @@ namespace Barotrauma } else { - // We'll want this to run each time, because the delegate is used to find a valid button component. bool canAccessButtons = false; - foreach (var button in door.Item.GetConnectedComponents(true, connectionFilter: c => c.Name == "toggle" || c.Name == "set_state")) + bool buttonsFound = false; + // Check wired controllers (e.g. buttons) + // Always run the buttonFilter delegate (inside CanAccessButton method), if defined, because it's used for find a valid controller component that can be used for closing the door, when needed. + foreach (Controller button in door.Item.GetConnectedComponents(recursive: true, connectionFilter: c => c.Name is "toggle" or "set_state")) { - if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button))) + buttonsFound = true; + if (CanAccessButton(button)) { canAccessButtons = true; } } - foreach (var linked in door.Item.linkedTo) + if (!canAccessButtons) { - if (linked is not Item linkedItem) { continue; } - var button = linkedItem.GetComponent(); - if (button == null) { continue; } - if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button))) + // Check linked controllers (more complex circuits) + foreach (MapEntity linked in door.Item.linkedTo) { - canAccessButtons = true; - } - } - return canAccessButtons || door.IsOpen || ShouldBreakDoor(door); + if (linked is not Item linkedItem) { continue; } + var button = linkedItem.GetComponent(); + if (button == null) { continue; } + buttonsFound = true; + if (CanAccessButton(button)) + { + canAccessButtons = true; + } + } + } + if (door.IsOpen || ShouldBreakDoor(door)) + { + return true; + } + // If no buttons were found, just trust it if we should have the access to the door. Could be there's some other mechanism controlling the door. + return buttonsFound ? canAccessButtons : door.HasAccess(character); + + bool CanAccessButton(Controller button) => button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)); } } @@ -796,10 +810,9 @@ namespace Barotrauma float? penalty = GetSingleNodePenalty(nextNode); if (penalty == null) { return null; } bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y; - //non-humanoids can't climb up ladders - if (!(character.AnimController is HumanoidAnimController)) + if (!character.CanClimb) { - if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands)|| + if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) || (nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up nextNodeAboveWaterLevel)) //upper node not underwater { @@ -847,7 +860,7 @@ namespace Barotrauma if (!node.Waypoint.IsTraversable) { return null; } if (node.IsBlocked()) { return null; } float penalty = 0.0f; - if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f) + if (node.Waypoint.ConnectedGap is { Open: < 0.9f }) { var door = node.Waypoint.ConnectedDoor; if (door == null) @@ -858,19 +871,19 @@ namespace Barotrauma { if (!CanAccessDoor(door, button => { - // Ignore buttons that are on the wrong side of the door + // Ignore buttons that are on the wrong side of the door, unless there's a motion sensor connected to the door, which can be triggered by the character. if (door.IsHorizontal) { if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y)) { - return false; + return door.Item.GetDirectlyConnectedComponent() is MotionSensor ms && ms.TriggersOn(character); } } else { if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X)) { - return false; + return door.Item.GetDirectlyConnectedComponent() is MotionSensor ms && ms.TriggersOn(character); } } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index a7cd0d8cd..7b6ec2cc6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -308,7 +308,7 @@ namespace Barotrauma if (enemyAI.AttackLimb == null) { break; } if (targetBody == null) { break; } if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; } - Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos); + Vector2 referencePos = TargetCharacter?.WorldPosition ?? ConvertUnits.ToDisplayUnits(transformedAttachPos); if (Vector2.DistanceSquared(referencePos, enemyAI.AttackLimb.WorldPosition) < enemyAI.AttackLimb.attack.DamageRange * enemyAI.AttackLimb.attack.DamageRange) { AttachToBody(transformedAttachPos); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index c2d9bf373..a4bf9270a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -513,18 +513,19 @@ namespace Barotrauma /// private bool Check() { + if (isCompleted) { return true; } if (AbortCondition != null && AbortCondition(this)) { Abandon = true; return false; } - return CheckObjectiveSpecific(); + return CheckObjectiveState(); } /// /// Should return whether the objective is completed or not. /// - protected abstract bool CheckObjectiveSpecific(); + protected abstract bool CheckObjectiveState(); private bool CheckState() { @@ -574,8 +575,6 @@ namespace Barotrauma } } - public virtual void SpeakAfterOrderReceived() { } - protected static bool CanPutInInventory(Character character, Item item, bool allowWearing) { if (item == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs index 931215416..a9ba2eae0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs @@ -45,7 +45,7 @@ namespace Barotrauma InitTimers(); } - protected override bool CheckObjectiveSpecific() => false; + protected override bool CheckObjectiveState() => false; protected override float GetPriority() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index e1f1e3ae9..05b059ddf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -117,7 +117,7 @@ namespace Barotrauma objectiveManager.GetObjective().Wander(deltaTime); } - protected override bool CheckObjectiveSpecific() + protected override bool CheckObjectiveState() { if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 78c98414a..a8eeb0daf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -81,6 +81,8 @@ namespace Barotrauma public static bool IsItemInsideValidSubmarine(Item item, Character character) { + if (item == null || item.Removed) { return false; } + if (character == null || character.Removed) { return false; } if (item.CurrentHull == null) { return false; } if (item.Submarine == 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 313448642..c8b27adc1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -257,7 +257,7 @@ namespace Barotrauma } } - protected override bool CheckObjectiveSpecific() + protected override bool CheckObjectiveState() { if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine) { @@ -898,23 +898,13 @@ namespace Barotrauma } } } - - private void Unequip() + + private void UnequipWeapon() { - if (!character.LockHands && character.HeldItems.Contains(Weapon)) - { - if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List() { InvSlotType.Any })) - { - if (Weapon.AllowedSlots.Contains(InvSlotType.Bag)) - { - if (character.Inventory.TryPutItem(Weapon, character, new List() { InvSlotType.Bag })) - { - return; - } - } - Weapon.Drop(character); - } - } + if (Weapon == null) { return; } + if (character.LockHands) { return; } + if (character.HeldItems.Contains(Weapon)) { return; } + character.Unequip(Weapon); } private bool Equip() @@ -929,7 +919,15 @@ namespace Barotrauma ClearInputs(); Weapon.TryInteract(character, forceSelectKey: true); var slots = Weapon.AllowedSlots.Where(CharacterInventory.IsHandSlotType); - if (character.Inventory.TryPutItem(Weapon, character, slots)) + bool successfullyEquipped = character.TryPutItem(Weapon, slots); + if (!successfullyEquipped && character.HasHandsFull(out (Item leftHandItem, Item rightHandItem) items)) + { + // Unequip and try again. + character.Unequip(items.leftHandItem); + character.Unequip(items.rightHandItem); + successfullyEquipped = character.TryPutItem(Weapon, slots); + } + if (successfullyEquipped) { SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed); SetReloadTime(WeaponComponent); @@ -1322,8 +1320,6 @@ namespace Barotrauma aimTimer -= deltaTime; return; } - if (reloadTimer > 0) { return; } - if (holdFireCondition != null && holdFireCondition()) { return; } sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition); distanceTimer = DistanceCheckInterval; if (WeaponComponent is MeleeWeapon meleeWeapon) @@ -1353,9 +1349,11 @@ namespace Barotrauma if (closeEnough && Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25) { // The target is probably knocked down? -> try to reach it by crouching. - HumanAIController.AnimController.Crouching = true; + HumanAIController.AnimController.Crouch(); } } + if (reloadTimer > 0) { return; } + if (holdFireCondition != null && holdFireCondition()) { return; } if (closeEnough) { UseWeapon(deltaTime); @@ -1371,7 +1369,8 @@ namespace Barotrauma { if (WeaponComponent is RepairTool repairTool) { - if (sqrDistance > repairTool.Range * repairTool.Range) { return; } + float reach = AIObjectiveFixLeak.CalculateReach(repairTool, character); + if (sqrDistance > reach * reach) { return; } } float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy); if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor) @@ -1420,11 +1419,8 @@ namespace Barotrauma break; } case MeleeWeapon mw: - { - if (character.AnimController is HumanoidAnimController { Crouching: false }) - { - reloadTime = mw.Reload; - } + { + reloadTime = mw.Reload; break; } } @@ -1485,7 +1481,7 @@ namespace Barotrauma } if (ShouldUnequipWeapon) { - Unequip(); + UnequipWeapon(); } SteeringManager?.Reset(); } @@ -1495,7 +1491,7 @@ namespace Barotrauma base.OnAbandon(); if (ShouldUnequipWeapon) { - Unequip(); + UnequipWeapon(); } SteeringManager?.Reset(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index f6d2702e2..2d55f68a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -77,9 +77,8 @@ namespace Barotrauma this.container = container; } - protected override bool CheckObjectiveSpecific() + protected override bool CheckObjectiveState() { - if (IsCompleted) { return true; } if (container?.Item == null || !container.Item.HasAccess(character)) { Abandon = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs index 5818d9a10..8a22ba597 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs @@ -15,6 +15,7 @@ namespace Barotrauma private Deconstructor deconstructor; private AIObjectiveDecontainItem decontainObjective; + private AIObjectiveGoTo gotoObjective; public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -45,14 +46,24 @@ namespace Barotrauma }, onCompleted: () => { - StartDeconstructor(); - //make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost - if (deconstructor.Item.Submarine is { Info.IsOutpost: true }) + if (character.CanInteractWith(deconstructor.Item)) { - HumanAIController.HandleRelocation(Item); - deconstructor.RelocateOutputToMainSub = true; + StartDeconstruction(); + } + else + { + TryAddSubObjective(ref gotoObjective, + constructor: () => new AIObjectiveGoTo(Item, character, objectiveManager, priorityModifier: PriorityModifier), + onCompleted: () => + { + StartDeconstruction(); + RemoveSubObjective(ref gotoObjective); + }, + onAbandon: () => + { + Abandon = true; + }); } - IsCompleted = true; RemoveSubObjective(ref decontainObjective); }, onAbandon: () => @@ -61,6 +72,18 @@ namespace Barotrauma }); } + private void StartDeconstruction() + { + StartDeconstructor(); + //make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost + if (deconstructor.Item.Submarine is { Info.IsOutpost: true }) + { + HumanAIController.HandleRelocation(Item); + deconstructor.RelocateOutputToMainSub = true; + } + IsCompleted = true; + } + private Deconstructor FindDeconstructor() { Deconstructor closestDeconstructor = null; @@ -86,7 +109,7 @@ namespace Barotrauma deconstructor.SetActive(active: true, user: character, createNetworkEvent: true); } - protected override bool CheckObjectiveSpecific() + protected override bool CheckObjectiveState() { if (Item.IgnoreByAI(character)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs index 4105273c6..4f7b6d28b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs @@ -59,12 +59,14 @@ namespace Barotrauma protected override bool IsValidTarget(Item target) { + if (target == null || target.Removed) { return false; } // If the target was selected as a valid target, we'll have to accept it so that the objective can be completed. // The validity changes when a character picks the item up. if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && AIObjectiveCleanupItems.IsItemInsideValidSubmarine(target, character); } + //note that the item can be outside hulls and still be a valid target - it can be in the character's inventory if (target.CurrentHull != null && target.CurrentHull.FireSources.Count > 0) { return false; } foreach (Character c in Character.CharacterList) @@ -96,7 +98,7 @@ namespace Barotrauma private static bool IsValidTarget(Item item, Character character, bool checkInventory) { - if (item == null) { return false; } + if (item == null || item.Removed) { return false; } if (item.GetRootInventoryOwner() == character) { return true; } return AIObjectiveCleanupItems.IsValidTarget( item, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index 4293dc327..5a81839a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -71,7 +71,7 @@ namespace Barotrauma this.targetContainer = targetContainer; } - protected override bool CheckObjectiveSpecific() => IsCompleted; + protected override bool CheckObjectiveState() => IsCompleted; protected override void Act(float deltaTime) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs index 995f8bb8b..47588af40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs @@ -28,7 +28,7 @@ namespace Barotrauma } public override bool CanBeCompleted => true; - protected override bool CheckObjectiveSpecific() => false; + protected override bool CheckObjectiveState() => false; // escape timer is set to 60 by default to allow players to locate prisoners in time private float escapeTimer = 60f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 302f7a462..a5faf7f4e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -76,7 +76,7 @@ namespace Barotrauma return Priority; } - protected override bool CheckObjectiveSpecific() => targetHull.FireSources.None(); + protected override bool CheckObjectiveState() => targetHull.FireSources.None(); private float sinTime; protected override void Act(float deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index a395f72d6..9a602a097 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -23,7 +23,7 @@ namespace Barotrauma public const float MIN_OXYGEN = 10; - protected override bool CheckObjectiveSpecific() => + protected override bool CheckObjectiveState() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head); public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -39,83 +39,98 @@ namespace Barotrauma TrySetTargetItem(character.Inventory.FindItem( it => it.HasTag(Tags.HeavyDivingGear) && IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true)); } - if (targetItem == null || - !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) && - targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it))) + + bool findDivingGear = targetItem == null || + (!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) && targetItem.ContainedItems.Any(IsSuitableContainedOxygenSource)); + + if (findDivingGear) { - bool mustFindMorePressureProtection = - !objectiveManager.FailedToFindDivingGearForDepth && - character.Inventory.FindItem( - it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null; - TryAddSubObjective(ref getDivingGear, () => + bool mustFindMorePressureProtection = !objectiveManager.FailedToFindDivingGearForDepth && + character.Inventory.FindItem(it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null; + + if (gearTag == Tags.LightDivingGear) { - if (targetItem == null && character.IsOnPlayerTeam) + if (character.GetEquippedItem(Tags.HeavyDivingGear, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes) is Item divingSuit && divingSuit.ContainedItems.None(IsSuitableContainedOxygenSource)) { - character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f); + // A special case: we are already wearing a suit without enough oxygen, but seeking for a mask, because a suit is not really needed. + // This would result into wearing boh the mask and the suit (because the suit shouldn't be unequipped in this situation), which is a bit weird and also suboptimal, because the mask uses the oxygen 2x faster. + // So, let's target the diving suit and try to find oxygen instead. + targetItem = divingSuit; + findDivingGear = false; } - var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) + } + if (findDivingGear) + { + TryAddSubObjective(ref getDivingGear, () => { - AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _), - AllowToFindDivingGear = false, - AllowDangerousPressure = true, - EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head, - Wear = true - }; - if (gearTag == Tags.HeavyDivingGear) - { - if (mustFindMorePressureProtection) + if (targetItem == null && character.IsOnPlayerTeam) { - //if we're looking for a suit specifically because the current suit isn't enough, - //let's ignore unsuitable suits altogether... - getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character); + character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f); } - else + var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { - //...Otherwise it's fine to give a very small priority - //to inadequate suits (a suit not adequate for the depth is better than no suit) - getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f; - } - getItemObjective.GetItemPriority = it => + AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _), + AllowToFindDivingGear = false, + AllowDangerousPressure = true, + EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head, + Wear = true + }; + if (gearTag == Tags.HeavyDivingGear) { - if (IsSuitablePressureProtection(it, gearTag, character)) + if (mustFindMorePressureProtection) { - return 1000.0f; + //if we're looking for a suit specifically because the current suit isn't enough, + //let's ignore unsuitable suits altogether... + getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character); } else { - //if we're looking for a suit specifically because the current suit isn't enough, - //let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority + //...Otherwise it's fine to give a very small priority //to inadequate suits (a suit not adequate for the depth is better than no suit) - return mustFindMorePressureProtection ? 0.0f : 1.0f; + getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f; } - }; - } - return getItemObjective; - }, - onAbandon: () => - { - if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; } - Abandon = true; - }, - onCompleted: () => - { - RemoveSubObjective(ref getDivingGear); - if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable masks, requireEquipped: true)) - { - foreach (Item mask in masks) - { - if (mask != targetItem) + getItemObjective.GetItemPriority = it => { - character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot); + if (IsSuitablePressureProtection(it, gearTag, character)) + { + return 1000.0f; + } + else + { + //if we're looking for a suit specifically because the current suit isn't enough, + //let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority + //to inadequate suits (a suit not adequate for the depth is better than no suit) + return mustFindMorePressureProtection ? 0.0f : 1.0f; + } + }; + } + return getItemObjective; + }, + onAbandon: () => + { + if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; } + Abandon = true; + }, + onCompleted: () => + { + RemoveSubObjective(ref getDivingGear); + if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable masks, requireEquipped: true)) + { + foreach (Item mask in masks) + { + if (mask != targetItem) + { + character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot); + } } } - } - }); + }); + } } - else + if (!findDivingGear) { float min = GetMinOxygen(character); - if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => IsSuitableContainedOxygenSource(it))) + if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(IsSuitableContainedOxygenSource)) { TryAddSubObjective(ref getOxygen, () => { @@ -226,14 +241,7 @@ namespace Barotrauma { if (targetItem == item) { return; } targetItem = item; - if (targetItem != null) - { - oxygenSourceSlotIndex = targetItem.GetComponent()?.FindSuitableSubContainerIndex(Tags.OxygenSource); - } - else - { - oxygenSourceSlotIndex = null; - } + oxygenSourceSlotIndex = targetItem?.GetComponent()?.FindSuitableSubContainerIndex(Tags.OxygenSource); } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 41e4add54..3d5e7ace7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -3,6 +3,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Barotrauma @@ -31,7 +32,7 @@ namespace Barotrauma public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - protected override bool CheckObjectiveSpecific() => false; + protected override bool CheckObjectiveState() => false; public override bool CanBeCompleted => true; private bool resetPriority; @@ -339,6 +340,10 @@ namespace Barotrauma float bestHullValue = 0; bool bestHullIsAirlock = false; Hull potentialBestHull; + +#if DEBUG + private readonly Stopwatch stopWatch = new Stopwatch(); +#endif /// /// Tries to find the best (safe, nearby) hull the character can find a path to. @@ -353,6 +358,9 @@ namespace Barotrauma bestHullIsAirlock = false; hulls.Clear(); var connectedSubs = character.Submarine?.GetConnectedSubs(); +#if DEBUG + stopWatch.Restart(); +#endif foreach (Hull hull in Hull.HullList) { if (hull.Submarine == null) { continue; } @@ -363,25 +371,66 @@ namespace Barotrauma if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; } if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; } if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; } - //sort the hulls based on distance and which sub they're in - //tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily - //(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive - //path calculations, only to discard all of them when going through the hulls in the outpost) - float hullSuitability = EstimateHullSuitability(character, hull); if (hulls.None()) { hulls.Add(hull); } else { + //sort the hulls first based on distance and a rough suitability estimation + //tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily + //(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive + //path calculations, only to discard all of them when going through the hulls in the outpost) + bool addLast = true; + float hullSuitability = EstimateHullSuitability(hull); for (int i = 0; i < hulls.Count; i++) { - if (hullSuitability > EstimateHullSuitability(character, hulls[i])) + Hull otherHull = hulls[i]; + float otherHullSuitability = EstimateHullSuitability(otherHull); + if (hullSuitability > otherHullSuitability) { hulls.Insert(i, hull); + addLast = false; break; } } + if (addLast) + { + hulls.Add(hull); + } + } + + float EstimateHullSuitability(Hull h) + { + float distX = Math.Abs(h.WorldPosition.X - character.WorldPosition.X); + float distY = Math.Abs(h.WorldPosition.Y - character.WorldPosition.Y); + if (character.CurrentHull != null) + { + distY *= 3; + } + float dist = distX + distY; + float suitability = -dist; + const float suitabilityReduction = 10000.0f; + if (h.Submarine != character.Submarine) + { + suitability -= suitabilityReduction; + } + if (character.CurrentHull != null) + { + if (h.AvoidStaying) + { + suitability -= suitabilityReduction; + } + if (HumanAIController.UnsafeHulls.Contains(h)) + { + suitability -= suitabilityReduction; + } + if (HumanAIController.NeedsDivingGear(h, out _)) + { + suitability -= suitabilityReduction; + } + } + return suitability; } } if (hulls.None()) @@ -390,19 +439,10 @@ namespace Barotrauma return HullSearchStatus.Finished; } hullSearchIndex = 0; - } - - static float EstimateHullSuitability(Character character, Hull hull) - { - float dist = - Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) + - Math.Abs(hull.WorldPosition.Y - character.WorldPosition.Y) * 3; - float suitability = -dist; - if (hull.Submarine != character.Submarine) - { - suitability -= 10000.0f; - } - return suitability; +#if DEBUG + stopWatch.Stop(); + DebugConsole.NewMessage($"({character.DisplayName}) Sorted hulls by suitability in {stopWatch.ElapsedMilliseconds} ms", debugOnly: true); +#endif } Hull potentialHull = hulls[hullSearchIndex]; @@ -420,7 +460,7 @@ namespace Barotrauma if (hullSafety > bestHullValue) { //avoid airlock modules if not allowed to change the sub - if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock")) + if (allowChangingSubmarine || potentialHull.OutpostModuleTags.All(t => t != "airlock")) { // Don't allow to go outside if not already outside. var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(potentialHull), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs index bcec87ea1..44519a290 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs @@ -176,6 +176,8 @@ namespace Barotrauma //only player's crew can steal, ignore other teams if (!target.IsOnPlayerTeam) { return false; } if (target.IsHandcuffed) { return false; } + //ignore thieves in the same team + if (character.OriginalTeamID == target.TeamID || character.TeamID == target.TeamID) { return false; } // Ignore targets that are climbing, because might need to use ladders to get to them. if (target.IsClimbing) { return false; } if (HumanAIController.IsTrueForAnyBotInTheCrew(bot => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 8709d29a6..c0b1114e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -31,7 +31,7 @@ namespace Barotrauma this.isPriority = isPriority; } - protected override bool CheckObjectiveSpecific() => Leak.Open <= 0 || Leak.Removed; + protected override bool CheckObjectiveState() => Leak.Open <= 0 || Leak.Removed; protected override float GetPriority() { @@ -166,7 +166,7 @@ namespace Barotrauma // TODO: use the collider size/reach? if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150) { - HumanAIController.AnimController.Crouching = true; + HumanAIController.AnimController.Crouch(); } float reach = CalculateReach(repairTool, character); bool canOperate = toLeak.LengthSquared() < reach * reach; @@ -180,7 +180,7 @@ namespace Barotrauma onAbandon: () => Abandon = true, onCompleted: () => { - if (CheckObjectiveSpecific()) { IsCompleted = true; } + if (CheckObjectiveState()) { IsCompleted = true; } else { // Failed to operate. Probably too far. @@ -202,11 +202,11 @@ namespace Barotrauma endNodeFilter = IsSuitableEndNode, // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) // Only report about contextual targets. - SpeakCannotReachCondition = () => isPriority && !CheckObjectiveSpecific() + SpeakCannotReachCondition = () => isPriority && !CheckObjectiveState() }, onAbandon: () => { - if (CheckObjectiveSpecific()) { IsCompleted = true; } + if (CheckObjectiveState()) { IsCompleted = true; } else if ((Leak.WorldPosition - character.AnimController.AimSourceWorldPos).LengthSquared() > MathUtils.Pow(reach * 2, 2)) { // Too far diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 0aad3d587..1600d588f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -658,9 +658,8 @@ namespace Barotrauma return bestItem; } - protected override bool CheckObjectiveSpecific() + protected override bool CheckObjectiveState() { - if (IsCompleted) { return true; } if (targetItem == null) { // Not yet ready diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs index f35f6f96b..8569b0776 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -44,7 +44,7 @@ namespace Barotrauma ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToImmutableHashSet(); } - protected override bool CheckObjectiveSpecific() => subObjectivesCreated && subObjectives.None(); + protected override bool CheckObjectiveState() => subObjectivesCreated && subObjectives.None(); protected override void Act(float deltaTime) { @@ -56,7 +56,7 @@ namespace Barotrauma AIObjectiveGetItem? getItem = null; TryAddSubObjective(ref getItem, () => { - var getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1) + getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1) { AllowVariants = AllowVariants, Wear = Wear, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 1fab5c816..d86d61052 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -95,6 +96,9 @@ namespace Barotrauma protected override bool AllowInAnySub => true; public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier(); + private readonly Identifier ExoSuitRefuel = "dialog.exosuit.refuel".ToIdentifier(); + private readonly Identifier ExoSuitOutOfFuel = "dialog.exosuit.outoffuel".ToIdentifier(); + public LocalizedString TargetName { get; set; } public ISpatialEntity Target { get; private set; } @@ -194,6 +198,43 @@ namespace Barotrauma Abandon = true; return; } + if (checkExoSuitTimer <= 0) + { + checkExoSuitTimer = CheckExoSuitTime * Rand.Range(0.9f, 1.1f); + if (character.GetEquippedItem(Tags.PoweredDivingSuit, InvSlotType.OuterClothes) is { OwnInventory: Inventory exoSuitInventory } exoSuit && + exoSuit.GetComponent() is not { HasPower: true }) + { + if (HumanAIController.HasItem(character, Tags.DivingSuitFuel, out IEnumerable fuelRods, conditionPercentage: 1, recursive: true)) + { + // Try to switch the fuel sources + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.Get(ExoSuitRefuel).Value, minDurationBetweenSimilar: 10f, identifier: ExoSuitRefuel); + } + // Have to copy the list, because it's modified when we unequip the item. + foreach (Item containedItem in exoSuit.ContainedItems.ToList()) + { + if (containedItem.HasTag(Tags.DivingSuitFuel) && containedItem.Condition <= 0) + { + character.Unequip(containedItem); + } + } + // Refuel + // The information about the target slot is defined in a status effect. We could parse it, but let's keep it simple and just presume that the target slot is the second slot, as it the case with the vanilla exosuits. + const int targetSlot = 1; + Item fuelRod = fuelRods.MaxBy(b => b.Condition); + exoSuitInventory.TryPutItem(fuelRod, targetSlot, allowSwapping: true, allowCombine: true, user: character); + } + else if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.Get(ExoSuitOutOfFuel).Value, minDurationBetweenSimilar: 30.0f, identifier: ExoSuitOutOfFuel); + } + } + } + else + { + checkExoSuitTimer -= deltaTime; + } if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy)) { // Wait @@ -353,9 +394,9 @@ namespace Barotrauma } else { - // Try again without requiring the diving suit + // Try again without requiring the diving suit (or mask) RemoveSubObjective(ref findDivingGear); - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: !tryToGetDivingSuit, objectiveManager), onAbandon: () => { Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); @@ -442,7 +483,7 @@ namespace Barotrauma if (checkScooterTimer <= 0) { useScooter = false; - checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f); + checkScooterTimer = CheckScooterTime * Rand.Range(0.9f, 1.1f); Item scooter = null; bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(Tags.Scooter, allowBroken: false); if (!shouldUseScooter) @@ -465,24 +506,25 @@ namespace Barotrauma } else if (shouldUseScooter) { - var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand); - var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand); - bool handsFull = - (leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem) && !character.Inventory.TryPutItem(leftHandItem, character, InvSlotType.Bag.ToEnumerable())) || - (rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem) && !character.Inventory.TryPutItem(rightHandItem, character, InvSlotType.Bag.ToEnumerable())); - if (!handsFull) + bool hasHandsFull = character.HasHandsFull(out (Item leftHandItem, Item rightHandItem) items); + if (hasHandsFull) + { + hasHandsFull = !character.TryPutItemInAnySlot(items.leftHandItem) && + !character.TryPutItemInAnySlot(items.rightHandItem) && + !character.TryPutItemInBag(items.leftHandItem) && + !character.TryPutItemInBag(items.rightHandItem); + } + if (!hasHandsFull) { bool hasBattery = false; - if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable nonEquippedScooters, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false)) + if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable nonEquippedScootersWithBattery, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false)) { - // Non-equipped scooter with a battery - scooter = nonEquippedScooters.FirstOrDefault(); + scooter = nonEquippedScootersWithBattery.FirstOrDefault(); hasBattery = true; } - else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable _nonEquippedScooters, requireEquipped: false)) + else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable nonEquippedScootersWithoutBattery, requireEquipped: false)) { - // Non-equipped scooter without a battery - scooter = _nonEquippedScooters.FirstOrDefault(); + scooter = nonEquippedScootersWithoutBattery.FirstOrDefault(); // Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this. hasBattery = HumanAIController.HasItem(character, Tags.MobileBattery, out _, requireEquipped: false, conditionPercentage: 1, recursive: false); } @@ -518,8 +560,7 @@ namespace Barotrauma } if (!useScooter) { - // Unequip - character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot); + character.TryPutItemInAnySlot(scooter); } } } @@ -663,7 +704,10 @@ namespace Barotrauma private bool useScooter; private float checkScooterTimer; - private readonly float checkScooterTime = 0.5f; + private const float CheckScooterTime = 0.5f; + + private float checkExoSuitTimer; + private const float CheckExoSuitTime = 2.0f; public Hull GetTargetHull() => GetTargetHull(Target); @@ -764,9 +808,8 @@ namespace Barotrauma } } - protected override bool CheckObjectiveSpecific() + protected override bool CheckObjectiveState() { - if (IsCompleted) { return true; } // First check the distance and then if can interact (heaviest) if (Target == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 07343eb5c..a185ad968 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -88,7 +88,7 @@ namespace Barotrauma CalculatePriority(); } - protected override bool CheckObjectiveSpecific() => false; + protected override bool CheckObjectiveState() => false; public override bool CanBeCompleted => true; public readonly HashSet PreferredOutpostModuleTypes = new HashSet(); @@ -158,8 +158,17 @@ namespace Barotrauma { character.DeselectCharacter(); } - - character.SelectedItem = null; + if (character.SelectedItem != null) + { + if (character.SelectedItem.Prefab.AllowDeselectWhenIdling) + { + character.SelectedItem = null; + } + else + { + return; + } + } if (!character.IsClimbing) { @@ -489,27 +498,26 @@ namespace Barotrauma if (checkItemsTimer <= 0) { checkItemsTimer = checkItemsInterval * Rand.Range(0.9f, 1.1f); - var hull = character.CurrentHull; - if (hull != null) + if (character.Submarine is not Submarine sub) { return; } + if (sub.TeamID != character.TeamID) { return; } + if (character.CurrentHull is not Hull currentHull) { return; } + itemsToClean.Clear(); + foreach (Item item in Item.CleanableItems) { - itemsToClean.Clear(); - foreach (Item item in Item.CleanableItems) + if (item.CurrentHull != currentHull) { continue; } + if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item)) { - if (item.CurrentHull != hull) { continue; } - if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item)) - { - itemsToClean.Add(item); - } + itemsToClean.Add(item); } - if (itemsToClean.Any()) + } + if (itemsToClean.Any()) + { + var targetItem = itemsToClean.MinBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X)); + if (targetItem != null) { - var targetItem = itemsToClean.OrderBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X)).FirstOrDefault(); - if (targetItem != null) - { - var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier); - cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem); - subObjectives.Add(cleanupObjective); - } + var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier); + cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem); + subObjectives.Add(cleanupObjective); } } } @@ -534,6 +542,8 @@ namespace Barotrauma itemsToClean.Clear(); ignoredItems.Clear(); autonomousObjectiveRetryTimer = 10; + timerMargin = 0; + newTargetTimer = 0; } public override void OnDeselected() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs index 7295391b4..b8639dd08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveInspectNoises.cs @@ -120,7 +120,6 @@ namespace Barotrauma { } - protected override bool CheckObjectiveSpecific() => false; - + protected override bool CheckObjectiveState() => false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index 3649eb11c..0b3b5c52e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -318,7 +318,7 @@ namespace Barotrauma return true; } - protected override bool CheckObjectiveSpecific() => IsCompleted; + protected override bool CheckObjectiveState() => IsCompleted; public override void Reset() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index 185d057b1..894f27e60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -71,7 +71,7 @@ namespace Barotrauma if (item.IsClaimedByBallastFlora) { return false; } if (!item.HasAccess(character)) { return false; } // Ignore items that require power but don't have it - if (item.GetComponent() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; } + if (item.GetComponent() is { PowerConsumption: > 0, HasPower: false }) { return false; } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 04cc059e3..3935b931a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -53,7 +53,7 @@ namespace Barotrauma : base(character, objectiveManager, priorityModifier, option) { } protected override void Act(float deltaTime) { } - protected override bool CheckObjectiveSpecific() => false; + protected override bool CheckObjectiveState() => false; public override bool CanBeCompleted => true; public override bool AbandonWhenCannotCompleteSubObjectives => false; public override bool AllowSubObjectiveSorting => true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index e59e3e0fb..f5d6340f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -228,11 +228,7 @@ namespace Barotrauma coroutine = CoroutineManager.Invoke(() => { //round ended before the coroutine finished -#if CLIENT - if (GameMain.GameSession == null || Level.Loaded == null && !(GameMain.GameSession.GameMode is TestGameMode)) { return; } -#else - if (GameMain.GameSession == null || Level.Loaded == null) { return; } -#endif + if (GameMain.GameSession == null || Level.Loaded == null && GameMain.GameSession.GameMode is not TestGameMode) { return; } DelayedObjectives.Remove(objective); AddObjective(objective); callback?.Invoke(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index be6674906..1d17d2100 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -312,7 +312,7 @@ namespace Barotrauma } } - protected override bool CheckObjectiveSpecific() => isDoneOperating && !Repeat; + protected override bool CheckObjectiveState() => isDoneOperating && !Repeat; public override void Reset() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index 95ea194d8..bd1a9e375 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -54,7 +54,7 @@ namespace Barotrauma } } - protected override bool CheckObjectiveSpecific() => IsCompleted; + protected override bool CheckObjectiveState() => IsCompleted; protected override float GetPriority() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index cb4becf62..aad92fb39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -91,7 +91,7 @@ namespace Barotrauma return Priority; } - protected override bool CheckObjectiveSpecific() + protected override bool CheckObjectiveState() { IsCompleted = Item.IsFullCondition; if (character.IsOnPlayerTeam && IsCompleted && IsRepairing()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 2d6c385d1..afdb12c89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -70,7 +70,7 @@ namespace Barotrauma if (otherRescuer != null && otherRescuer != character) { // Someone else is rescuing/holding the target. - Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical"); + Abandon = otherRescuer.IsPlayer || character.GetSkillLevel(Tags.MedicalSkill) < otherRescuer.GetSkillLevel(Tags.MedicalSkill); return; } if (Target != character) @@ -391,9 +391,18 @@ namespace Barotrauma ("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value, null, 2.0f, $"listrequiredtreatments{Target.Name}".ToIdentifier(), 60.0f); } + + var itemsToFind = currentTreatmentSuitabilities + //items that have a positive effect and that the bot doesn't yet have + .Where(kvp => kvp.Value > 0.0f && character.Inventory.AllItems.None(it => it.Prefab.Identifier == kvp.Key)) + .Select(kvp => kvp.Key); + RemoveSubObjective(ref getItemObjective); TryAddSubObjective(ref getItemObjective, - constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), + constructor: () => new AIObjectiveGetItem(character, itemsToFind, objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + { + GetItemPriority = it => currentTreatmentSuitabilities.GetValueOrDefault(it.Prefab.Identifier) + }, onCompleted: () => RemoveSubObjective(ref getItemObjective), onAbandon: () => { @@ -468,16 +477,16 @@ namespace Barotrauma item.ApplyTreatment(character, Target, Target.CharacterHealth.GetAfflictionLimb(affliction)); } - protected override bool CheckObjectiveSpecific() + protected override bool CheckObjectiveState() { - bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(Target) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, Target); - if (isCompleted && Target != character && character.IsOnPlayerTeam) + IsCompleted = AIObjectiveRescueAll.GetVitalityFactor(Target) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, Target); + if (IsCompleted && Target != character && character.IsOnPlayerTeam) { string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed"; string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.Name)?.Value; character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f); } - return isCompleted; + return IsCompleted; } protected override float GetPriority() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index e27ce1933..4c8331b52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -96,13 +96,20 @@ namespace Barotrauma { float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f); vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100; - if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + if (affliction.Strength > affliction.Prefab.TreatmentThreshold) { - vitality -= affliction.Strength; - } - else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType) - { - vitality -= affliction.Strength; + if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + { + vitality -= affliction.Strength; + } + else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType) + { + vitality -= affliction.Strength; + } + else if (affliction.Prefab == AfflictionPrefab.HuskInfection) + { + vitality -= affliction.Strength; + } } } return Math.Clamp(vitality, 0, 100); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index c580a2d0c..749515fce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -7,7 +7,7 @@ namespace Barotrauma class AIObjectiveReturn : AIObjective { public override Identifier Identifier { get; set; } = "return".ToIdentifier(); - public Submarine ReturnTarget { get; } + public Submarine Target { get; } private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective; private bool usingEscapeBehavior, isSteeringThroughGap; @@ -17,10 +17,13 @@ namespace Barotrauma public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier) { - ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded); - if (ReturnTarget == null) + Target = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded); + if (Target == null) { - DebugConsole.AddSafeError("Error with a Return objective: no suitable return target found"); + if (GameMain.GameSession.GameMode is not TestGameMode) + { + DebugConsole.AddWarning($"({character.DisplayName}) No suitable return target found. Cannot return back to the main sub."); + } Abandon = true; } @@ -54,7 +57,7 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (ReturnTarget == null) + if (Target == null) { Abandon = true; return; @@ -62,7 +65,7 @@ namespace Barotrauma bool shouldUseEscapeBehavior = false; if (character.CurrentHull != null || isSteeringThroughGap) { - if (character.Submarine == null || !character.Submarine.IsConnectedTo(ReturnTarget)) + if (character.Submarine == null || !character.Submarine.IsConnectedTo(Target)) { // Character is on another sub that is not connected to the target sub, use the escape behavior to get them out shouldUseEscapeBehavior = true; @@ -76,13 +79,13 @@ namespace Barotrauma Abandon = true; } } - else if (character.Submarine != ReturnTarget) + else if (character.Submarine != Target) { // Character is on another sub that is connected to the target sub, create a Go To objective to reach the target sub if (moveInsideObjective == null) { Hull targetHull = null; - foreach (var d in ReturnTarget.ConnectedDockingPorts.Values) + foreach (var d in Target.ConnectedDockingPorts.Values) { if (!d.Docked) { continue; } if (d.DockingTarget == null) { continue; } @@ -143,7 +146,7 @@ namespace Barotrauma Hull targetHull = null; float targetDistanceSquared = float.MaxValue; bool targetIsAirlock = false; - foreach (var hull in ReturnTarget.GetHulls(false)) + foreach (var hull in Target.GetHulls(false)) { bool hullIsAirlock = hull.IsAirlock; if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character))) @@ -178,18 +181,14 @@ namespace Barotrauma usingEscapeBehavior = shouldUseEscapeBehavior; } - protected override bool CheckObjectiveSpecific() + protected override bool CheckObjectiveState() { - if (IsCompleted) - { - return true; - } - if (ReturnTarget == null) + if (Target == null) { Abandon = true; return false; } - if (character.Submarine == ReturnTarget) + if (character.Submarine == Target) { IsCompleted = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 9533252c1..95a6cab00 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -204,12 +204,8 @@ namespace Barotrauma var allTargetItems = new List(); for (int i = 0; i < AllOptions.Length; i++) { - Identifier[] optionTargetItemsSplit = i < splitTargetItems.Length ? splitTargetItems[i].Split(',', ',').ToIdentifiers() : Array.Empty(); - for (int j = 0; j < optionTargetItemsSplit.Length; j++) - { - optionTargetItemsSplit[j] = optionTargetItemsSplit[j].Value.Trim().ToIdentifier(); - allTargetItems.Add(optionTargetItemsSplit[j]); - } + Identifier[] optionTargetItemsSplit = i < splitTargetItems.Length ? splitTargetItems[i].ToIdentifiers().ToArray() : Array.Empty(); + allTargetItems.AddRange(optionTargetItemsSplit); optionTargetItems.Add(AllOptions[i], optionTargetItemsSplit.ToImmutableArray()); } TargetItems = allTargetItems.ToImmutableArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index a859d0c50..6484ee5cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -45,7 +45,8 @@ namespace Barotrauma public float HappyThreshold { get; set; } public float MaxHappiness { get; set; } - + + public bool HideStatusIndicators { get; set; } /// /// At which point is the pet considered "hungry" (playing unhappy sounds and showing the icon) @@ -59,6 +60,14 @@ namespace Barotrauma public float PlayForce { get; set; } public float PlayTimer { get; set; } + + public float PlayCooldown { get; set; } + + /// + /// Should the pet lose ownership (and stop following) when the same character interacts with it twice? Unlike with other pets, if another character interacts with the pet, they will become the owner. + /// + public bool ToggleOwner { get; set; } + private float? UnstunY { get; set; } public EnemyAIController AIController { get; private set; } = null; @@ -162,7 +171,7 @@ namespace Barotrauma private class Food { - public string Tag; + public Identifier Tag; public Vector2 HungerRange; public float Hunger; public float Happiness; @@ -182,6 +191,7 @@ namespace Barotrauma MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f); UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f); HappyThreshold = element.GetAttributeFloat(nameof(HappyThreshold), MaxHappiness * 0.8f); + HideStatusIndicators = element.GetAttributeBool(nameof(HideStatusIndicators), false); MaxHunger = element.GetAttributeFloat(nameof(MaxHunger), 100.0f); HungryThreshold = element.GetAttributeFloat(nameof(HungryThreshold), MaxHunger * 0.5f); @@ -192,7 +202,9 @@ namespace Barotrauma HappinessDecreaseRate = element.GetAttributeFloat(nameof(HappinessDecreaseRate), 0.1f); HungerIncreaseRate = element.GetAttributeFloat(nameof(HungerIncreaseRate), 0.25f); - PlayForce = element.GetAttributeFloat("playforce", 15.0f); + PlayForce = element.GetAttributeFloat(nameof(PlayForce), 15.0f); + PlayCooldown = element.GetAttributeFloat(nameof(PlayCooldown), 5.0f); + ToggleOwner = element.GetAttributeBool(nameof(ToggleOwner), false); foreach (var subElement in element.Elements()) { @@ -204,7 +216,7 @@ namespace Barotrauma case "eat": Food food = new Food { - Tag = subElement.GetAttributeString("tag", ""), + Tag = subElement.GetAttributeIdentifier("tag", Identifier.Empty), Hunger = subElement.GetAttributeFloat("hunger", -1), Happiness = subElement.GetAttributeFloat("happiness", 1), Priority = subElement.GetAttributeFloat("priority", 100), @@ -227,6 +239,7 @@ namespace Barotrauma public StatusIndicatorType GetCurrentStatusIndicatorType() { + if (HideStatusIndicators) { return StatusIndicatorType.None; } if (Hunger > HungryThreshold) { return StatusIndicatorType.Hungry; } if (Happiness > HappyThreshold) { return StatusIndicatorType.Happy; } if (Happiness < UnhappyThreshold) { return StatusIndicatorType.Sad; } @@ -283,14 +296,22 @@ namespace Barotrauma public void Play(Character player) { if (PlayTimer > 0.0f) { return; } - Owner ??= player; - PlayTimer = 5.0f; + if (!AIController.Character.IsFriendly(player)) { return; } + if (ToggleOwner) + { + Owner = Owner == player ? null : player; + } + else + { + Owner ??= player; + } + PlayTimer = PlayCooldown; AIController.Character.IsRagdolled = true; Happiness += 10.0f; AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce); UnstunY = AIController.Character.SimPosition.Y; #if CLIENT - AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f); + AIController.Character.PlaySound(Owner == null ? CharacterSound.SoundType.Unhappy : CharacterSound.SoundType.Happy); #endif } @@ -318,7 +339,7 @@ namespace Barotrauma if (UnstunY.HasValue) { - if (PlayTimer > 4.0f) + if (PlayTimer > PlayCooldown - 1.0f) { float extent = character.AnimController.MainLimb.body.GetMaxExtent(); if (character.SimPosition.Y < (UnstunY.Value + extent * 3.0f) && @@ -354,9 +375,12 @@ namespace Barotrauma { if (food.TargetParams == null) { - if (AIController.AIParams.TryGetTarget(food.Tag, out TargetParams target)) + if (AIController.AIParams.TryGetTargets(food.Tag, out IEnumerable existingTargetParams)) { - food.TargetParams = target; + foreach (var targetParams in existingTargetParams) + { + food.TargetParams = targetParams; + } } else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams)) { @@ -444,11 +468,15 @@ namespace Barotrauma } else { + WayPoint spawnPoint = null; //try to find a spawnpoint in the main sub - var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced(); + if (Submarine.MainSub != null) + { + spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced(); + } //if not found, try any player sub (shuttle/drone etc) spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced(); - spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; + spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub?.WorldPosition ?? Vector2.Zero; } var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName.ToIdentifier()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index bf6db4177..166623265 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -52,7 +52,9 @@ namespace Barotrauma { if (orderedCharacter != CommandingCharacter) { - CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false), minDurationBetweenSimilar: 5); + CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false), + minDurationBetweenSimilar: 5, + identifier: ("GiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier()); } CurrentOrder = SuggestedOrder .WithOption(Option) @@ -60,7 +62,9 @@ namespace Barotrauma .WithOrderGiver(CommandingCharacter) .WithManualPriority(CharacterInfo.HighestManualOrderPriority); OrderedCharacter.SetOrder(CurrentOrder, CommandingCharacter != OrderedCharacter); - OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f, minDurationBetweenSimilar: 5); + OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f, + minDurationBetweenSimilar: 5, + identifier: ("ReceiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier()); } TimeSinceLastAttempt = 0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs index aa6328e96..648b5f224 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs @@ -11,7 +11,7 @@ namespace Barotrauma public override void CalculateImportanceSpecific() { if (shipCommandManager.NavigationState == ShipCommandManager.NavigationStates.Inactive) { return; } - if (TargetItemComponent is Powered powered && powered.Voltage <= powered.MinVoltage) { return; } + if (TargetItemComponent is Powered { HasPower: false }) { return; } if (TargetItem.Condition <= 0f) { return; } Importance = 70f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 958368329..5f9ba04f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -38,21 +38,7 @@ namespace Barotrauma public readonly AnimationType AnimationType; public readonly AnimationParams TemporaryAnimation; public readonly float Priority; - public bool IsActive - { - get { return _isActive; } - set - { - if (value) - { - expirationTimer = expirationTime; - } - _isActive = value; - } - } - private bool _isActive; - private float expirationTimer; - private const float expirationTime = 0.1f; + public bool IsActive; public AnimSwap(AnimationParams temporaryAnimation, float priority) { @@ -61,15 +47,6 @@ namespace Barotrauma Priority = priority; IsActive = true; } - - public void Update(float deltaTime) - { - expirationTimer -= deltaTime; - if (expirationTimer <= 0) - { - IsActive = false; - } - } } protected readonly Dictionary tempAnimations = new Dictionary(); @@ -151,7 +128,8 @@ namespace Barotrauma } else { - return Math.Abs(TargetMovement.X) > (WalkParams.MovementSpeed + RunParams.MovementSpeed) / 2.0f; + float movementSpeed = IsClimbing ? TargetMovement.Y : TargetMovement.X; + return Math.Abs(movementSpeed) > (WalkParams.MovementSpeed + RunParams.MovementSpeed) / 2.0f; } } } @@ -226,7 +204,7 @@ namespace Barotrauma public void UpdateAnimations(float deltaTime) { - UpdateTemporaryAnimations(deltaTime); + UpdateTemporaryAnimations(); UpdateAnim(deltaTime); } @@ -338,6 +316,31 @@ namespace Barotrauma { FlipLockTime = (float)Timing.TotalTime + time; } + + protected void UpdateConstantTorque(float deltaTime) + { + foreach (var limb in Limbs) + { + if (limb.IsSevered) { continue; } + if (Math.Abs(limb.Params.ConstantTorque) > 0) + { + // TODO: not sure if this works on ground + float movementFactor = Math.Max(character.AnimController.Collider.LinearVelocity.Length() * 0.5f, 1); + limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque * movementFactor, wrapAngle: true); + } + } + } + + protected void UpdateBlink(float deltaTime) + { + foreach (var limb in Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.Params.BlinkFrequency <= 0) { continue; } + if (!limb.InWater && limb.Params.OnlyBlinkInWater) { continue; } + limb.UpdateBlink(deltaTime, MainLimb.Rotation); + } + } public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { @@ -408,9 +411,9 @@ namespace Barotrauma character.WorldPosition.Y - handWorldPos.Y > ConvertUnits.ToDisplayUnits(CurrentGroundedParams.TorsoPosition) / 4 && this is HumanoidAnimController humanoidAnimController) { - humanoidAnimController.Crouching = true; + humanoidAnimController.Crouch(); + // TODO: is this redundant/required? humanoidAnimController.ForceSelectAnimationType = AnimationType.Crouch; - character.SetInput(InputType.Crouch, hit: false, held: true); } } @@ -696,6 +699,294 @@ namespace Barotrauma hand.body.SmoothRotate(handAngle, 10.0f * handTorque * hand.Mass, wrapAngle: false); } } + + private float prevFootPos; + protected void UpdateClimbing() + { + var ladder = character.SelectedSecondaryItem?.GetComponent(); + if (character.IsIncapacitated) + { + Anim = Animation.None; + return; + } + else if (ladder == null) + { + StopClimbing(); + return; + } + + onGround = false; + IgnorePlatforms = true; + + bool climbFast = !character.Params.ForceSlowClimbing && character.AnimController.IsMovingFast; + var animParams = climbFast ? RunParams : WalkParams; + // Don't slide if we can climb faster than slide. + bool slide = animParams.SlideSpeed > animParams.ClimbSpeed && targetMovement.Y < -0.1f && climbFast; + float maxClimbingSpeed = climbFast && !character.Params.ForceSlowClimbing ? RunParams.ClimbSpeed : WalkParams.ClimbSpeed; + Vector2 tempTargetMovement = TargetMovement; + tempTargetMovement.Y = Math.Clamp(tempTargetMovement.Y, slide ? -animParams.SlideSpeed : -maxClimbingSpeed, maxClimbingSpeed); + + movement = MathUtils.SmoothStep(movement, tempTargetMovement, 0.3f); + + Limb leftFoot = GetClimbingLimb(LimbType.LeftFoot); + Limb rightFoot = GetClimbingLimb(LimbType.RightFoot); + Limb head = GetClimbingLimb(LimbType.Head); + Limb torso = GetClimbingLimb(LimbType.Torso); + + Limb leftHand = GetClimbingLimb(LimbType.LeftHand); + Limb rightHand = GetClimbingLimb(LimbType.RightHand); + + Vector2 ladderSimPos = ConvertUnits.ToSimUnits( + ladder.Item.Rect.X + ladder.Item.Rect.Width / 2.0f, + ladder.Item.Rect.Y); + + Vector2 ladderSimSize = ConvertUnits.ToSimUnits(ladder.Item.Rect.Size.ToVector2()); + + var lowestNearbyLadder = GetLowestNearbyLadder(ladder); + if (lowestNearbyLadder != null && lowestNearbyLadder != ladder) + { + ladderSimSize.Y = ConvertUnits.ToSimUnits(ladder.Item.WorldRect.Y - (lowestNearbyLadder.Item.WorldRect.Y - lowestNearbyLadder.Item.Rect.Size.Y)); + } + + float stepHeight = ConvertUnits.ToSimUnits(animParams.ClimbStepHeight); + + if (currentHull == null && ladder.Item.Submarine != null) + { + ladderSimPos += ladder.Item.Submarine.SimPosition; + } + else if (currentHull?.Submarine != null && currentHull.Submarine != ladder.Item.Submarine && ladder.Item.Submarine != null) + { + ladderSimPos += ladder.Item.Submarine.SimPosition - currentHull.Submarine.SimPosition; + } + else if (currentHull?.Submarine != null && ladder.Item.Submarine == null) + { + ladderSimPos -= currentHull.Submarine.SimPosition; + } + + float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.Radius - Collider.Height / 2.0f; + float torsoPos = TorsoPosition ?? 0; + float bodyMoveForce = animParams.ClimbBodyMoveForce; + if (torso != null) + { + MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), bodyMoveForce); + } + if (head != null) + { + float headPos = HeadPosition ?? 0; + MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), bodyMoveForce); + } + + Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), bodyMoveForce); + + Vector2 handPos = new Vector2( + ladderSimPos.X, + bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y); + if (climbFast) { handPos.Y -= stepHeight; } + + float handMoveForce = animParams.ClimbHandMoveForce; + + //prevent the hands from going above the top of the ladders + handPos.Y = Math.Min(-0.5f, handPos.Y); + if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f) + { + if (rightHand != null) + { + MoveLimb(rightHand, + new Vector2(slide ? handPos.X + ladderSimSize.X * 0.75f : handPos.X, + (slide ? handPos.Y + stepHeight : MathUtils.Round(handPos.Y, stepHeight * 2.0f)) + ladderSimPos.Y), + handMoveForce); + rightHand.body.ApplyTorque(Dir * 2.0f); + } + } + if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f) + { + if (leftHand != null) + { + MoveLimb(leftHand, + new Vector2(handPos.X - ladderSimSize.X * (slide ? 1.0f : 0.5f), + (slide ? handPos.Y + stepHeight : MathUtils.Round(handPos.Y - stepHeight, stepHeight * 2.0f) + stepHeight) + ladderSimPos.Y), + handMoveForce); ; + leftHand.body.ApplyTorque(Dir * 2.0f); + } + } + + float stepHeightAdjustment = stepHeight * 2.7f; + Vector2 footPos = new Vector2( + handPos.X - Dir * 0.05f, + bottomPos + ColliderHeightFromFloor - stepHeightAdjustment - ladderSimPos.Y); + if (climbFast) { footPos.Y += stepHeight; } + + //apply torque to the legs to make the knees bend + Limb leftLeg = GetClimbingLimb(LimbType.LeftLeg); + Limb rightLeg = GetClimbingLimb(LimbType.RightLeg); + + //only move the feet if they're above the bottom of the ladders + //(if not, they'll just dangle in air, and the character holds itself up with its arms) + if (footPos.Y > -ladderSimSize.Y - 0.2f && leftFoot != null && rightFoot != null && leftLeg != null && rightLeg != null) + { + Limb refLimb = GetClimbingLimb(LimbType.Waist) ?? GetClimbingLimb(LimbType.Torso) ?? MainLimb; + bool leftLegBackwards = Math.Abs(leftLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi; + bool rightLegBackwards = Math.Abs(rightLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi; + float footMoveForce = animParams.ClimbFootMoveForce; + if (slide) + { + if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X - ladderSimSize.X * 0.5f, footPos.Y + ladderSimPos.Y), footMoveForce, pullFromCenter: true); } + if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), footMoveForce, pullFromCenter: true); } + } + else + { + float leftFootPos = MathUtils.Round(footPos.Y + stepHeight, stepHeight * 2.0f) - stepHeight; + float prevLeftFootPos = MathUtils.Round(prevFootPos + stepHeight, stepHeight * 2.0f) - stepHeight; + if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), footMoveForce, pullFromCenter: true); } + + float rightFootPos = MathUtils.Round(footPos.Y, stepHeight * 2.0f); + float prevRightFootPos = MathUtils.Round(prevFootPos, stepHeight * 2.0f); + if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), footMoveForce, pullFromCenter: true); } +#if CLIENT + if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval) + { + SoundPlayer.PlaySound("footstep_armor_heavy", leftFoot.WorldPosition, hullGuess: currentHull); + leftFoot.LastImpactSoundTime = (float)Timing.TotalTime; + } + if (Math.Abs(rightFootPos - prevRightFootPos) > stepHeight && rightFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval) + { + SoundPlayer.PlaySound("footstep_armor_heavy", rightFoot.WorldPosition, hullGuess: currentHull); + rightFoot.LastImpactSoundTime = (float)Timing.TotalTime; + } +#endif + prevFootPos = footPos.Y; + } + + if (!leftLegBackwards) { leftLeg.body.ApplyTorque(Dir * -8.0f); } // TODO: expose? + if (!rightLegBackwards) { rightLeg.body.ApplyTorque(Dir * -8.0f); } + } + + float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI; + movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor)); + + Vector2 subSpeed = currentHull != null || ladder.Item.Submarine == null + ? Vector2.Zero : ladder.Item.Submarine.Velocity; + + //reached the top of the ladders -> can't go further up + Vector2 climbForce = new Vector2(0.0f, movement.Y) * movementFactor; + + if (!InWater) { climbForce.Y += 0.3f * movementFactor; } + + if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); } + //reached the bottom -> can't go further down + float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.Height; + if (floorFixture != null && + !floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) && + !floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) && + character.SimPosition.Y < standOnFloorY + minHeightFromFloor) + { + climbForce.Y = MathHelper.Clamp((standOnFloorY + minHeightFromFloor - character.SimPosition.Y) * 5.0f, climbForce.Y, 1.0f); + } + + //apply forces to the collider to move the Character up/down + Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass); + // Don't rotate the head on non-humanoids, because it can cause issues with some ragdolls. + // E.g. the head might not actually be head, or it's not where we expect it to be. + if (head != null && character.IsHumanoid) + { + if (Aiming) + { + RotateHead(head); + } + else if (Anim == Animation.UsingItemWhileClimbing && character.SelectedItem is { } selectedItem) + { + Vector2 diff = (selectedItem.WorldPosition - head.WorldPosition) * Dir; + float targetRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver4 * Dir); + head.body.SmoothRotate(targetRotation, force: animParams.HeadTorque); + } + else + { + float movementMultiplier = targetMovement.Y < 0 ? 0 : 1; + head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, force: animParams.HeadTorque); + } + } + + if (ladder.Item.Prefab.Triggers.None()) + { + character.ReleaseSecondaryItem(); + return; + } + + Rectangle trigger = ladder.Item.Prefab.Triggers.FirstOrDefault(); + trigger = ladder.Item.TransformTrigger(trigger); + + bool isRemote = false; + bool isClimbing = true; + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + isRemote = character.IsRemotelyControlled; + } + if (isRemote) + { + if (Math.Abs(targetMovement.X) > 0.05f || + (TargetMovement.Y < 0.0f && ConvertUnits.ToSimUnits(trigger.Height) + handPos.Y < HeadPosition) || + (TargetMovement.Y > 0.0f && handPos.Y > 0.1f)) + { + isClimbing = false; + } + } + else if ((character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right)) && + (!character.IsKeyDown(InputType.Up) && !character.IsKeyDown(InputType.Down))) + { + isClimbing = false; + } + + if (!isClimbing) + { + character.StopClimbing(); + IgnorePlatforms = false; + } + + Ladder GetLowestNearbyLadder(Ladder currentLadder, float threshold = 16.0f) + { + foreach (Ladder ladder in Ladder.List) + { + if (ladder == currentLadder || !ladder.Item.IsInteractable(character)) { continue; } + if (Math.Abs(ladder.Item.WorldPosition.X - currentLadder.Item.WorldPosition.X) > threshold) { continue; } + if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y) { continue; } + if ((currentLadder.Item.WorldRect.Y - currentLadder.Item.Rect.Height) - ladder.Item.WorldRect.Y > threshold) { continue; } + return ladder; + } + return null; + } + + Limb GetClimbingLimb(LimbType limbType) + { + if (HasMultipleLimbsOfSameType) + { + // First try to find a match using the secondary type, if that fails, use the primary type and exclude all the limbs with the secondary type. + // Secondary limbs are first excluded and then targeted, because some feet are meant to be used as hands in this context, which means we don't want to get them when seeking the feet. + return GetLimb(limbType, useSecondaryType: true) ?? GetLimb(limbType, excludeLimbsWithSecondaryType: true); + } + else + { + return GetLimb(limbType); + } + } + } + + protected void RotateHead(Limb head) + { + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 dir = (mousePos - head.SimPosition) * Dir; + float rot = MathUtils.VectorToAngle(dir); + var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso); + if (neckJoint != null) + { + float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation); + float lowerLimit = neckJoint.LowerLimit + offset; + float upperLimit = neckJoint.UpperLimit + offset; + float min = Math.Min(lowerLimit, upperLimit); + float max = Math.Max(lowerLimit, upperLimit); + rot = Math.Clamp(rot, min, max); + } + head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque); + } public void ApplyPose(Vector2 leftHandPos, Vector2 rightHandPos, Vector2 leftFootPos, Vector2 rightFootPos, float footMoveForce = 10) { @@ -818,6 +1109,13 @@ namespace Barotrauma CalculateArmLengths(); } } + + public void RecreateAndRespawn(RagdollParams ragdollParams = null) + { + Vector2 pos = character.WorldPosition; + Recreate(ragdollParams); + character.TeleportTo(pos); + } private void StartAnimation(Animation animation) { @@ -906,7 +1204,7 @@ namespace Barotrauma return true; } - private void UpdateTemporaryAnimations(float deltaTime) + private void UpdateTemporaryAnimations() { if (tempAnimations.None()) { return; } foreach ((AnimationType animationType, AnimSwap animSwap) in tempAnimations) @@ -932,7 +1230,8 @@ namespace Barotrauma expiredAnimations.Clear(); foreach (AnimSwap animSwap in tempAnimations.Values) { - animSwap.Update(deltaTime); + // Will be removed on the next frame, unless something keeps it alive. + animSwap.IsActive = false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index fa20dd94c..5a6cb8e32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -139,9 +139,11 @@ namespace Barotrauma ResetState(); return; } + UpdateConstantTorque(deltaTime); + UpdateBlink(deltaTime); var mainLimb = MainLimb; - levitatingCollider = !IsHangingWithRope; + levitatingCollider = !IsHangingWithRope && !IsClimbing; if (!character.CanMove) { @@ -208,6 +210,11 @@ namespace Barotrauma { TargetMovement = TargetMovement.ClampLength(2); } + + if (IsClimbing) + { + UpdateClimbing(); + } if (inWater && !forceStanding) { @@ -336,7 +343,6 @@ namespace Barotrauma if (target == null) { return; } Limb mouthLimb = GetLimb(LimbType.Head); if (mouthLimb == null) { return; } - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { //stop dragging if there's something between the pull limb and the target @@ -357,23 +363,23 @@ namespace Barotrauma return; } } - - float dmg = character.Params.EatingSpeed; - float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10); - eatTimer += deltaTime * eatSpeed; - - Vector2 mouthPos = SimplePhysicsEnabled ? character.SimPosition : GetMouthPosition().Value; - Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition; - - Vector2 limbDiff = attackSimPosition - mouthPos; - float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1); - bool tooFar = character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent; - if (tooFar) - { - character.SelectedCharacter = null; - } - else + if (Character.CanEat) { + Vector2 mouthPos = SimplePhysicsEnabled ? character.SimPosition : GetMouthPosition() ?? Vector2.Zero; + Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition; + Vector2 limbDiff = attackSimPosition - mouthPos; + float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1); + bool tooFar = character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent; + if (tooFar) + { + character.DeselectCharacter(); + return; + } + + float dmg = character.Params.EatingSpeed; + float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10); + eatTimer += deltaTime * eatSpeed; + //pull the target character to the position of the mouth //(+ make the force fluctuate to waggle the character a bit) float dragForce = MathHelper.Clamp(eatSpeed * 10, 0, 40); @@ -405,20 +411,19 @@ namespace Barotrauma else { float force = (float)Math.Sin(eatTimer * 100) * mouthLimb.Mass; - mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * 2, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - mouthLimb.body.ApplyTorque(-force * 50); + mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * mouthLimb.Params.EatImpulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + mouthLimb.body.ApplyTorque(-force * mouthLimb.Params.EatTorque); } - - if (Character.CanEat && target.IsDead) + + var jaw = GetLimb(LimbType.Jaw); + if (jaw != null) + { + jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25); + } + character.ApplyStatusEffects(ActionType.OnEating, deltaTime); + + if (target.IsDead) { - var jaw = GetLimb(LimbType.Jaw); - if (jaw != null) - { - jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25); - } - - character.ApplyStatusEffects(ActionType.OnEating, deltaTime); - float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f); if (Rand.Value() < particleFrequency / 6) { @@ -430,7 +435,7 @@ namespace Barotrauma } if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f) { - static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered; + static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA is { IsSevered: false } && j.LimbB is { IsSevered: false }; //keep severing joints until there is only one limb left var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered); if (nonSeveredJoints.None()) @@ -440,16 +445,13 @@ namespace Barotrauma { target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null)); } - //only one limb left, the character is now full eaten Entity.Spawner?.AddEntityToRemoveQueue(target); - if (Character.AIController is EnemyAIController enemyAi) { enemyAi.PetBehavior?.OnEat(target); } - - character.SelectedCharacter = null; + character.DeselectCharacter(); } else //sever a random joint { @@ -460,7 +462,7 @@ namespace Barotrauma } } - public bool reverse; + public bool Reverse; void UpdateSineAnim(float deltaTime) { @@ -510,7 +512,7 @@ namespace Barotrauma } else { - Vector2 transformedMovement = reverse ? -movement : movement; + Vector2 transformedMovement = Reverse ? -movement : movement; float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2; float mainLimbAngle = 0; if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue) @@ -555,7 +557,6 @@ namespace Barotrauma foreach (var limb in Limbs) { if (limb.IsSevered) { continue; } - if (limb.type != LimbType.Tail) { continue; } if (!limb.Params.ApplyTailAngle) { continue; } RotateTail(limb); isAngleApplied = true; @@ -592,7 +593,7 @@ namespace Barotrauma else { movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2; - if (reverse) + if (Reverse) { movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi); } @@ -651,29 +652,21 @@ namespace Barotrauma foreach (var limb in Limbs) { if (limb.IsSevered) { continue; } - switch (limb.type) + if (limb.type is LimbType.LeftFoot or LimbType.RightFoot) { - case LimbType.LeftFoot: - case LimbType.RightFoot: - if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID)) - { - SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque); - } - break; - case LimbType.Tail: - if (waveLength > 0 && waveAmplitude > 0) - { - float waveRotation = (float)Math.Sin(WalkPos * limb.Params.SineFrequencyMultiplier); - limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude * limb.Params.SineAmplitudeMultiplier); - } - break; + if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID)) + { + SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque); + } + } + if (limb.type == LimbType.Tail || limb.Params.ApplySineMovement) + { + if (waveLength > 0 && waveAmplitude > 0) + { + float waveRotation = (float)Math.Sin(WalkPos * limb.Params.SineFrequencyMultiplier); + limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude * limb.Params.SineAmplitudeMultiplier); + } } - } - - for (int i = 0; i < Limbs.Length; i++) - { - var limb = Limbs[i]; - if (limb.IsSevered) { continue; } if (limb.SteerForce <= 0.0f) { continue; } if (!Collider.PhysEnabled) { continue; } Vector2 pullPos = limb.PullJointWorldAnchorA; @@ -698,20 +691,6 @@ namespace Barotrauma } } - foreach (var limb in Limbs) - { - if (limb.IsSevered) { continue; } - if (Math.Abs(limb.Params.ConstantTorque) > 0) - { - float movementFactor = Math.Max(character.AnimController.Collider.LinearVelocity.Length() * 0.5f, 1); - limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque * movementFactor, wrapAngle: true); - } - if (limb.Params.BlinkFrequency > 0) - { - limb.UpdateBlink(deltaTime, MainLimb.Rotation); - } - } - floorY = Limbs[0].SimPosition.Y; } @@ -744,9 +723,9 @@ namespace Barotrauma } float offset = MathHelper.Pi * CurrentGroundedParams.StepLiftOffset; - if (CurrentGroundedParams.MultiplyByDir) + if (character.AnimController.Dir < 0) { - offset *= Dir; + offset += MathHelper.Pi * CurrentGroundedParams.StepLiftFrequency; } float stepLift = TargetMovement.X == 0.0f ? 0 : (float)Math.Sin(WalkPos * Dir * CurrentGroundedParams.StepLiftFrequency + offset) * (CurrentGroundedParams.StepLiftAmount / 100); @@ -847,14 +826,6 @@ namespace Barotrauma foreach (Limb limb in Limbs) { if (limb.IsSevered) { continue; } - if (Math.Abs(limb.Params.ConstantTorque) > 0) - { - limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true); - } - if (limb.Params.BlinkFrequency > 0 && !limb.Params.OnlyBlinkInWater) - { - limb.UpdateBlink(deltaTime, MainLimb.Rotation); - } switch (limb.type) { case LimbType.LeftFoot: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 14c49042d..0d1699f2d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -13,10 +13,8 @@ namespace Barotrauma private const float SteepestWalkableSlopeAngleDegrees = 55f; private const float SlowlyWalkableSlopeAngleDegrees = 30f; - private static readonly float SteepestWalkableSlopeNormalX = - MathF.Sin(MathHelper.ToRadians(SteepestWalkableSlopeAngleDegrees)); - private static readonly float SlowlyWalkableSlopeNormalX = - MathF.Sin(MathHelper.ToRadians(SlowlyWalkableSlopeAngleDegrees)); + private static readonly float SteepestWalkableSlopeNormalX = MathF.Sin(MathHelper.ToRadians(SteepestWalkableSlopeAngleDegrees)); + private static readonly float SlowlyWalkableSlopeNormalX = MathF.Sin(MathHelper.ToRadians(SlowlyWalkableSlopeAngleDegrees)); private const float MaxSpeedOnStairs = 1.7f; private const float SteepSlopePushMagnitude = MaxSpeedOnStairs; @@ -254,7 +252,8 @@ namespace Barotrauma { if (Frozen) { return; } if (MainLimb == null) { return; } - + UpdateConstantTorque(deltaTime); + UpdateBlink(deltaTime); levitatingCollider = !IsHangingWithRope; if (onGround && character.CanMove) { @@ -652,9 +651,14 @@ namespace Barotrauma { movement = Vector2.Zero; } - + + float offset = MathHelper.Pi * currentGroundedParams.StepLiftOffset; + if (character.AnimController.Dir < 0) + { + offset += MathHelper.Pi * currentGroundedParams.StepLiftFrequency; + } float stepLift = TargetMovement.X == 0.0f ? 0 : - (float)Math.Sin(WalkPos * currentGroundedParams.StepLiftFrequency + MathHelper.Pi * currentGroundedParams.StepLiftOffset) * (currentGroundedParams.StepLiftAmount / 100); + (float)Math.Sin(WalkPos * Dir * currentGroundedParams.StepLiftFrequency + offset) * (currentGroundedParams.StepLiftAmount / 100); float y = colliderPos.Y + stepLift; @@ -987,7 +991,7 @@ namespace Barotrauma { head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque); } - else + else if (character.FollowCursor) { RotateHead(head); } @@ -1145,245 +1149,6 @@ namespace Barotrauma } } - private float prevFootPos; - - void UpdateClimbing() - { - var ladder = character.SelectedSecondaryItem?.GetComponent(); - if (character.IsIncapacitated) - { - Anim = Animation.None; - return; - } - else if (ladder == null) - { - StopClimbing(); - return; - } - - onGround = false; - IgnorePlatforms = true; - - bool climbFast = targetMovement.Y > 3.0f; - bool slide = targetMovement.Y < -1.1f; - Vector2 tempTargetMovement = TargetMovement; - tempTargetMovement.Y = climbFast ? - Math.Min(tempTargetMovement.Y, 2.0f) : - Math.Min(tempTargetMovement.Y, 1.0f); - - movement = MathUtils.SmoothStep(movement, tempTargetMovement, 0.3f); - - Limb leftFoot = GetLimb(LimbType.LeftFoot); - Limb rightFoot = GetLimb(LimbType.RightFoot); - Limb head = GetLimb(LimbType.Head); - Limb torso = GetLimb(LimbType.Torso); - - Limb leftHand = GetLimb(LimbType.LeftHand); - Limb rightHand = GetLimb(LimbType.RightHand); - - if (leftHand == null || rightHand == null || head == null || torso == null) { return; } - - Vector2 ladderSimPos = ConvertUnits.ToSimUnits( - ladder.Item.Rect.X + ladder.Item.Rect.Width / 2.0f, - ladder.Item.Rect.Y); - - Vector2 ladderSimSize = ConvertUnits.ToSimUnits(ladder.Item.Rect.Size.ToVector2()); - - float lowestLadderSimPos = ladderSimPos.Y - ladderSimPos.Y; - var lowestNearbyLadder = GetLowestNearbyLadder(ladder); - if (lowestNearbyLadder != null && lowestNearbyLadder != ladder) - { - ladderSimSize.Y = ConvertUnits.ToSimUnits(ladder.Item.WorldRect.Y - (lowestNearbyLadder.Item.WorldRect.Y - lowestNearbyLadder.Item.Rect.Size.Y)); - } - - float stepHeight = ConvertUnits.ToSimUnits(30.0f); - if (climbFast) { stepHeight *= 2; } - - if (currentHull == null && ladder.Item.Submarine != null) - { - ladderSimPos += ladder.Item.Submarine.SimPosition; - } - else if (currentHull?.Submarine != null && currentHull.Submarine != ladder.Item.Submarine && ladder.Item.Submarine != null) - { - ladderSimPos += ladder.Item.Submarine.SimPosition - currentHull.Submarine.SimPosition; - } - else if (currentHull?.Submarine != null && ladder.Item.Submarine == null) - { - ladderSimPos -= currentHull.Submarine.SimPosition; - } - - float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.Radius - Collider.Height / 2.0f; - float torsoPos = TorsoPosition ?? 0; - MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), 10.5f); - float headPos = HeadPosition ?? 0; - MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), 10.5f); - - Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), 10.5f); - - Vector2 handPos = new Vector2( - ladderSimPos.X, - bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y); - if (climbFast) { handPos.Y -= stepHeight; } - - //prevent the hands from going above the top of the ladders - handPos.Y = Math.Min(-0.5f, handPos.Y); - if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f) - { - MoveLimb(rightHand, - new Vector2(slide ? handPos.X + ladderSimSize.X * 0.5f : handPos.X, - (slide ? handPos.Y : MathUtils.Round(handPos.Y, stepHeight * 2.0f)) + ladderSimPos.Y), - 5.2f); - rightHand.body.ApplyTorque(Dir * 2.0f); - } - if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f) - { - MoveLimb(leftHand, - new Vector2(handPos.X - ladderSimSize.X * 0.5f, - (slide ? handPos.Y : MathUtils.Round(handPos.Y - stepHeight, stepHeight * 2.0f) + stepHeight) + ladderSimPos.Y), - 5.2f); ; - leftHand.body.ApplyTorque(Dir * 2.0f); - } - - Vector2 footPos = new Vector2( - handPos.X - Dir * 0.05f, - bottomPos + ColliderHeightFromFloor - stepHeight * 2.7f - ladderSimPos.Y); - if (climbFast) { footPos.Y += stepHeight; } - - //apply torque to the legs to make the knees bend - Limb leftLeg = GetLimb(LimbType.LeftLeg); - Limb rightLeg = GetLimb(LimbType.RightLeg); - - //only move the feet if they're above the bottom of the ladders - //(if not, they'll just dangle in air, and the character holds itself up with it's arms) - if (footPos.Y > -ladderSimSize.Y - 0.2f && leftFoot != null && rightFoot != null) - { - Limb refLimb = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); - bool leftLegBackwards = Math.Abs(leftLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi; - bool rightLegBackwards = Math.Abs(rightLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi; - - if (slide) - { - if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X - ladderSimSize.X * 0.5f, footPos.Y + ladderSimPos.Y), 15.5f, true); } - if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true); } - } - else - { - float leftFootPos = MathUtils.Round(footPos.Y + stepHeight, stepHeight * 2.0f) - stepHeight; - float prevLeftFootPos = MathUtils.Round(prevFootPos + stepHeight, stepHeight * 2.0f) - stepHeight; - if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), 15.5f, true); } - - float rightFootPos = MathUtils.Round(footPos.Y, stepHeight * 2.0f); - float prevRightFootPos = MathUtils.Round(prevFootPos, stepHeight * 2.0f); - if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), 15.5f, true); } -#if CLIENT - if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval) - { - SoundPlayer.PlaySound("footstep_armor_heavy", leftFoot.WorldPosition, hullGuess: currentHull); - leftFoot.LastImpactSoundTime = (float)Timing.TotalTime; - } - if (Math.Abs(rightFootPos - prevRightFootPos) > stepHeight && rightFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval) - { - SoundPlayer.PlaySound("footstep_armor_heavy", rightFoot.WorldPosition, hullGuess: currentHull); - rightFoot.LastImpactSoundTime = (float)Timing.TotalTime; - } -#endif - prevFootPos = footPos.Y; - } - - if (!leftLegBackwards) { leftLeg.body.ApplyTorque(Dir * -8.0f); } - if (!rightLegBackwards) { rightLeg.body.ApplyTorque(Dir * -8.0f); } - } - - float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI; - movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor)); - - Vector2 subSpeed = currentHull != null || ladder.Item.Submarine == null - ? Vector2.Zero : ladder.Item.Submarine.Velocity; - - //reached the top of the ladders -> can't go further up - Vector2 climbForce = new Vector2(0.0f, movement.Y) * movementFactor; - - if (!InWater) { climbForce.Y += 0.3f * movementFactor; } - - if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); } - //reached the bottom -> can't go further down - float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.Height; - if (floorFixture != null && - !floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) && - !floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) && - character.SimPosition.Y < standOnFloorY + minHeightFromFloor) - { - climbForce.Y = MathHelper.Clamp((standOnFloorY + minHeightFromFloor - character.SimPosition.Y) * 5.0f, climbForce.Y, 1.0f); - } - - //apply forces to the collider to move the Character up/down - Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass); - if (Aiming) - { - RotateHead(head); - } - else if (Anim == Animation.UsingItemWhileClimbing && character.SelectedItem is { } selectedItem) - { - Vector2 diff = (selectedItem.WorldPosition - head.WorldPosition) * Dir; - float targetRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver4 * Dir); - head.body.SmoothRotate(targetRotation, force: WalkParams.HeadTorque); - } - else - { - float movementMultiplier = targetMovement.Y < 0 ? 0 : 1; - head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, force: WalkParams.HeadTorque); - } - - if (ladder.Item.Prefab.Triggers.None()) - { - character.ReleaseSecondaryItem(); - return; - } - - Rectangle trigger = ladder.Item.Prefab.Triggers.FirstOrDefault(); - trigger = ladder.Item.TransformTrigger(trigger); - - bool isRemote = false; - bool isClimbing = true; - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) - { - isRemote = character.IsRemotelyControlled; - } - if (isRemote) - { - if (Math.Abs(targetMovement.X) > 0.05f || - (TargetMovement.Y < 0.0f && ConvertUnits.ToSimUnits(trigger.Height) + handPos.Y < HeadPosition) || - (TargetMovement.Y > 0.0f && handPos.Y > 0.1f)) - { - isClimbing = false; - } - } - else if ((character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right)) && - (!character.IsKeyDown(InputType.Up) && !character.IsKeyDown(InputType.Down))) - { - isClimbing = false; - } - - if (!isClimbing) - { - character.StopClimbing(); - IgnorePlatforms = false; - } - - Ladder GetLowestNearbyLadder(Ladder currentLadder, float threshold = 16.0f) - { - foreach (Ladder ladder in Ladder.List) - { - if (ladder == currentLadder || !ladder.Item.IsInteractable(character)) { continue; } - if (Math.Abs(ladder.Item.WorldPosition.X - currentLadder.Item.WorldPosition.X) > threshold) { continue; } - if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y) { continue; } - if ((currentLadder.Item.WorldRect.Y - currentLadder.Item.Rect.Height) - ladder.Item.WorldRect.Y > threshold) { continue; } - return ladder; - } - return null; - } - } - void UpdateFallingProne(float strength, bool moveHands = true, bool moveTorso = true, bool moveLegs = true) { if (strength <= 0.0f) { return; } @@ -1518,7 +1283,7 @@ namespace Barotrauma float cprBoost = character.GetStatValue(StatTypes.CPRBoost); - int skill = (int)character.GetSkillLevel("medical"); + int skill = (int)character.GetSkillLevel(Tags.MedicalSkill); if (GameMain.NetworkMember is not { IsClient: true }) { @@ -1595,7 +1360,7 @@ namespace Barotrauma //otherwise it's easy to abuse the system by repeatedly reviving in a low-oxygen room if (!target.IsDead) { - target.CharacterHealth.CalculateVitality(); + target.CharacterHealth.RecalculateVitality(); if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f) { character.Info?.ApplySkillGain(Tags.MedicalSkill, SkillSettings.Current.SkillIncreasePerCprRevive); @@ -1876,23 +1641,11 @@ namespace Barotrauma } } } - - private void RotateHead(Limb head) + + public void Crouch() { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); - Vector2 dir = (mousePos - head.SimPosition) * Dir; - float rot = MathUtils.VectorToAngle(dir); - var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso); - if (neckJoint != null) - { - float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation); - float lowerLimit = neckJoint.LowerLimit + offset; - float upperLimit = neckJoint.UpperLimit + offset; - float min = Math.Min(lowerLimit, upperLimit); - float max = Math.Max(lowerLimit, upperLimit); - rot = Math.Clamp(rot, min, max); - } - head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque); + Crouching = true; + character.SetInput(InputType.Crouch, hit: false, held: true); } private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 9ce0e88a9..9a21be2bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -91,7 +91,7 @@ namespace Barotrauma private bool simplePhysicsEnabled; public Character Character => character; - protected Character character; + protected readonly Character character; protected float strongestImpact; @@ -385,15 +385,16 @@ namespace Barotrauma if (ragdollParams != null) { RagdollParams = ragdollParams; - if (!character.VariantOf.IsEmpty) - { - RagdollParams.TryApplyVariantScale(character.Params.VariantFile); - } } else { + // Only re-equip items if the ragdoll doesn't change, because re-equiping items might throw exceptions if the limbs have changed. items = limbs?.ToDictionary(l => l.Params, l => l.WearingItems); } + if (character.Params.VariantFile is XDocument variantFile) + { + RagdollParams.TryApplyVariantScale(variantFile); + } foreach (var limbParams in RagdollParams.Limbs) { if (!PhysicsBody.IsValidShape(limbParams.Radius, limbParams.Height, limbParams.Width)) @@ -430,18 +431,13 @@ namespace Barotrauma if (character.IsHusk && character.Params.UseHuskAppendage) { - bool inEditor = false; -#if CLIENT - inEditor = Screen.Selected == GameMain.CharacterEditorScreen; -#endif - var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath); if (characterPrefab?.ConfigElement != null) { var mainElement = characterPrefab.ConfigElement; foreach (var huskAppendage in mainElement.GetChildElements("huskappendage")) { - if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; } + if (huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; } Identifier afflictionIdentifier = huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty); if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab affliction) || @@ -452,7 +448,7 @@ namespace Barotrauma } else { - AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskAppendage, ragdoll: this); + AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskedSpeciesName: character.SpeciesName, huskAppendage, ragdoll: this); } } } @@ -478,7 +474,7 @@ namespace Barotrauma DebugConsole.ThrowError("Invalid collider dimensions: " + cParams.Name); break; ; } - var body = new PhysicsBody(cParams); + var body = new PhysicsBody(cParams, findNewContacts: false); collider.Add(body); body.UserData = character; body.FarseerBody.OnCollision += OnLimbCollision; @@ -520,7 +516,7 @@ namespace Barotrauma { if (joint == null) { continue; } float angle = (joint.LowerLimit + joint.UpperLimit) / 2.0f; - joint.LimbB?.body?.SetTransform( + joint.LimbB?.body?.SetTransformIgnoreContacts( (joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, joint.BodyA.Rotation + angle, true)), joint.BodyA.Rotation + angle); } @@ -529,10 +525,11 @@ namespace Barotrauma protected void CreateLimbs() { limbs?.ForEach(l => l.Remove()); + Mass = 0; DebugConsole.Log($"Creating limbs from {RagdollParams.Name}."); limbDictionary = new Dictionary(); limbs = new Limb[RagdollParams.Limbs.Count]; - RagdollParams.Limbs.ForEach(l => AddLimb(l)); + RagdollParams.Limbs.ForEach(AddLimb); if (limbs.Contains(null)) { return; } SetupDrawOrder(); } @@ -549,11 +546,11 @@ namespace Barotrauma /// /// Resets the serializable data to the currently selected ragdoll params. - /// Force reloading always loads the xml stored on the disk. + /// Always loads the xml stored on the disk. /// - public void ResetRagdoll(bool forceReload = false) + public void ResetRagdoll() { - RagdollParams.Reset(forceReload); + RagdollParams.Reset(forceReload: true); ResetJoints(); ResetLimbs(); } @@ -577,7 +574,7 @@ namespace Barotrauma public void AddJoint(JointParams jointParams) { - if (!checkLimbIndex(jointParams.Limb2, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2")) + if (!checkLimbIndex(jointParams.Limb1, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2")) { return; } @@ -683,8 +680,7 @@ namespace Barotrauma } LimbJoints = newJoints; } - - SubtractMass(limb); + limb.Remove(); foreach (LimbJoint limbJoint in attachedJoints) { @@ -1400,7 +1396,12 @@ namespace Barotrauma limb.Update(deltaTime); } - if (!inWater && character.AllowInput && levitatingCollider) + bool isAttachedToController = + character.SelectedItem?.GetComponent() is { } controller && + controller.User == character && + controller.IsAttachedUser(controller.User); + + if (!inWater && character.AllowInput && levitatingCollider && !isAttachedToController) { if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance) { @@ -1910,7 +1911,7 @@ namespace Barotrauma } else { - Collider.SetTransform(simPosition, Collider.Rotation); + Collider.SetTransformIgnoreContacts(simPosition, Collider.Rotation); } if (!MathUtils.NearlyEqual(limbMoveAmount, Vector2.Zero)) @@ -2009,7 +2010,7 @@ namespace Barotrauma } else { - limb.body.SetTransform(movePos, rotation); + limb.body.SetTransformIgnoreContacts(movePos, rotation); limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA; limb.PullJointEnabled = false; } @@ -2113,26 +2114,48 @@ namespace Barotrauma /// /// Note that if there are multiple limbs of the same type, only the first (valid) limb is returned. /// - public Limb GetLimb(LimbType limbType, bool excludeSevered = true) + /// + /// Should we filter out severed limbs? + /// Should we target limbs with secondary type instead of (primary) type? + /// Should we filter out all limbs with a secondary type something else than "None"? + /// + public Limb GetLimb(LimbType limbType, bool excludeSevered = true, bool excludeLimbsWithSecondaryType = false, bool useSecondaryType = false) { - if (limbDictionary.TryGetValue(limbType, out Limb limb)) + Limb limb = null; + if (!HasMultipleLimbsOfSameType && !useSecondaryType && !excludeLimbsWithSecondaryType) { - if (excludeSevered && limb.IsSevered) + // Faster method, but doesn't work when there's multiple limbs of the same type or if we want to seek/exclude limbs with different conditions. + if (limbDictionary.TryGetValue(limbType, out limb)) { - limb = null; - } + if (limb.Removed) + { + limb = null; + } + if (excludeSevered && limb is { IsSevered: true } ) + { + limb = null; + } + } } - if (limb == null && HasMultipleLimbsOfSameType) + if (limb == null) { - // Didn't find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs. + // Didn't seek or find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs. foreach (var l in limbs) { - if (l.type != limbType) { continue; } - if (!excludeSevered || !l.IsSevered) + if (l.Removed) { continue; } + if (useSecondaryType) { - limb = l; - break; + if (l.Params.SecondaryType != limbType) { continue; } } + else if (l.type != limbType) + { + continue; + } + if (excludeSevered && l.IsSevered) { continue; } + if (excludeLimbsWithSecondaryType && l.Params.SecondaryType != LimbType.None) { continue; } + // Found a valid and match + limb = l; + break; } } return limb; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 92f4d9897..5bbb47768 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -24,11 +25,22 @@ namespace Barotrauma NotDefined } + [Flags] public enum AttackTarget { - Any, - Character, - Structure // Including hulls etc. Evaluated as anything but a character. + Any = 0, + /// + /// Characters only + /// + Character = 1, + /// + /// Structures and hulls, but also items (for backwards support)! + /// + Structure = 2, + /// + /// Items only + /// + Item = 4 } public enum AIBehaviorAfterAttack @@ -816,15 +828,28 @@ namespace Barotrauma return true; } - public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType; + public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType.HasAnyFlag(targetType); public bool IsValidTarget(Entity target) { return TargetType switch { AttackTarget.Character => target is Character, - AttackTarget.Structure => !(target is Character), - _ => true, + AttackTarget.Structure => target is Structure or Hull or Item, // Items are intentionally included for backwards-support. + AttackTarget.Item => target is Item, + _ => IsValidTarget(GetAttackTargetTypeFromEntity(target)) + }; + } + + private static AttackTarget GetAttackTargetTypeFromEntity(Entity entity) + { + return entity switch + { + Character => AttackTarget.Character, + Item => AttackTarget.Item, + Structure => AttackTarget.Structure, + Hull => AttackTarget.Structure, + _ => AttackTarget.Any }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 6e2a69bc8..8e75cc189 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -213,7 +213,6 @@ namespace Barotrauma } } - private CharacterTeamType? originalTeamID; public CharacterTeamType OriginalTeamID { @@ -242,6 +241,26 @@ namespace Barotrauma protected readonly Dictionary activeTeamChanges = new Dictionary(); protected ActiveTeamChange currentTeamChange; private const string OriginalChangeTeamIdentifier = "original"; + + public bool AllowPlayDead { get; set; } + + public void EvaluatePlayDeadProbability(float? probability = null) + { + if (Params.AI is CharacterParams.AIParams aiParams) + { + if (probability.HasValue) + { + // Override so that can't revert back to the old value. + aiParams.PlayDeadProbability = probability.Value; + } + AllowPlayDead = Rand.Value() <= aiParams.PlayDeadProbability; + } + else if (probability.HasValue) + { + AllowPlayDead = Rand.Value() <= probability.Value; + } + // Do nothing, if no value is defined and no AI Params were found. + } private void ThrowIfAccessingWalletsInSingleplayer() { @@ -254,26 +273,37 @@ namespace Barotrauma } } - public void SetOriginalTeam(CharacterTeamType newTeam) + /// + /// Saves the character's original team (which affects e.g. whether the character considers the sub/outpost they're in to be their own or a "foreign" one), + /// and adds a new team change to be processed on the next update. + /// + /// Should the team change be processed right now, or along with any other pending team changes in the next Update? + public void SetOriginalTeamAndChangeTeam(CharacterTeamType newTeam, bool processImmediately = false) { TryRemoveTeamChange(OriginalChangeTeamIdentifier); currentTeamChange = new ActiveTeamChange(newTeam, ActiveTeamChange.TeamChangePriorities.Base); TryAddNewTeamChange(OriginalChangeTeamIdentifier, currentTeamChange); + if (processImmediately) + { + UpdateTeam(); + } } private void ChangeTeam(CharacterTeamType newTeam) { if (newTeam == teamID) { return; } - if (originalTeamID == null) { originalTeamID = teamID; } + originalTeamID ??= teamID; TeamID = newTeam; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - // clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated) - var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority); - SetOrder(order, isNewOrder: true, speak: false); - + if (AIController is HumanAIController) + { + // clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated) + var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority); + SetOrder(order, isNewOrder: true, speak: false); + } #if SERVER GameMain.NetworkMember.CreateEntityEvent(this, new TeamChangeEventData()); #endif @@ -292,7 +322,7 @@ namespace Barotrauma if (currentTeamChange == null) { // set team logic to use active team changes as soon as the first team change is added - SetOriginalTeam(TeamID); + SetOriginalTeamAndChangeTeam(TeamID); } } else @@ -330,12 +360,13 @@ namespace Barotrauma bestTeamChange = desiredTeamChange.Value; } } - if (TeamID != bestTeamChange.DesiredTeamId) + if (TeamID != bestTeamChange.DesiredTeamId) { ChangeTeam(bestTeamChange.DesiredTeamId); currentTeamChange = bestTeamChange; - if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior + // this seemed like the least disruptive way to induce aggressive behavior on human characters + if (bestTeamChange.AggressiveBehavior && AIController is HumanAIController) { var order = OrderPrefab.Prefabs["fightintruders"].CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority); SetOrder(order, isNewOrder: true, speak: false); @@ -343,11 +374,13 @@ namespace Barotrauma } } - public bool IsOnPlayerTeam => teamID == CharacterTeamType.Team1 || teamID == CharacterTeamType.Team2; + public bool IsOnPlayerTeam => + teamID == CharacterTeamType.Team1 || + (teamID == CharacterTeamType.Team2 && !IsFriendlyNPCTurnedHostile); public bool IsOriginallyOnPlayerTeam => originalTeamID == CharacterTeamType.Team1 || originalTeamID == CharacterTeamType.Team2; - public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && teamID == CharacterTeamType.Team2; + public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && (teamID == CharacterTeamType.Team2 || teamID == CharacterTeamType.None); public bool IsInstigator => CombatAction is { IsInstigator: true }; @@ -413,15 +446,15 @@ namespace Barotrauma public Identifier GetBaseCharacterSpeciesName() => Prefab.GetBaseCharacterSpeciesName(SpeciesName); - public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group; + public Identifier Group => HumanPrefab is { Group.IsEmpty: false } prefab ? prefab.Group : Params.Group; public bool IsHumanoid => Params.Humanoid; public bool IsMachine => Params.IsMachine; public bool IsHusk => Params.Husk; - public bool IsDisguisedAsHusk => CharacterHealth.GetAfflictionStrengthByType("disguiseashusk".ToIdentifier()) > 0; - public bool IsHuskInfected => CharacterHealth.GetActiveAfflictionTags().Contains("huskinfected".ToIdentifier()); + public bool IsDisguisedAsHusk => CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.DisguisedAsHuskType) > 0; + public bool IsHuskInfected => CharacterHealth.GetActiveAfflictionTags().Contains(Tags.HuskInfected); public bool IsMale => info?.IsMale ?? false; @@ -668,6 +701,8 @@ namespace Barotrauma // Eating is not implemented for humanoids. If we implement that at some point, we could remove this restriction. public bool CanEat => !IsHumanoid && Params.CanEat && AllowInput && AnimController.GetLimb(LimbType.Head) != null; + + public bool CanClimb => Params.CanClimb && CanInteract; public Vector2 CursorPosition { @@ -862,7 +897,7 @@ namespace Barotrauma private float ragdollingLockTimer; public bool IsRagdolled; public bool IsForceRagdolled; - public bool dontFollowCursor; + public bool FollowCursor = true; public bool IsIncapacitated { @@ -1145,7 +1180,7 @@ namespace Barotrauma { get { - return (SelectedItem == null || SelectedItem.GetComponent() is { AllowAiming: true }) && !IsIncapacitated && (!IsRagdolled || AnimController.IsHoldingToRope); + return (SelectedItem == null || SelectedItem.GetComponent() is { AllowAiming: true }) && !IsKnockedDown && (!IsRagdolled || AnimController.IsHoldingToRope); } } @@ -1368,6 +1403,14 @@ namespace Barotrauma //no longer a new hire after spawning (only displayed as a new hire at the end of the outpost round, when the character hasn't spawned yet) Info.IsNewHire = false; } + if (characterInfo?.HumanPrefabIds is { } prefabIds && + prefabIds.NpcSetIdentifier != default && prefabIds.NpcIdentifier != default) + { + humanPrefab = NPCSet.Get( + characterInfo.HumanPrefabIds.NpcSetIdentifier, + characterInfo.HumanPrefabIds.NpcIdentifier); + } + keys = new Key[Enum.GetNames(typeof(InputType)).Length]; for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++) { @@ -1460,32 +1503,38 @@ namespace Barotrauma CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement); } - if (Params.Husk && speciesName != "husk" && Prefab.VariantOf != "husk") + if (Params.Husk) { - Identifier nonHuskedSpeciesName = Identifier.Empty; - AfflictionPrefabHusk matchingAffliction = null; - foreach (var huskPrefab in AfflictionPrefab.Prefabs.OfType()) + Identifier nonHuskedSpeciesName = Params.NonHuskedSpecies; + if (!nonHuskedSpeciesName.IsEmpty || Params.UseHuskAppendage) { - var nonHuskedName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, huskPrefab); - if (huskPrefab.TargetSpecies.Contains(nonHuskedName)) + // Check that there's a matching species and affliction for the non-husked species definition. + AfflictionPrefab matchingAffliction = null; + foreach (var huskPrefab in AfflictionPrefab.Prefabs.OfType()) { - var huskedSpeciesName = AfflictionHusk.GetHuskedSpeciesName(nonHuskedName, huskPrefab); - if (huskedSpeciesName.Equals(speciesName)) + if (huskPrefab.HuskedSpeciesName.IsEmpty) { continue; } + Identifier nonHuskedSpecies = nonHuskedSpeciesName; + if (nonHuskedSpeciesName.IsEmpty) { - nonHuskedSpeciesName = nonHuskedName; + nonHuskedSpecies = AfflictionHusk.GetNonHuskedSpeciesName(Params, huskPrefab); + } + if (huskPrefab.TargetSpecies.Contains(nonHuskedSpecies)) + { + nonHuskedSpeciesName = nonHuskedSpecies; matchingAffliction = huskPrefab; break; } - } - } - if (matchingAffliction == null || nonHuskedSpeciesName.IsEmpty) - { - DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition!\n" - + "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\"", - contentPackage: Prefab.ContentPackage); - // Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg. - nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier(); - speciesName = nonHuskedSpeciesName; + } + if (matchingAffliction == null) + { + DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition! " + + $"If the name of the character doesn't match the default pattern ('Crawlerhusk', 'Humanhusk', etc), you'll also need to define the non-husked species with {nameof(Params.NonHuskedSpecies)} attribute in the character config file.", + contentPackage: Prefab.ContentPackage); + + // Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg. + nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier(); + speciesName = nonHuskedSpeciesName; + } } if (ragdollParams == null && prefab.VariantOf == null) { @@ -1745,7 +1794,7 @@ namespace Barotrauma #endif } - public void GiveJobItems(WayPoint spawnPoint = null) + public void GiveJobItems(bool isPvPMode, WayPoint spawnPoint = null) { if (info == null) { return; } if (info.HumanPrefabIds != default) @@ -1760,7 +1809,7 @@ namespace Barotrauma return; } } - info.Job?.GiveJobItems(this, spawnPoint); + info.Job?.GiveJobItems(this, isPvPMode, spawnPoint); } public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false) @@ -1778,6 +1827,10 @@ namespace Barotrauma { item.AddTag(s); } + if (GameMain.GameSession?.GameMode is PvPMode) + { + item.AddTag($"id_{TeamID}".ToIdentifier()); + } if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true }) { GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item)); @@ -1785,9 +1838,6 @@ namespace Barotrauma } } - public float GetSkillLevel(string skillIdentifier) => - GetSkillLevel(skillIdentifier.ToIdentifier()); - private static readonly ImmutableDictionary overrideStatTypes = new Dictionary { { new("helm"), StatTypes.HelmSkillOverride }, @@ -1797,6 +1847,9 @@ namespace Barotrauma { new("mechanical"), StatTypes.MechanicalSkillOverride } }.ToImmutableDictionary(); + /// + /// Get the character's current skill level, taking into account any temporary boosts from wearables and afflictions + /// public float GetSkillLevel(Identifier skillIdentifier) { if (Info?.Job == null) { return 0.0f; } @@ -1839,7 +1892,11 @@ namespace Barotrauma } } - skillLevel += GetStatValue(GetSkillStatType(skillIdentifier)); + var skillStatType = GetSkillStatType(skillIdentifier); + if (skillStatType != StatTypes.None) + { + skillLevel += GetStatValue(skillStatType); + } return Math.Max(skillLevel, 0); } @@ -1876,11 +1933,30 @@ namespace Barotrauma // - dragging someone // - crouching // - moving backwards - public bool CanRun => CanRunWhileDragging() && + public bool CanRun => + !DisableRunning && + CanRunWhileDragging() && AnimController is not HumanoidAnimController { Crouching: true } && !AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk) && !AnimController.IsHoldingToRope; + private double disableRunningLastSet; + + /// + /// Can be used to temporarily disable running using StatusEffects. Resets in 0.1 seconds if not set. + /// + public bool DisableRunning + { + get => disableRunningLastSet > Timing.TotalTime - 0.1; + set + { + if (value) + { + disableRunningLastSet = Timing.TotalTime; + } + } + } + public bool CanRunWhileDragging() { if (selectedCharacter is not { IsDraggable: true }) { return true; } @@ -1919,8 +1995,7 @@ namespace Barotrauma /// Can be used to modify the character's speed via StatusEffects /// public float SpeedMultiplier { get; private set; } = 1; - - + private double propulsionSpeedMultiplierLastSet; private float propulsionSpeedMultiplier; /// @@ -1984,6 +2059,7 @@ namespace Barotrauma /// public float GetTemporarySpeedReduction() { + if (!Params.Health.ApplyMovementPenalties) { return 0; } float reduction = 0; reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightFoot, excludeSevered: false), reduction); reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftFoot, excludeSevered: false), reduction); @@ -2021,6 +2097,7 @@ namespace Barotrauma private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.8f) { + if (!Params.Health.ApplyMovementPenalties) { return 0; } if (limb != null) { sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: AfflictionPrefab.DamageType)); @@ -2111,7 +2188,7 @@ namespace Barotrauma ((!IsClimbing && AnimController.OnGround) || (IsClimbing && IsKeyDown(InputType.Aim))) && !AnimController.InWater) { - if (dontFollowCursor) + if (!FollowCursor) { AnimController.TargetDir = Direction.Right; } @@ -2242,7 +2319,7 @@ namespace Barotrauma if (attackTarget != null) { if (!attack.IsValidTarget(attackTarget as Entity)) { return false; } - if (attackTarget is ISerializableEntity se && attackTarget is Character) + if (attackTarget is ISerializableEntity se and Character) { if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; } } @@ -2279,7 +2356,7 @@ namespace Barotrauma if (Inventory != null) { - if (IsKeyHit(InputType.DropItem)) + if (IsKeyHit(InputType.DropItem) && Screen.Selected is { IsEditor: false }) { foreach (Item item in HeldItems) { @@ -2423,6 +2500,7 @@ namespace Barotrauma { System.Diagnostics.Debug.Assert(target != null); if (target == null || target.Removed) { return false; } + if (seeingEntity == null) { return false; } if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; } if (!target.AnimController.SimplePhysicsEnabled) { @@ -2583,6 +2661,39 @@ namespace Barotrauma } return null; } + + public bool HasHandsFull(out (Item leftHandItem, Item rightHandItem) items) + { + var leftHandItem = GetEquippedItem(slotType: InvSlotType.LeftHand); + var rightHandItem = GetEquippedItem(slotType: InvSlotType.RightHand); + items = (leftHandItem, rightHandItem); + bool handsFull = leftHandItem != null && rightHandItem != null; + return handsFull; + } + + public bool TryPutItem(Item item, IEnumerable allowedSlots) => Inventory.TryPutItem(item, user: this, allowedSlots); + public bool TryPutItemInBag(Item item) => item != null && item.AllowedSlots.Contains(InvSlotType.Bag) && TryPutItem(item, CharacterInventory.BagSlot); + public bool TryPutItemInAnySlot(Item item) => item != null && item.AllowedSlots.Contains(InvSlotType.Any) && TryPutItem(item, CharacterInventory.AnySlot); + + /// + /// Attempts to unequip an item. + /// First tries to put the item in any slot. + /// If that fails, tries to put in the bag slot. + /// If that too fails, drops the item. + /// + /// false only if the item is not equipped. + public bool Unequip(Item item) + { + if (!HasEquippedItem(item)) { return false; } + if (!TryPutItemInAnySlot(item)) + { + if (!TryPutItemInBag(item)) + { + item.Drop(this); + } + } + return true; + } public bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited) { @@ -2608,7 +2719,6 @@ namespace Barotrauma if (container != null) { if (!container.HasRequiredItems(this, addMessage: false)) { return false; } - if (!container.AllowAccess) { return false; } } } return true; @@ -2771,6 +2881,12 @@ namespace Barotrauma #endif if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; } + Controller controller = item.GetComponent(); + if (controller != null && IsAnySelectedItem(item) && controller.IsAttachedUser(this)) + { + return true; + } + if (item.ParentInventory != null) { return CanAccessInventory(item.ParentInventory); @@ -2880,7 +2996,7 @@ namespace Barotrauma { //don't allow selecting another Controller if it'd try to turn the character in the opposite direction //(e.g. periscope that's facing the wrong way while sitting in a chair) - if (item.GetComponent() is { } controller && controller.Direction != 0 && controller.Direction != AnimController.Direction) { return false; } + if (controller != null && controller.Direction != 0 && controller.Direction != AnimController.Direction) { return false; } //if a Controller that controls the character's pose is selected, //don't allow selecting items that are behind the character's back @@ -3358,7 +3474,9 @@ namespace Barotrauma if (Inventory != null) { - foreach (Item item in Inventory.AllItems) + //do not check for duplicates: this is code is called very frequently, and duplicates don't matter here, + //so it's better just to avoid the relatively expensive duplicate check + foreach (Item item in Inventory.GetAllItems(checkForDuplicates: false)) { if (item.body == null || item.body.Enabled) { continue; } item.SetTransform(SimPosition, 0.0f); @@ -3751,41 +3869,51 @@ namespace Barotrauma if (!IsDead || (CauseOfDeath?.Type == CauseOfDeathType.Disconnected && GameMain.GameSession?.Campaign != null)) { return; } - int subCorpseCount = 0; - - if (Submarine != null) - { - subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine); - if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; } - } - if (SelectedBy != null) { despawnTimer = 0.0f; return; } - float distToClosestPlayer = GetDistanceToClosestPlayer(); - if (distToClosestPlayer > Params.DisableDistance) - { - //despawn in 1 minute if very far from all human players - despawnTimer = Math.Max(despawnTimer, GameSettings.CurrentConfig.CorpseDespawnDelay - 60.0f); - } - + float despawnDelay = GameSettings.CurrentConfig.CorpseDespawnDelay; float despawnPriority = 1.0f; - if (subCorpseCount > GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) + if (GameMain.GameSession?.GameMode is PvPMode && + GameMain.NetworkMember?.RespawnManager != null) { - //despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast) - despawnPriority += (subCorpseCount - GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) / (float)GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold; + //simpler despawning logic in PvP modes with respawning: just a short timer + despawnDelay = GameSettings.CurrentConfig.CorpseDespawnDelayPvP; } - if (AIController is EnemyAIController) + else { - //enemies despawn faster - despawnPriority *= 2.0f; + int subCorpseCount = 0; + if (Submarine != null) + { + subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine); + if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; } + } + + if (subCorpseCount > GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) + { + //despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast) + despawnPriority += (subCorpseCount - GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) / (float)GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold; + } + + float distToClosestPlayer = GetDistanceToClosestPlayer(); + if (distToClosestPlayer > Params.DisableDistance) + { + //despawn in 1 minute if very far from all human players + despawnTimer = Math.Max(despawnTimer, despawnDelay - 60.0f); + } + + if (AIController is EnemyAIController) + { + //enemies despawn faster + despawnPriority *= 2.0f; + } } despawnTimer += deltaTime * despawnPriority; - if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; } + if (despawnTimer < despawnDelay) { return; } Despawn(); } @@ -3798,7 +3926,10 @@ namespace Barotrauma IsHuman ? Tags.DespawnContainer : Params.DespawnContainer; - if (!despawnContainerId.IsEmpty) + + //don't spawn duffel bags in PvP modes that include respawning, because it can lead to a ton of accumulated items in the sub/outpost + bool pvpWithRespawning = GameMain.GameSession?.GameMode is PvPMode && GameMain.NetworkMember?.RespawnManager != null; + if (!despawnContainerId.IsEmpty && !pvpWithRespawning) { var containerPrefab = MapEntityPrefab.FindByIdentifier(despawnContainerId) as ItemPrefab ?? @@ -3933,8 +4064,6 @@ namespace Barotrauma targetRange = Math.Min(targetRange, maxAIRange); float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed); - - newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier); if (!float.IsNaN(newRange)) { aiTarget.SoundRange = newRange; @@ -4165,13 +4294,24 @@ namespace Barotrauma { prevAiChatMessages.Remove(identifier); } - - //already sent a similar message a moment ago - if (identifier != Identifier.Empty && minDurationBetweenSimilar > 0.0f && - (aiChatMessageQueue.Any(m => m.Identifier == identifier) || prevAiChatMessages.ContainsKey(identifier))) + + if (minDurationBetweenSimilar > 0) { - return; + if (identifier == Identifier.Empty) + { +#if DEBUG + // TODO: This is stupid. We shouldn't allow passing minDurationBetweenSimilar without an identifier in the first place, but need to think how to refactor this. + DebugConsole.AddWarning($"Called Character.Speak() with minDurationBetweenSimilar but didn't define the identifier! Cannot compare with the old messages. The message will be sent each time the function is called."); + Debugger.Break(); +#endif + } + else if (aiChatMessageQueue.Any(m => m.Identifier == identifier) || prevAiChatMessages.ContainsKey(identifier)) + { + //already sent a similar message a moment ago + return; + } } + aiChatMessageQueue.Add(new AIChatMessage(message, messageType, identifier, delay)); } @@ -4269,7 +4409,9 @@ namespace Barotrauma Limb limbHit = targetLimb; - float impulseMagnitude = (attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier) * deltaTime; + // TODO: should we apply deltatime only on TargetForce, not TargetImpulse? Changing this would have implications on many existing monster attacks, so all the monsters would have to be tested and possibly readjusted. + // Should be (attack.TargetImpulse + attack.TargetForce * deltaTime) * attack.ImpactMultiplier? + float impulseMagnitude = (attack.TargetImpulse + attack.TargetForce) * attack.ImpactMultiplier * deltaTime; Vector2 attackImpulse = Vector2.Zero; if (Math.Abs(impulseMagnitude) > 0.0f) @@ -4303,7 +4445,7 @@ namespace Barotrauma } if (limbHit == null) { return new AttackResult(); } - Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld * attack.ImpactMultiplier; + Vector2 forceWorld = (attack.TargetImpulseWorld + attack.TargetForceWorld) * attack.ImpactMultiplier; if (attacker != null) { forceWorld.X *= attacker.AnimController.Dir; @@ -4445,35 +4587,21 @@ namespace Barotrauma CreatureMetrics.RecordKill(target.SpeciesName); } - public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false) + /// + /// + /// + /// Set false as an optimization only when you manually call . Only applies to limb specific afflictions. + public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false, bool ignoreDamageOverlay = false, bool recalculateVitality = true) { if (Removed) { return new AttackResult(); } - //character inside the sub received damage from a monster outside the sub - //can happen during normal gameplay if someone for example fires a ranged weapon from outside, - //the intention of this error message is to diagnose an issue with monsters being able to damage characters from outside - - // Disabled, because this happens every now and then when the monsters can get in and out of the sub. - -// if (attacker?.AIController is EnemyAIController && Submarine != null && attacker.Submarine == null) -// { -// string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})"; -// GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name, -// GameAnalyticsManager.ErrorSeverity.Warning, -// errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); -//#if DEBUG -// DebugConsole.ThrowError(errorMsg); -//#endif -// } - SetStun(stun); if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire) { if (attacker.TeamID == TeamID) { - afflictions = afflictions.Where(a => a.Prefab.IsBuff); - if (!afflictions.Any()) { return new AttackResult(); } + if (afflictions.None(a => a.Prefab.IsBuff)) { return new AttackResult(); } } } @@ -4493,9 +4621,8 @@ namespace Barotrauma } bool wasDead = IsDead; Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir); - float prevVitality = CharacterHealth.Vitality; AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration, attacker: attacker); - CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking); + CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking, recalculateVitality); if (shouldImplode) { // Only used by assistant's True Potential talent. Has to run here in order to properly give kill credit when it activates. @@ -4504,8 +4631,16 @@ namespace Barotrauma if (attacker != this) { + bool wasDamageOverlayVisible = CharacterHealth.ShowDamageOverlay; + if (ignoreDamageOverlay) + { + // Temporarily ignore damage overlay (husk transition damage) + CharacterHealth.ShowDamageOverlay = false; + } OnAttacked?.Invoke(attacker, attackResult); OnAttackedProjSpecific(attacker, attackResult, stun); + // Reset damage overlay + CharacterHealth.ShowDamageOverlay = wasDamageOverlayVisible; if (!wasDead) { TryAdjustAttackerSkill(attacker, attackResult); @@ -4633,6 +4768,13 @@ namespace Barotrauma return; } } + + // apply pvp stun resistance to humans (reduce stun amount via resist multiplier) + if (newStun > 0 && GameMain.NetworkMember is { } networkMember && GameMain.GameSession?.GameMode is PvPMode && IsHuman) + { + newStun = Math.Max(0, newStun - (newStun * networkMember.ServerSettings.PvPStunResist)); + } + if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; } if (Math.Sign(newStun) != Math.Sign(Stun)) { @@ -4811,6 +4953,24 @@ namespace Barotrauma } partial void ImplodeFX(); + + public void TurnIntoHusk(AfflictionPrefabHusk huskInfection = null, bool? playDead = null) + { + huskInfection ??= AfflictionPrefab.HuskInfection as AfflictionPrefabHusk; + if (huskInfection == null) + { + DebugConsole.ThrowError($"Cannot turn {Name} into husk, because husk infection was not found!", contentPackage: AfflictionPrefab.Prefabs.First().ContentPackage); + return; + } + // Randomize the start strength a bit, so that the husks don't turn at the same time, which can cause performance issues when turning multiple characters to husk at the same time. + float startStrength = Rand.Range(Math.Max(huskInfection.MaxStrength - 2, huskInfection.ActiveThreshold), huskInfection.MaxStrength); + startStrength *= MaxVitality / 100f; + CharacterHealth.ApplyAffliction(AnimController.MainLimb, huskInfection.Instantiate(startStrength)); + if (playDead.HasValue) + { + AllowPlayDead = playDead.Value; + } + } public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true) { @@ -5387,6 +5547,27 @@ namespace Barotrauma public IReadOnlyCollection CharacterTalents => characterTalents; + /// + /// Removes the talents the character has unlocked in their talent tree. + /// + public void ResetTalents(bool applyXpPenalty) + { + characterTalents.Clear(); + abilityResistances.Clear(); + abilityFlags = AbilityFlags.None; + CharacterHealth.RemoveAfflictions(affliction => affliction.Prefab.AfflictionType == Tags.AfflictionTypeTalentBuff); + statValues.Clear(); + + if (applyXpPenalty) + { + int currentLevel = info.GetCurrentLevel(); + if (currentLevel > 0) + { + info.SetExperience(info.ExperiencePoints - CharacterInfo.ExperienceRequiredPerLevel(currentLevel)); + } + } + } + public void LoadTalents() { List toBeRemoved = null; @@ -5590,12 +5771,12 @@ namespace Barotrauma #if CLIENT public void SetMoney(int amount) { - if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; } - if (amount == campaign.Wallet.Balance) { return; } + if (Wallet == null) { return; } + if (amount == Wallet.Balance) { return; } - int prevAmount = campaign.Wallet.Balance; - campaign.Wallet.Balance = amount; - OnMoneyChanged(prevAmount, campaign.Wallet.Balance); + int prevAmount = Wallet.Balance; + Wallet.Balance = amount; + OnMoneyChanged(prevAmount, Wallet.Balance); } #endif @@ -5788,9 +5969,12 @@ namespace Barotrauma return myTeam switch { // NPCs are friendly to the same team and the friendly NPCs - CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC, + CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC, // Friendly NPCs are friendly to both player teams CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2, + // None (bandits and such) consider friendly NPCs friendly, not attacking them unless they attack first + // Otherwise bandits would for example attach the hostages. + CharacterTeamType.None => otherTeam == CharacterTeamType.FriendlyNPC, _ => true }; } @@ -5802,6 +5986,8 @@ namespace Barotrauma public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other); public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group); + + public bool MatchesSpeciesNameOrGroup(Identifier speciesNameOrGroup) => Prefab.MatchesSpeciesNameOrGroup(speciesNameOrGroup); public void StopClimbing() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs index 5392608ed..e05e5b992 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -29,11 +29,13 @@ namespace Barotrauma UpdatePermanentStats = 14, RemoveFromCrew = 15, LatchOntoTarget = 16, - + UpdateTalentRefundPoints = 17, + ConfirmTalentRefund = 18, + MinValue = 0, - MaxValue = 16 + MaxValue = 18 } - + private interface IEventData : NetEntityEvent.IData { public EventType EventType { get; } @@ -230,9 +232,18 @@ namespace Barotrauma public struct UpdateSkillsEventData : IEventData { - public EventType EventType => EventType.UpdateSkills; + public readonly EventType EventType => EventType.UpdateSkills; + + public readonly bool ForceNotification; + public readonly Identifier SkillIdentifier; + + public UpdateSkillsEventData(Identifier skillIdentifier, bool forceNotification) + { + SkillIdentifier = skillIdentifier; + ForceNotification = forceNotification; + } } - + private struct UpdateMoneyEventData : IEventData { public EventType EventType => EventType.UpdateMoney; @@ -242,11 +253,21 @@ namespace Barotrauma { public EventType EventType => EventType.UpdatePermanentStats; public readonly StatTypes StatType; - + public UpdatePermanentStatsEventData(StatTypes statType) { StatType = statType; } } + + public struct UpdateRefundPointsEventData : IEventData + { + public EventType EventType => EventType.UpdateTalentRefundPoints; + } + + public struct ConfirmRefundEventData : IEventData + { + public EventType EventType => EventType.ConfirmTalentRefund; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 3706c4360..a153949dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -344,15 +344,37 @@ namespace Barotrauma /// Note: Can be null. /// public Character Character; - + public Job Job; - + public int Salary; public int ExperiencePoints { get; private set; } + private int talentRefundPoints; + + /// + /// How many times the player is eligible to refund talents + /// + public int TalentRefundPoints + { + get => talentRefundPoints; + set => talentRefundPoints = MathHelper.Max(value, 0); + } + public HashSet UnlockedTalents { get; private set; } = new HashSet(); + private int talentResetCount; + + /// + /// How many times have the characters' talents been reset? + /// + public int TalentResetCount + { + get => talentResetCount; + set => talentResetCount = MathHelper.Max(value, 0); + } + public (Identifier factionId, float reputation) MinReputationToHire; /// @@ -708,7 +730,9 @@ namespace Barotrauma SetAttachments(randSync); SetColors(randSync); - Job = job ?? ((jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant)); + Job = job ?? ((jobPrefab == null) ? + Job.Random(isPvP: false, Rand.RandSync.Unsynced) : + new Job(jobPrefab, isPvP: false, randSync, variant)); if (!string.IsNullOrEmpty(name)) { @@ -725,6 +749,8 @@ namespace Barotrauma } OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name; + TalentRefundPoints = CharacterConfigElement.GetAttributeInt("refundpoints", 0); + int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1); if (loadedLastRewardDistribution >= 0) { @@ -799,6 +825,7 @@ namespace Barotrauma Salary = infoElement.GetAttributeInt("salary", 1000); ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0); AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0); + TalentResetCount = infoElement.GetAttributeInt(nameof(talentResetCount), 0); HashSet tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); LoadTagsBackwardsCompatibility(infoElement, tags); SpeciesName = infoElement.GetAttributeIdentifier("speciesname", ""); @@ -1047,6 +1074,7 @@ namespace Barotrauma public string ReplaceVars(string str) { + if (Head == null) { return str; } return Prefab.ReplaceVars(str, Head.Preset); } @@ -1271,18 +1299,18 @@ namespace Barotrauma /// Increases the characters skill at a rate proportional to their current skill. /// If you want to increase the skill level by a specific amount instead, use /// - public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f) + public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f, bool forceNotification = false) { float skillLevel = Job.GetSkillLevel(skillIdentifier); // The formula is too generous on low skill levels, hence the minimum divider. float skillDivider = MathF.Pow(Math.Max(skillLevel, 15f), SkillSettings.Current.SkillIncreaseExponent); - IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility); + IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility, forceNotification); } /// /// Increase the skill by a specific amount. Talents may affect the actual, final skill increase. /// - public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false) + public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false, bool forceNotification = false) { if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } @@ -1312,14 +1340,14 @@ namespace Barotrauma } } - OnSkillChanged(skillIdentifier, prevLevel, newLevel); + OnSkillChanged(skillIdentifier, prevLevel, newLevel, forceNotification); } private static readonly ImmutableDictionary skillGainStatValues = new Dictionary { { new("helm"), StatTypes.HelmSkillGainSpeed }, - { new("medical"), StatTypes.WeaponsSkillGainSpeed }, - { new("weapons"), StatTypes.MedicalSkillGainSpeed }, + { new("weapons"), StatTypes.WeaponsSkillGainSpeed }, + { new("medical"), StatTypes.MedicalSkillGainSpeed }, { new("electrical"), StatTypes.ElectricalSkillGainSpeed }, { new("mechanical"), StatTypes.MechanicalSkillGainSpeed } }.ToImmutableDictionary(); @@ -1334,7 +1362,7 @@ namespace Barotrauma return increase; } - public void SetSkillLevel(Identifier skillIdentifier, float level) + public void SetSkillLevel(Identifier skillIdentifier, float level, bool forceNotification = false) { if (Job == null) { return; } @@ -1342,17 +1370,17 @@ namespace Barotrauma if (skill == null) { Job.IncreaseSkillLevel(skillIdentifier, level, increasePastMax: false); - OnSkillChanged(skillIdentifier, 0.0f, level); + OnSkillChanged(skillIdentifier, 0.0f, level, forceNotification); } else { float prevLevel = skill.Level; skill.Level = level; - OnSkillChanged(skillIdentifier, prevLevel, skill.Level); + OnSkillChanged(skillIdentifier, prevLevel, skill.Level, forceNotification); } } - partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel); + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel, bool forceNotification); public void GiveExperience(int amount) { @@ -1437,10 +1465,11 @@ namespace Barotrauma experienceRequired += ExperienceRequiredPerLevel(level); level++; } - return level; + + return Math.Max(level, 0); } - private static int ExperienceRequiredPerLevel(int level) + public static int ExperienceRequiredPerLevel(int level) { return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level; } @@ -1449,6 +1478,45 @@ namespace Barotrauma partial void OnPermanentStatChanged(StatTypes statType); + public void RefundTalents() + { + if (TalentRefundPoints <= 0) { return; } + + //e.g. talents from endocrine booster or extra talents some special NPC has + var talentsFromOutsideTree = GetUnlockedTalentsOutsideTree().ToList(); + + bool applyXpPenalty = talentResetCount > 0; + + UnlockedTalents.Clear(); + SavedStatValues.Clear(); + Character?.ResetTalents(applyXpPenalty); + TalentRefundPoints--; + talentResetCount++; + + //it's simpler to just remove everything first and then reapply the "extra" talents than to + //try determining which talent the resistances, ability flags etc came from and only remove specific ones + if (Character == null) + { + talentsFromOutsideTree.ForEach(talentId => UnlockedTalents.Add(talentId)); + } + else + { + talentsFromOutsideTree.ForEach(talentId => Character.GiveTalent(talentId, addingFirstTime: true)); + } + + GameMain.NetworkMember?.CreateEntityEvent(Character, new Character.ConfirmRefundEventData()); + } + + public void AddRefundPoints(int newRefundPoints) + { + TalentRefundPoints += newRefundPoints; +#if SERVER + GameMain.NetworkMember?.CreateEntityEvent(Character, new Character.UpdateRefundPointsEventData()); +#elif CLIENT + ShowTalentResetPopupOnOpen = true; +#endif + } + public void Rename(string newName) { if (string.IsNullOrEmpty(newName)) { return; } @@ -1491,6 +1559,7 @@ namespace Barotrauma new XAttribute("salary", Salary), new XAttribute("experiencepoints", ExperiencePoints), new XAttribute("additionaltalentpoints", AdditionalTalentPoints), + new XAttribute(nameof(talentResetCount), TalentResetCount), new XAttribute("hairindex", Head.HairIndex), new XAttribute("beardindex", Head.BeardIndex), new XAttribute("moustacheindex", Head.MoustacheIndex), @@ -1500,6 +1569,7 @@ namespace Barotrauma new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)), new XAttribute("startitemsgiven", StartItemsGiven), new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty), + new XAttribute("refundpoints", TalentRefundPoints), new XAttribute("lastrewarddistribution", LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()), new XAttribute("permanentlydead", PermanentlyDead), new XAttribute("renamingenabled", RenamingEnabled) @@ -1601,7 +1671,7 @@ namespace Barotrauma targetAvailableInNextLevel = !isOutside && GameMain.GameSession?.Campaign is not { SwitchedSubsThisRound: true } && - (isOnConnectedLinkedSub || entitySub == Submarine.MainSub); + (isOnConnectedLinkedSub || (Submarine.MainSub != null && entitySub == Submarine.MainSub)); if (!targetAvailableInNextLevel) { if (!order.Prefab.CanBeGeneralized) @@ -1636,7 +1706,7 @@ namespace Barotrauma } if (order.TargetSpatialEntity?.Submarine is Submarine targetSub) { - if (targetSub == Submarine.MainSub) + if (Submarine.MainSub != null && targetSub == Submarine.MainSub) { orderElement.Add(new XAttribute("onmainsub", true)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index 22f22f6c6..93e4b1bf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -33,6 +33,10 @@ namespace Barotrauma } public bool HasCharacterInfo { get; private set; } + + public Identifier Group { get; private set; } + + public bool MatchesSpeciesNameOrGroup(Identifier speciesNameOrGroup) => Identifier == speciesNameOrGroup || Group == speciesNameOrGroup; public void InheritFrom(CharacterPrefab parent) { @@ -52,9 +56,10 @@ namespace Barotrauma { CharacterInfoPrefab = new CharacterInfoPrefab(this, headsElement, varsElement, menuCategoryElement, pronounsElement); } + Group = ConfigElement.GetAttributeIdentifier(nameof(Group), Identifier.Empty); } - private readonly XElement originalElement; + private readonly ContentXElement originalElement; public ContentXElement ConfigElement { get; private set; } public CharacterInfoPrefab CharacterInfoPrefab { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index baa030c31..17a793fcc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -337,18 +337,24 @@ namespace Barotrauma } } - public float GetResistance(Identifier afflictionId) + /// + /// How much resistance to the specified affliction does this affliction currently give? + /// + public float GetResistance(Identifier afflictionId, LimbType limbType) { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } var affliction = AfflictionPrefab.Prefabs[afflictionId]; AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } - if (!currentEffect.ResistanceFor.Any(r => - r == affliction.Identifier || - r == affliction.AfflictionType)) - { - return 0.0f; - } + + bool hasResistanceForAffliction = currentEffect.ResistanceFor.Any(identifier => + identifier == affliction.Identifier || + identifier == affliction.AfflictionType); + if (!hasResistanceForAffliction) { return 0.0f; } + + bool hasResistanceForLimb = limbType == LimbType.None || currentEffect.ResistanceLimbs.None() || currentEffect.ResistanceLimbs.Contains(limbType); + if (!hasResistanceForLimb) { return 0.0f; } + return MathHelper.Lerp( currentEffect.MinResistance, currentEffect.MaxResistance, @@ -430,7 +436,7 @@ namespace Barotrauma } else if (currentEffect.StrengthChange > 0) // Reduce strengthening of afflictions if resistant { - _strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab)); + _strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab, targetLimb?.type ?? LimbType.None)); } // Don't use the property, because it's virtual and some afflictions like husk overload it for external use. _strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs index bc8ac1329..ea52bef11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs @@ -13,7 +13,7 @@ public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) { base.Update(characterHealth, targetLimb, deltaTime); - float bloodlossResistance = GetResistance(characterHealth.BloodlossAffliction.Identifier); + float bloodlossResistance = characterHealth.GetResistance(characterHealth.BloodlossAffliction.Prefab, targetLimb?.type ?? LimbType.None); characterHealth.BloodlossAmount += Strength * (1.0f - bloodlossResistance) / 60.0f * deltaTime; if (Source != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index a4338ac70..fbed88ebe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -28,8 +28,6 @@ namespace Barotrauma private bool stun = false; - private readonly List huskInfection = new List(); - [Serialize(0f, IsPropertySaveable.Yes), Editable] public override float Strength { @@ -62,7 +60,7 @@ namespace Barotrauma } } - private readonly AfflictionPrefabHusk HuskPrefab; + public readonly AfflictionPrefabHusk HuskPrefab; private float DormantThreshold => HuskPrefab.DormantThreshold; private float ActiveThreshold => HuskPrefab.ActiveThreshold; @@ -129,7 +127,7 @@ namespace Barotrauma { State = InfectionState.Final; ActivateHusk(); - ApplyDamage(deltaTime, applyForce: true); + ApplyDamage(deltaTime); character.SetStun(5); } } @@ -192,27 +190,41 @@ namespace Barotrauma prevDisplayedMessage = State; } - private void ApplyDamage(float deltaTime, bool applyForce) + private const float DamageCooldown = 0.1f; + private float damageCooldownTimer; + private void ApplyDamage(float deltaTime) { - int limbCount = character.AnimController.Limbs.Count(l => !l.IgnoreCollisions && !l.IsSevered && !l.Hidden); + if (damageCooldownTimer > 0) + { + damageCooldownTimer -= deltaTime; + return; + } + damageCooldownTimer = DamageCooldown; + int limbCount = character.AnimController.Limbs.Count(IsValidLimb); foreach (Limb limb in character.AnimController.Limbs) { - if (limb.IsSevered) { continue; } - if (limb.Hidden) { continue; } + if (!IsValidLimb(limb)) { continue; } float random = Rand.Value(); - huskInfection.Clear(); - huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount)); + if (random == 0) { continue; } + const float damageRate = 2; + float dmg = random / limbCount * damageRate; character.LastDamageSource = null; - float force = applyForce ? random * 0.5f * limb.Mass : 0; - character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, Rand.Vector(force)); + var afflictions = AfflictionPrefab.InternalDamage.Instantiate(dmg).ToEnumerable(); + const float forceMultiplier = 5; + float force = dmg * limb.Mass * forceMultiplier; + character.DamageLimb(limb.WorldPosition, limb, afflictions, stun: 0, playSound: false, Rand.Vector(force), ignoreDamageOverlay: true, recalculateVitality: false); } + character.CharacterHealth.RecalculateVitality(); + + static bool IsValidLimb(Limb limb) => !limb.IgnoreCollisions && !limb.IsSevered && !limb.Hidden; } public void ActivateHusk() { if (huskAppendage == null && character.Params.UseHuskAppendage) { - huskAppendage = AttachHuskAppendage(character, Prefab as AfflictionPrefabHusk); + var huskAffliction = Prefab as AfflictionPrefabHusk; + huskAppendage = AttachHuskAppendage(character, huskAffliction, GetHuskedSpeciesName(character.Params, huskAffliction)); } if (Prefab is AfflictionPrefabHusk { NeedsAir: false }) @@ -287,7 +299,7 @@ namespace Barotrauma Entity.Spawner.AddEntityToRemoveQueue(character); UnsubscribeFromDeathEvent(); - Identifier huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk); + Identifier huskedSpeciesName = GetHuskedSpeciesName(character.Params, Prefab as AfflictionPrefabHusk); CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName); if (prefab == null) @@ -314,6 +326,7 @@ namespace Barotrauma husk.Info.Character = husk; husk.Info.TeamID = CharacterTeamType.None; } + husk.AllowPlayDead = character.AllowPlayDead; if (Prefab is AfflictionPrefabHusk huskPrefab) { @@ -379,11 +392,9 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public static List AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null) + public static List AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, Identifier huskedSpeciesName, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null) { var appendage = new List(); - Identifier nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction); - Identifier huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction); CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName); if (huskPrefab?.ConfigElement == null) { @@ -406,10 +417,7 @@ namespace Barotrauma ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty; XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage); if (doc == null) { return appendage; } - if (ragdoll == null) - { - ragdoll = character.AnimController; - } + ragdoll ??= character.AnimController; if (ragdoll.Dir < 1.0f) { ragdoll.Flip(); @@ -463,19 +471,31 @@ namespace Barotrauma ragdoll.AddLimb(huskAppendage); ragdoll.AddJoint(jointParams); appendage.Add(huskAppendage); - } + } } return appendage; } - public static Identifier GetHuskedSpeciesName(Identifier speciesName, AfflictionPrefabHusk prefab) + public static Identifier GetHuskedSpeciesName(CharacterParams character, AfflictionPrefabHusk prefab) { - return new Identifier(speciesName.Value + prefab.HuskedSpeciesName.Value); + Identifier huskedSpecies = character.HuskedSpecies; + if (huskedSpecies.IsEmpty) + { + // Default pattern: Crawler -> Crawlerhusk, Human -> Humanhusk + return new Identifier(character.SpeciesName.Value + prefab.HuskedSpeciesName.Value); + } + return huskedSpecies; } - public static Identifier GetNonHuskedSpeciesName(Identifier huskedSpeciesName, AfflictionPrefabHusk prefab) + public static Identifier GetNonHuskedSpeciesName(CharacterParams character, AfflictionPrefabHusk prefab) { - return huskedSpeciesName.Remove(prefab.HuskedSpeciesName); + Identifier nonHuskedSpecies = character.NonHuskedSpecies; + if (nonHuskedSpecies.IsEmpty) + { + // Default pattern: Crawlerhusk -> Crawler, Humanhusk -> Human + return character.SpeciesName.Remove(prefab.HuskedSpeciesName); + } + return nonHuskedSpecies; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index e74665360..69aa033cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -329,6 +329,11 @@ namespace Barotrauma /// public readonly ImmutableArray ResistanceFor; + /// + /// List of limb types that the resistance applies to. If empty, the resistance applies to the whole body. + /// + public readonly ImmutableArray ResistanceLimbs; + [Serialize(0.0f, IsPropertySaveable.No, description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's lowest strength.")] public float MinResistance { get; private set; } @@ -359,6 +364,14 @@ namespace Barotrauma description: "Color to tint the affected character's entire body with at this effect's highest strength. The alpha channel is used to determine how much to tint the character.")] public Color MaxBodyTint { get; private set; } + [Serialize(0.0f, IsPropertySaveable.No, + description: "Range of the \"thermal goggles overlay\" enabled by the affliction.")] + public float ThermalOverlayRange { get; private set; } + + [Serialize("255,0,0,255", IsPropertySaveable.No, + description: $"Color of the \"thermal goggles overlay\" enabled by the affliction. Only has an effect if {nameof(ThermalOverlayRange)} is larger than 0.")] + public Color ThermalOverlayColor { get; private set; } + /// /// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength. /// @@ -423,6 +436,8 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, element); ResistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty())!.ToImmutableArray(); + ResistanceLimbs = element.GetAttributeEnumArray("resistancelimbs", Array.Empty()).ToImmutableArray(); + BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty())!.ToImmutableArray(); var afflictionStatValues = new Dictionary(); @@ -594,7 +609,7 @@ namespace Barotrauma } else { - MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 1.0f); + MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 0.1f); MaxInterval = Math.Max(element.GetAttributeFloat(nameof(MaxInterval), 1.0f), MinInterval); MinStrength = Math.Max(element.GetAttributeFloat(nameof(MinStrength), 0f), 0f); MaxStrength = Math.Max(element.GetAttributeFloat(nameof(MaxStrength), MinStrength), MinStrength); @@ -612,6 +627,7 @@ namespace Barotrauma public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier(); public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier(); public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier(); + public static readonly Identifier DisguisedAsHuskType = "disguiseashusk".ToIdentifier(); public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"]; public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"]; @@ -624,7 +640,7 @@ namespace Barotrauma public static AfflictionPrefab OrganDamage => Prefabs["organdamage"]; public static AfflictionPrefab Stun => Prefabs[StunType]; public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"]; - + public static AfflictionPrefab HuskInfection => Prefabs["huskinfection"]; public static readonly PrefabCollection Prefabs = new PrefabCollection(); @@ -898,7 +914,10 @@ namespace Barotrauma if (element.GetAttribute("nameidentifier") != null) { - Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty)).Fallback(Name); + string nameIdentifier = element.GetAttributeString("nameidentifier", string.Empty); + Name = TextManager.Get(nameIdentifier) + .Fallback(TextManager.Get($"AfflictionName.{nameIdentifier}")) + .Fallback(Name); } LimbSpecific = element.GetAttributeBool("limbspecific", false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 03a1f1e26..bb796b36e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -248,6 +248,12 @@ namespace Barotrauma /// Was the character in full health at the beginning of the frame? /// public bool WasInFullHealth { get; private set; } + + /// + /// Show the blood overlay screen space effect when the character takes damage. + /// Enabled normally, but can be disabled for some special cases. + /// + public bool ShowDamageOverlay = true; public Affliction PressureAffliction { @@ -442,7 +448,7 @@ namespace Barotrauma return strength; } - public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false) + public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false, bool recalculateVitality = true) { if (Character.GodMode) { return; } if (!ignoreUnkillability) @@ -456,12 +462,12 @@ namespace Barotrauma //if a limb-specific affliction is applied to no specific limb, apply to all limbs foreach (LimbHealth limbHealth in limbHealths) { - AddLimbAffliction(limbHealth, affliction, allowStacking: allowStacking); + AddLimbAffliction(limbHealth, limb: null, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality); } } else { - AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking); + AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality); } } else @@ -470,14 +476,17 @@ namespace Barotrauma } } - public float GetResistance(AfflictionPrefab afflictionPrefab) + /// + /// How much resistance all the afflictions the character has give to the specified affliction? + /// + public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType) { // This is a % resistance (0 to 1.0) float resistance = 0.0f; foreach (KeyValuePair kvp in afflictions) { var affliction = kvp.Key; - resistance += affliction.GetResistance(afflictionPrefab.Identifier); + resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType); } // This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab); @@ -610,7 +619,11 @@ namespace Barotrauma CalculateVitality(); } - public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true) + /// + /// + /// + /// Set false only as an optimization when you manually call . Only applies to limb specific afflictions. + public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true, bool recalculateVitality = true) { if (Unkillable || Character.GodMode) { return; } if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count) @@ -619,18 +632,19 @@ namespace Barotrauma "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + hitLimb.type + " is targeting index " + hitLimb.HealthIndex); return; } - + foreach (Affliction newAffliction in attackResult.Afflictions) { if (newAffliction.Prefab.LimbSpecific) { - AddLimbAffliction(hitLimb, newAffliction, allowStacking); + AddLimbAffliction(hitLimb, newAffliction, allowStacking, recalculateVitality: recalculateVitality); } else { + // Always recalculate vitality for non-limb specific afflictions. AddAffliction(newAffliction, allowStacking); } - } + } } private void KillIfOutOfVitality() @@ -664,9 +678,8 @@ namespace Barotrauma if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); } if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); } } - - CalculateVitality(); - KillIfOutOfVitality(); + + RecalculateVitality(); } public float GetLimbDamage(Limb limb, Identifier afflictionType) @@ -697,6 +710,17 @@ namespace Barotrauma } } + public void RemoveAfflictions(Func predicate) + { + afflictionsToRemove.Clear(); + afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction))); + foreach (var affliction in afflictionsToRemove) + { + afflictions.Remove(affliction); + } + CalculateVitality(); + } + public void RemoveAllAfflictions() { afflictionsToRemove.Clear(); @@ -731,7 +755,11 @@ namespace Barotrauma CalculateVitality(); } - private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true) + /// + /// + /// + /// Set false only as an optimization when you manually call + private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true) { if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; } if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) @@ -740,11 +768,16 @@ namespace Barotrauma "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex); return; } - AddLimbAffliction(limbHealths[limb.HealthIndex], newAffliction, allowStacking); + AddLimbAffliction(limbHealths[limb.HealthIndex], limb, newAffliction, allowStacking, recalculateVitality); } - private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true) + /// + /// + /// + /// Set false only as an optimization when you manually call + private void AddLimbAffliction(LimbHealth limbHealth, Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true) { + LimbType limbType = limb?.type ?? LimbType.None; if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; } if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } @@ -778,7 +811,7 @@ namespace Barotrauma if (existingAffliction != null) { - float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab)); + float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab, limbType)); if (allowStacking) { // Add the existing strength @@ -789,15 +822,17 @@ namespace Barotrauma existingAffliction.Strength = newStrength; existingAffliction.Duration = existingAffliction.Prefab.Duration; if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; } - CalculateVitality(); - KillIfOutOfVitality(); + if (recalculateVitality) + { + RecalculateVitality(); + } return; } //create a new instance of the affliction to make sure we don't use the same instance for multiple characters //or modify the affliction instance of an Attack or a StatusEffect var copyAffliction = newAffliction.Prefab.Instantiate( - Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), + Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType))), newAffliction.Source); afflictions.Add(copyAffliction, limbHealth); AchievementManager.OnAfflictionReceived(copyAffliction, Character); @@ -805,8 +840,10 @@ namespace Barotrauma Character.HealthUpdateInterval = 0.0f; - CalculateVitality(); - KillIfOutOfVitality(); + if (recalculateVitality) + { + RecalculateVitality(); + } #if CLIENT if (OpenHealthWindow != this && limbHealth != null) { @@ -817,7 +854,7 @@ namespace Barotrauma private void AddAffliction(Affliction newAffliction, bool allowStacking = true) { - AddLimbAffliction(limbHealth: null, newAffliction, allowStacking); + AddLimbAffliction(limbHealth: null, limb: null, newAffliction, allowStacking); } partial void UpdateSkinTint(); @@ -902,14 +939,15 @@ namespace Barotrauma if (!Character.GodMode) { #if CLIENT - if (Character.IsVisible) + updateVisualsTimer -= deltaTime; + if (Character.IsVisible && updateVisualsTimer <= 0.0f) { UpdateLimbAfflictionOverlays(); UpdateSkinTint(); + updateVisualsTimer = UpdateVisualsInterval; } #endif - CalculateVitality(); - KillIfOutOfVitality(); + RecalculateVitality(); } } @@ -941,7 +979,7 @@ namespace Barotrauma /// /// 0-1. /// - public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab); + public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab, LimbType.None); private void UpdateOxygen(float deltaTime) { @@ -951,7 +989,7 @@ namespace Barotrauma return; } - float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab); + float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab, LimbType.None); float prevOxygen = OxygenAmount; if (IsUnconscious) { @@ -991,13 +1029,13 @@ namespace Barotrauma CalculateVitality(); } - public void CalculateVitality() + private void CalculateVitality() { vitality = MaxVitality; IsParalyzed = false; if (Unkillable || Character.GodMode) { return; } - foreach (var (affliction, limbHealth) in afflictions) + foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions) { float vitalityDecrease = affliction.GetVitalityDecrease(this); if (limbHealth != null) @@ -1020,6 +1058,12 @@ namespace Barotrauma } #endif } + + public void RecalculateVitality() + { + CalculateVitality(); + KillIfOutOfVitality(); + } private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index dcdd8ddb4..6416787d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -53,7 +53,7 @@ namespace Barotrauma private set { rawAfflictionIdentifierString = value; - ParseAfflictionIdentifiers(); + parsedAfflictionIdentifiers = rawAfflictionIdentifierString.ToIdentifiers().ToImmutableArray(); } } @@ -67,7 +67,7 @@ namespace Barotrauma private set { rawAfflictionTypeString = value; - ParseAfflictionTypes(); + parsedAfflictionTypes = rawAfflictionTypeString.ToIdentifiers().ToImmutableArray(); } } @@ -119,30 +119,6 @@ namespace Barotrauma } } - private void ParseAfflictionTypes() - { - if (string.IsNullOrWhiteSpace(rawAfflictionTypeString)) - { - parsedAfflictionTypes = Enumerable.Empty().ToImmutableArray(); - return; - } - - parsedAfflictionTypes = rawAfflictionTypeString.Split(',', ',') - .Select(s => s.Trim()).ToIdentifiers().ToImmutableArray(); - } - - private void ParseAfflictionIdentifiers() - { - if (string.IsNullOrWhiteSpace(rawAfflictionIdentifierString)) - { - parsedAfflictionIdentifiers = Enumerable.Empty().ToImmutableArray(); - return; - } - - parsedAfflictionIdentifiers = rawAfflictionIdentifierString.Split(',', ',') - .Select(s => s.Trim()).ToIdentifiers().ToImmutableArray(); - } - public bool MatchesAfflictionIdentifier(string identifier) => MatchesAfflictionIdentifier(identifier.ToIdentifier()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 925d30006..baf34fdaa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -94,6 +94,9 @@ namespace Barotrauma } } + [Serialize(false, IsPropertySaveable.No, description: "If enabled, the NPC will not spawn if the specified spawn point tags can't be found.")] + public bool RequireSpawnPointTag { get; protected set; } + [Serialize(CampaignMode.InteractionType.None, IsPropertySaveable.No)] public CampaignMode.InteractionType CampaignInteractionType { get; protected set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 2dff4a2f4..820f18fdb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -21,9 +21,9 @@ namespace Barotrauma public Skill PrimarySkill { get; private set; } - public Job(JobPrefab jobPrefab) : this(jobPrefab, randSync: Rand.RandSync.Unsynced, variant: 0) { } + public Job(JobPrefab jobPrefab, bool isPvP) : this(jobPrefab, isPvP, randSync: Rand.RandSync.Unsynced, variant: 0) { } - public Job(JobPrefab jobPrefab, Rand.RandSync randSync, int variant, params Skill[] s) + public Job(JobPrefab jobPrefab, bool isPvP, Rand.RandSync randSync, int variant, params Skill[] s) { prefab = jobPrefab; Variant = variant; @@ -40,7 +40,7 @@ namespace Barotrauma } else { - skill = new Skill(skillPrefab, randSync); + skill = new Skill(skillPrefab, isPvP, randSync); skills.Add(skillPrefab.Identifier, skill); } if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; } @@ -74,11 +74,11 @@ namespace Barotrauma } } - public static Job Random(Rand.RandSync randSync) + public static Job Random(bool isPvP, Rand.RandSync randSync) { var prefab = JobPrefab.Random(randSync); - var variant = Rand.Range(0, prefab.Variants, randSync); - return new Job(prefab, randSync, variant); + int variant = Rand.Range(0, prefab.Variants, randSync); + return new Job(prefab, isPvP, randSync, variant); } public IEnumerable GetSkills() @@ -128,13 +128,18 @@ namespace Barotrauma } } - public void GiveJobItems(Character character, WayPoint spawnPoint = null) + public void GiveJobItems(Character character, bool isPvPMode, WayPoint spawnPoint = null) { - if (!prefab.ItemSets.TryGetValue(Variant, out var spawnItems)) { return; } + if (!prefab.JobItems.TryGetValue(Variant, out var spawnItems)) { return; } - foreach (XElement itemElement in spawnItems.GetChildElements("Item")) + foreach (JobPrefab.JobItem jobItem in spawnItems) { - InitializeJobItem(character, itemElement, spawnPoint); + //spawn the "root items" here, InitializeJobItem goes through the children recursively + if (jobItem.ParentItem != null) { continue; } + for (int i = 0; i < jobItem.Amount; i++) + { + InitializeJobItem(character, isPvPMode, jobItem, spawnItems, spawnPoint); + } } if (GameMain.GameSession is { TraitorsEnabled: true } && character.IsSecurity) @@ -144,29 +149,14 @@ namespace Barotrauma } } - private void InitializeJobItem(Character character, XElement itemElement, WayPoint spawnPoint = null, Item parentItem = null) + private void InitializeJobItem(Character character, bool isPvPMode, JobPrefab.JobItem jobItem, IEnumerable allJobItems, WayPoint spawnPoint = null, Item parentItem = null) { - ItemPrefab itemPrefab; - if (itemElement.Attribute("name") != null) + Identifier itemIdentifier = jobItem.GetItemIdentifier(character.TeamID, isPvPMode); + if (itemIdentifier.IsEmpty) { return; } + if ((MapEntityPrefab.FindByIdentifier(itemIdentifier) ?? MapEntityPrefab.FindByName(itemIdentifier.Value)) is not ItemPrefab itemPrefab) { - string itemName = itemElement.Attribute("name").Value; - DebugConsole.ThrowErrorLocalized("Error in Job config (" + Name + ") - use item identifiers instead of names to configure the items."); - itemPrefab = MapEntityPrefab.FindByName(itemName) as ItemPrefab; - if (itemPrefab == null) - { - DebugConsole.ThrowErrorLocalized("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found."); - return; - } - } - else - { - string itemIdentifier = itemElement.GetAttributeString("identifier", ""); - itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; - if (itemPrefab == null) - { - DebugConsole.ThrowErrorLocalized("Tried to spawn \"" + Name + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); - return; - } + DebugConsole.ThrowErrorLocalized($"Tried to spawn \"{Name}\" with the item \"{itemIdentifier}\". Matching item prefab not found."); + return; } Item item = new Item(itemPrefab, character.Position, null); @@ -187,7 +177,7 @@ namespace Barotrauma } #endif - if (itemElement.GetAttributeBool("equip", false)) + if (jobItem.Equip) { //if the item is both pickable and wearable, try to wear it instead of picking it up List allowedSlots = @@ -229,12 +219,18 @@ namespace Barotrauma wifiComponent.TeamID = character.TeamID; } - if (parentItem != null) { parentItem.Combine(item, user: null); } + parentItem?.Combine(item, user: null); - foreach (XElement childItemElement in itemElement.Elements()) + foreach (JobPrefab.JobItem childItem in allJobItems) { - InitializeJobItem(character, childItemElement, spawnPoint, item); - } + if (childItem.ParentItem == jobItem) + { + for (int i = 0; i < childItem.Amount; i++) + { + InitializeJobItem(character, isPvPMode, childItem, allJobItems, spawnPoint, parentItem: item); + } + } + } } public XElement Save(XElement parentElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index a01454ba2..a5f000c10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -95,20 +95,59 @@ namespace Barotrauma } } - public class PreviewItem + public class JobItem { - public readonly Identifier ItemIdentifier; - public readonly bool ShowPreview; - - public PreviewItem(Identifier itemIdentifier, bool showPreview) + public enum GameModeType { - ItemIdentifier = itemIdentifier; - ShowPreview = showPreview; + Any, PvP, PvE + } + + public readonly Identifier ItemIdentifier; + public readonly Identifier ItemIdentifierTeam2; + public readonly bool ShowPreview; + public readonly bool Equip; + public readonly bool Outfit; + public readonly int Amount = 1; + + public readonly JobItem ParentItem; + + public readonly GameModeType GameMode; + + public JobItem(ContentXElement element, JobItem parentItem) + { + ItemIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + ItemIdentifierTeam2 = element.GetAttributeIdentifier("identifierteam2", Identifier.Empty); + ShowPreview = element.GetAttributeBool("showpreview", true); + GameMode = element.GetAttributeEnum("gamemode", parentItem?.GameMode ?? GameModeType.Any); + Amount = element.GetAttributeInt("amount", 1); + Equip = element.GetAttributeBool("equip", false); + Outfit = element.GetAttributeBool("outfit", false); + ParentItem = parentItem; + } + + public Identifier GetItemIdentifier(CharacterTeamType team, bool isPvPMode) + { + switch (GameMode) + { + case GameModeType.PvP: + if (!isPvPMode) { return Identifier.Empty; } + break; + case GameModeType.PvE: + if (isPvPMode) { return Identifier.Empty; } + break; + } + + return + team == CharacterTeamType.Team2 && !ItemIdentifierTeam2.IsEmpty ? + ItemIdentifierTeam2 : + ItemIdentifier; } } - public readonly Dictionary ItemSets = new Dictionary(); - public readonly ImmutableDictionary> PreviewItems; + /// + /// The items the character can get when spawning. The key is the index of the job variant. + /// + public readonly ImmutableDictionary> JobItems; public readonly List Skills = new List(); public readonly List AutonomousObjectives = new List(); public readonly List AppropriateOrders = new List(); @@ -211,7 +250,7 @@ namespace Barotrauma Description = TextManager.Get("JobDescription." + Identifier); Element = element; - var previewItems = new Dictionary>(); + var jobItems = new Dictionary>(); int variant = 0; foreach (var subElement in element.Elements()) @@ -219,9 +258,8 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "itemset": - ItemSets.Add(variant, subElement); - previewItems[variant] = new List(); - loadItemIdentifiers(subElement, variant); + jobItems[variant] = new List(); + loadJobItems(subElement, variant, parentItem: null); variant++; break; case "skills": @@ -246,35 +284,39 @@ namespace Barotrauma } } - void loadItemIdentifiers(XElement parentElement, int variant) + void loadJobItems(ContentXElement parentElement, int variant, JobItem parentItem) { - foreach (XElement itemElement in parentElement.GetChildElements("Item")) + foreach (ContentXElement itemElement in parentElement.GetChildElements("Item")) { - if (itemElement.Element("name") != null) + if (itemElement.GetAttribute("name") != null) { - DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items."); + DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items.", + contentPackage: parentElement.ContentPackage); continue; } Identifier itemIdentifier = itemElement.GetAttributeIdentifier("identifier", Identifier.Empty); + JobItem jobItem = null; if (itemIdentifier.IsEmpty) { - DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - item with no identifier."); + DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - item with no identifier.", + contentPackage: parentElement.ContentPackage); } else { - previewItems[variant].Add(new PreviewItem(itemIdentifier, itemElement.GetAttributeBool("showpreview", true))); + jobItem = new JobItem(itemElement, parentItem); + jobItems[variant].Add(jobItem); } - loadItemIdentifiers(itemElement, variant); + loadJobItems(itemElement, variant, parentItem: jobItem); } } - PreviewItems = previewItems.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())) + JobItems = jobItems.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())) .ToImmutableDictionary(); Variants = variant; - Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start)); + Skills.Sort((x,y) => y.GetLevelRange(isPvP: false).Start.CompareTo(x.GetLevelRange(isPvP: false).Start)); } public static JobPrefab Random(Rand.RandSync sync, Func predicate = null) => Prefabs.GetRandom(p => !p.HiddenJob && (predicate == null || predicate(p)), sync); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index ff8c4ecaa..4fe173b6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -40,10 +40,12 @@ namespace Barotrauma public readonly float PriceMultiplier = 1.0f; - public Skill(SkillPrefab prefab, Rand.RandSync randSync) + public Skill(SkillPrefab prefab, bool isPvP, Rand.RandSync randSync) { Identifier = prefab.Identifier; - Level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync); + + var levelRange = prefab.GetLevelRange(isPvP); + Level = Rand.Range(levelRange.Start, levelRange.End, randSync); iconJobId = GetIconJobId(); PriceMultiplier = prefab.PriceMultiplier; DisplayName = TextManager.Get("SkillName." + Identifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs index 3a8ed2a6f..cdf076761 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs @@ -1,5 +1,4 @@ -using Microsoft.Xna.Framework; -using System.Xml.Linq; +using System.Globalization; namespace Barotrauma { @@ -7,7 +6,8 @@ namespace Barotrauma { public readonly Identifier Identifier; - public Range LevelRange { get; private set; } + private readonly Range levelRange; + private readonly Range levelRangePvP; /// /// How much this skill affects characters' hiring cost @@ -20,19 +20,32 @@ namespace Barotrauma { Identifier = element.GetAttributeIdentifier("identifier", ""); PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 25.0f); - var levelString = element.GetAttributeString("level", ""); - if (levelString.Contains(",")) - { - var rangeVector2 = XMLExtensions.ParseVector2(levelString, false); - LevelRange = new Range(rangeVector2.X, rangeVector2.Y); - } - else - { - float skillLevel = float.Parse(levelString, System.Globalization.CultureInfo.InvariantCulture); - LevelRange = new Range(skillLevel, skillLevel); - } - + levelRange = GetSkillRange("level", element, defaultValue: new Range(0, 0)); + levelRangePvP = GetSkillRange("pvplevel", element, defaultValue: levelRange); IsPrimarySkill = element.GetAttributeBool("primary", false); + + static Range GetSkillRange(string attributeName, ContentXElement element, Range defaultValue) + { + string levelString = element.GetAttributeString(attributeName, string.Empty); + if (levelString.Contains(',')) + { + var rangeVector2 = XMLExtensions.ParseVector2(levelString, false); + return new Range(rangeVector2.X, rangeVector2.Y); + } + else if (float.TryParse(levelString, NumberStyles.Any, CultureInfo.InvariantCulture, out float skillLevel)) + { + return new Range(skillLevel, skillLevel); + } + else + { + return defaultValue; + } + } + } + + public Range GetLevelRange(bool isPvP) + { + return isPvP ? levelRangePvP : levelRange; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index b6de960f5..4bcf6b50c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -258,10 +258,7 @@ namespace Barotrauma { get { - if (!mouthPos.HasValue) - { - mouthPos = Params.MouthPos; - } + mouthPos ??= Params.MouthPos; return mouthPos.Value; } set @@ -366,7 +363,7 @@ namespace Barotrauma if (isSevered) { ragdoll.SubtractMass(this); - if (type == LimbType.Head) + if (type == LimbType.Head && character.Params.Health.DieFromBeheading) { character.Kill(CauseOfDeathType.Unknown, null); } @@ -386,10 +383,18 @@ namespace Barotrauma public Submarine Submarine => character?.Submarine; + private bool _hidden; public bool Hidden { - get => Params.Hide; - set => Params.Hide = value; + get => _hidden || Params.Hide; + set => _hidden = value; + } + + // Just a wrapper for Hidden, but both can be used via status effects, so it's not safe to remove it. + public bool Hide + { + get => Hidden; + set => Hidden = value; } public Vector2 WorldPosition @@ -636,7 +641,7 @@ namespace Barotrauma //if (character.Params.CanInteract) { return false; } if (this == character.AnimController.MainLimb) { return false; } bool canBeSevered = Params.CanBeSeveredAlive; - if (character.AnimController.CanWalk) + if (character.AnimController.CanWalk && !character.Params.Health.AllowSeveringLegs) { switch (type) { @@ -671,7 +676,7 @@ namespace Barotrauma this.character = character; this.Params = limbParams; dir = Direction.Right; - body = new PhysicsBody(limbParams); + body = new PhysicsBody(limbParams, findNewContacts: false); type = limbParams.Type; IgnoreCollisions = limbParams.IgnoreCollisions; body.UserData = this; @@ -937,7 +942,7 @@ namespace Barotrauma severedFadeOutTimer = SeveredFadeOutTime; } } - else if (!IsDead) + else if (!IsDead && (character.IsPlayer || character.AIState is not AIState.PlayDead)) { if (Params.BlinkFrequency > 0) { @@ -989,6 +994,7 @@ namespace Barotrauma public void ReEnable() { if (!temporarilyDisabled) { return; } + temporarilyDisabled = false; Hidden = false; Disabled = false; IgnoreCollisions = originalIgnoreCollisions; @@ -1008,6 +1014,8 @@ namespace Barotrauma float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos)); bool wasRunning = attack.IsRunning; attack.UpdateAttackTimer(deltaTime, character); + attack.DamageMultiplier = 1.0f + character.GetStatValue(attack.Ranged ? StatTypes.NaturalRangedAttackMultiplier : StatTypes.NaturalMeleeAttackMultiplier); + if (attack.Blink) { if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Any()) @@ -1434,6 +1442,7 @@ namespace Barotrauma public void Remove() { + ragdoll.SubtractMass(this); body?.Remove(); body = null; if (pullJoint != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 83f685fbd..2404f0b57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -44,9 +44,6 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)] public float StepLiftAmount { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] - public bool MultiplyByDir { get; set; } - [Serialize(0.5f, IsPropertySaveable.Yes, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)] public float StepLiftOffset { get; set; } @@ -56,6 +53,48 @@ namespace Barotrauma [Header("Movement")] [Serialize(0.75f, IsPropertySaveable.Yes, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)] public float BackwardsMovementMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Adjusts the maximum speed while climbing. The actual speed is affected by the MovementSpeed."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10f, DecimalCount = 2)] + public float ClimbSpeed { get; set; } + + [Serialize(2.0f, IsPropertySaveable.Yes, description: "Used instead of ClimbSpeed when descending ladders while moving fast (running). Not used if lower than ClimbSpeed."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10f, DecimalCount = 2)] + public float SlideSpeed { get; set; } + + [Serialize(10.5f, IsPropertySaveable.Yes, description: "Force applied to the main collider, torso and head, when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)] + public float ClimbBodyMoveForce { get; set; } + + [Serialize(5.2f, IsPropertySaveable.Yes, description: "Force applied to the hands when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)] + public float ClimbHandMoveForce { get; set; } + + [Serialize(10.0f, IsPropertySaveable.Yes, description: "Force applied to the feet when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)] + public float ClimbFootMoveForce { get; set; } + + [Serialize(30.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)] + public float ClimbStepHeight { get; set; } + + protected override bool Deserialize(XElement element = null) + { + if (element.GetAttributeEnum(nameof(AnimationType), AnimationType.NotDefined) is AnimationType.Run) + { + // These values were previously hard-coded when running, so we need to set different default values for the run animations, when they are not defined. + const string climbSpeedName = nameof(ClimbSpeed); + if (element.GetAttribute(climbSpeedName) == null) + { + element.SetAttribute(climbSpeedName, 2.0f); + } + const string climbStepName = nameof(ClimbStepHeight); + if (element.GetAttribute(climbStepName) == null) + { + element.SetAttribute(climbStepName, 60.0f); + } + const string slideSpeedName = nameof(SlideSpeed); + if (element.GetAttribute(slideSpeedName) == null) + { + element.SetAttribute(slideSpeedName, 4.0f); + } + } + return base.Deserialize(element); + } } abstract class SwimParams : AnimationParams @@ -92,7 +131,7 @@ namespace Barotrauma /// /// In degrees. /// - [Header("Standing")] + [Header("Orientation")] [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float HeadAngle { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 7a8544a75..3dd5f9a7a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -19,6 +19,16 @@ namespace Barotrauma { [Serialize("", IsPropertySaveable.Yes), Editable] public Identifier SpeciesName { get; private set; } + + [Serialize("", IsPropertySaveable.Yes), Editable] + public string Tags + { + get => tags.ConvertToString(); + set => tags = value.ToIdentifiers().ToHashSet(); + } + private HashSet tags = new HashSet(); + + public bool HasTag(Identifier tag) => tags.Contains(tag); [Serialize("", IsPropertySaveable.Yes, description: "References to another species. Define only if the creature is a variant that needs to use a pre-existing translation."), Editable] public Identifier SpeciesTranslationOverride { get; private set; } @@ -37,9 +47,21 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature interact with items?"), Editable] public bool CanInteract { get; private set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Can the creature use ladders? Doesn't have an effect, if CanInteract is false."), Editable] + public bool CanClimb { get; private set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "If set true, this character only uses the climbing parameters defined in the walk parameters (not run)."), Editable] + public bool ForceSlowClimbing { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description: "Should this character be treated as a husk?"), Editable] public bool Husk { get; private set; } + + [Serialize("", IsPropertySaveable.Yes, description: "If this character can turn into a husk, which character it turns to? If not defined, uses the default pattern (e.g. Crawler -> Crawlerhusk, Human -> Humanhusk)."), Editable] + public Identifier HuskedSpecies { get; private set; } + + [Serialize("", IsPropertySaveable.Yes, description: "If this character is a husk, from what species it can be turned into? If not defined, uses the default pattern (e.g. Crawlerhusk -> Crawler, Humanhusk -> Human)."), Editable] + public Identifier NonHuskedSpecies { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description:"Should this character use a special husk appendage, attached to the ragdoll, when it turns into a husk?"), Editable] public bool UseHuskAppendage { get; private set; } @@ -161,7 +183,7 @@ namespace Barotrauma } } - public static XElement CreateVariantXml(XElement variantXML, XElement baseXML) + public static XElement CreateVariantXml(ContentXElement variantXML, ContentXElement baseXML) { XElement newXml = variantXML.CreateVariantXML(baseXML); XElement variantAi = variantXML.GetChildElement("ai"); @@ -433,17 +455,11 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Which tags are required for this sound to play?"), Editable()] public string Tags { - get { return string.Join(',', TagSet); } - private set - { - TagSet = value.Split(',') - .ToIdentifiers() - .Where(id => !id.IsEmpty) - .ToImmutableHashSet(); - } + get => TagSet.ConvertToString(); + private set => TagSet = value.ToIdentifiers().ToImmutableHashSet(); } - public ImmutableHashSet TagSet { get; private set; } + public ImmutableHashSet TagSet { get; private set; } = ImmutableHashSet.Empty; public SoundParams(ContentXElement element, CharacterParams character) : base(element, character) { @@ -549,6 +565,15 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes), Editable] public float EmpVulnerability { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Apply movement penalties when legs or tail limbs get damaged. Enabled by default."), Editable] + public bool ApplyMovementPenalties { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Normally characters die when they don't have a head. But maybe not all of them?"), Editable] + public bool DieFromBeheading { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Severing legs doesn't work with most characters, because we'd need to take that into account with the walking animations and the standing position of the main collider etc. But there might be cases where you'll want to override this default."), Editable] + public bool AllowSeveringLegs { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable] public bool ApplyAfflictionColors { get; private set; } @@ -719,34 +744,47 @@ namespace Barotrauma [Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: "Defines the method of checking whether there's a blocking (submarine) wall."), Editable] public WallTargetingMethod WallTargetingMethod { get; private set; } + + [Serialize(0f, IsPropertySaveable.Yes, "How likely it is that the creature plays dead (= ragdolls) while idling? Only allowed inside a sub (not in the open waters). Evaluated once, when the creature spawns."), Editable] + public float PlayDeadProbability { get; set; } public IEnumerable Targets => targets; - protected readonly List targets = new List(); + private readonly List targets = new List(); public AIParams(ContentXElement element, CharacterParams character) : base(element, character) { if (element == null) { return; } - element.GetChildElements("target").ForEach(t => TryAddTarget(t, out _)); - element.GetChildElements("targetpriority").ForEach(t => TryAddTarget(t, out _)); + element.GetChildElements("target").ForEach(t => AddTarget(t)); + element.GetChildElements("targetpriority").ForEach(t => AddTarget(t)); } + /// + /// Adds a target but checks for duplicates first. Doesn't allow adding multiple targets with the same tag (see ). + /// private bool TryAddTarget(ContentXElement targetElement, out TargetParams target) { string tag = targetElement.GetAttributeString("tag", null); if (HasTag(tag)) { target = null; - DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!", - targetElement.ContentPackage); - return false; + DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!", targetElement.ContentPackage); } else { - target = new TargetParams(targetElement, Character); - targets.Add(target); - SubParams.Add(target); - return true; + target = AddTarget(targetElement); } + return target != null; + } + + /// + /// This method allows adding multiple targets with the same tag. + /// + private TargetParams AddTarget(ContentXElement targetElement) + { + var target = new TargetParams(targetElement, Character); + targets.Add(target); + SubParams.Add(target); + return target; } public bool TryAddEmptyTarget(out TargetParams targetParams) => TryAddNewTarget("newtarget" + targets.Count, AIState.Attack, 0f, out targetParams); @@ -782,26 +820,40 @@ namespace Barotrauma } public bool RemoveTarget(TargetParams target) => RemoveSubParam(target, targets); - - public bool TryGetTarget(string targetTag, out TargetParams target) - => TryGetTarget(targetTag.ToIdentifier(), out target); - public bool TryGetTarget(Identifier targetTag, out TargetParams target) + public IEnumerable GetMatchingTargets(Func predicate) => targets.Where(predicate); + public IEnumerable GetTargets(Identifier target) => GetMatchingTargets(t => t.Tag == target); + public IEnumerable GetTargets(Character target) => GetMatchingTargets(t => t.Tag == target.SpeciesName || t.Tag == target.Params.Group || target.Params.HasTag(t.Tag)); + public TargetParams GetHighestPriorityTarget(Identifier target) => GetHighestPriorityTarget(GetTargets(target)); + public TargetParams GetHighestPriorityTarget(Character target) => GetHighestPriorityTarget(GetTargets(target)); + + private static TargetParams GetHighestPriorityTarget(IEnumerable targetParams) => targetParams.MaxBy(static t => t.Priority); + + public bool TryGetTargets(Identifier target, out IEnumerable targetParams) { - target = targets.FirstOrDefault(t => t.Tag == targetTag); - return target != null; + targetParams = GetTargets(target); + return targetParams.Any(); + } + + public bool TryGetTargets(Character target, out IEnumerable targetParams) + { + targetParams = GetTargets(target); + return targetParams.Any(); + } + + public bool TryGetHighestPriorityTarget(Identifier target, out TargetParams targetParams) + { + targetParams = GetHighestPriorityTarget(target); + return targetParams != null; + } + + public bool TryGetHighestPriorityTarget(Character target, out TargetParams targetParams) + { + targetParams = GetHighestPriorityTarget(target); + return targetParams != null; } - public bool TryGetTarget(Character targetCharacter, out TargetParams target) - { - if (!TryGetTarget(targetCharacter.SpeciesName, out target)) - { - target = targets.FirstOrDefault(t => t.Tag == targetCharacter.Params.Group); - } - return target != null; - } - - public bool TryGetTarget(IEnumerable tags, out TargetParams target) + public bool TryGetHighestPriorityTarget(IEnumerable tags, out TargetParams target) { target = null; if (tags == null || tags.None()) { return false; } @@ -819,22 +871,6 @@ namespace Barotrauma } return target != null; } - - public TargetParams GetTarget(string targetTag, bool throwError = true) - => GetTarget(targetTag.ToIdentifier(), throwError); - - public TargetParams GetTarget(Identifier targetTag, bool throwError = true) - { - if (targetTag.IsEmpty) { return null; } - if (!TryGetTarget(targetTag, out TargetParams target)) - { - if (throwError) - { - DebugConsole.ThrowError($"Cannot find a target with the tag {targetTag}!"); - } - } - return target; - } } public class TargetParams : SubParam @@ -889,7 +925,7 @@ namespace Barotrauma [Serialize(-1f, IsPropertySaveable.Yes, description: "A generic max threshold. Not used if set to negative."), Editable] public float ThresholdMax { get; private set; } - [Serialize(1.0f, IsPropertySaveable.Yes, description: "Can be used to make the monster perceive the target further than it normally can."), Editable] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Can be used to make the monster perceive the target further or closer than it normally can."), Editable] public float PerceptionDistanceMultiplier { get; private set; } [Serialize(-1.0f, IsPropertySaveable.Yes, description: "Maximum distance at which the monster can perceive the target, regardless of the sight/hearing or how visible or how much noise the target is making. Not used if set to negative."), Editable] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index e19fb68a5..17d2210a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -113,7 +113,7 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes), Editable] public bool CanWalk { get; set; } - + [Serialize(true, IsPropertySaveable.Yes, description: "Can the character be dragged around by other creatures?"), Editable()] public bool Draggable { get; set; } @@ -654,7 +654,7 @@ namespace Barotrauma [Serialize(0.25f, IsPropertySaveable.Yes), Editable] public float Stiffness { get; set; } - [Serialize(1f, IsPropertySaveable.Yes, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable] + [Serialize(1f, IsPropertySaveable.Yes, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable(DecimalCount = 2)] public float Scale { get; set; } [Serialize(false, IsPropertySaveable.No), Editable(ReadOnly = true)] @@ -705,6 +705,9 @@ namespace Barotrauma [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()] public LimbType Type { get; set; } + + [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "Secondary limb type to be used for generic purposes. Currently only used in climbing animations."), Editable()] + public LimbType SecondaryType { get; set; } /// /// The orientation of the sprite as drawn on the sprite sheet (in radians). @@ -775,6 +778,12 @@ namespace Barotrauma [Serialize("0, 0", IsPropertySaveable.Yes, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)] public Vector2 MouthPos { get; set; } + + [Serialize(50f, IsPropertySaveable.Yes, description: "How much torque is applied on the head while updating the eating animations?"), Editable] + public float EatTorque { get; set; } + + [Serialize(2f, IsPropertySaveable.Yes, description: "How strong a linear impulse is applied on the head while updating the eating animations?"), Editable] + public float EatImpulse { get; set; } [Serialize(0f, IsPropertySaveable.Yes), Editable] public float ConstantTorque { get; set; } @@ -795,8 +804,11 @@ namespace Barotrauma [Serialize(10f, IsPropertySaveable.Yes, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)] public float SeveredFadeOutTime { get; set; } = 10.0f; - [Serialize(false, IsPropertySaveable.Yes, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the tail angle be applied on this limb? If none of the limbs have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable] public bool ApplyTailAngle { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Should this limb be moved like a tail when swimming? Always true for tail limbs. On tails, disable by setting SineFrequencyMultiplier to 0."), Editable] + public bool ApplySineMovement { get; set; } [Serialize(1f, IsPropertySaveable.Yes), Editable(ValueStep = 0.1f, DecimalCount = 2)] public float SineFrequencyMultiplier { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index 71ae4f7dd..c374bfe75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -7,31 +6,13 @@ namespace Barotrauma.Abilities { class AbilityConditionMission : AbilityConditionData { - private readonly ImmutableHashSet missionType; + private readonly ImmutableHashSet missionType; private readonly bool isAffiliated; public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!; - HashSet missionTypes = new HashSet(); + missionType = conditionElement.GetAttributeIdentifierImmutableHashSet("missiontype", ImmutableHashSet.Empty)!; isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false); - - foreach (string missionTypeString in missionTypeStrings) - { - if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None) - { - if (!isAffiliated) - { - DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.", - contentPackage: conditionElement.ContentPackage); - } - continue; - } - - missionTypes.Add(parsedMission); - } - - missionType = missionTypes.ToImmutableHashSet(); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs index ede7abe7a..23fe93b3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific() { - foreach (Character c in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + foreach (Character c in Character.GetFriendlyCrew(character)) { if (!c.IsDead && c.IsUnconscious) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs index 865384e7b..0bea1e7a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs @@ -1,18 +1,13 @@ -using Barotrauma.Items.Components; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionHasSkill : AbilityConditionDataless { - private readonly string skillIdentifier; + private readonly Identifier skillIdentifier; private readonly float minValue; public AbilityConditionHasSkill(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - skillIdentifier = conditionElement.GetAttributeString("skillidentifier", string.Empty); + skillIdentifier = conditionElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty); minValue = conditionElement.GetAttributeFloat("minvalue", 0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs index fb6749419..adda5b464 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities protected override bool MatchesCharacter(Character character) { int ownLevel = character.Info.GetCurrentLevel(); - foreach (Character otherCharacter in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + foreach (Character otherCharacter in Character.GetFriendlyCrew(character)) { if (otherCharacter == character) { continue; } if (otherCharacter.Info.GetCurrentLevel() < ownLevel) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs index acf06d837..41c0b04f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs @@ -13,13 +13,15 @@ { if (GameMain.GameSession == null) { return false; } - foreach (Character character in GameMain.GameSession.Casualties) + foreach (Character deadCharacter in GameMain.GameSession.Casualties) { - if (assistantsDontCount && character.Info?.Job?.Prefab.Identifier == "assistant") + if (deadCharacter.TeamID != character.TeamID) { continue; } + + if (assistantsDontCount && deadCharacter.Info?.Job?.Prefab.Identifier == "assistant") { continue; } - if (character.CauseOfDeath != null && character.CauseOfDeath.Type != CauseOfDeathType.Disconnected) + if (deadCharacter.CauseOfDeath != null && deadCharacter.CauseOfDeath.Type != CauseOfDeathType.Disconnected) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs index a8d33433a..d88f2651d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs @@ -14,7 +14,8 @@ float waterVolume = 0.0f, totalVolume = 0.0f; foreach (Hull hull in Hull.HullList) { - if (hull.Submarine != character.Submarine) { continue; } + if (hull.Submarine is not { } hullSubmarine) { continue; } + if (hullSubmarine != character.Submarine || hullSubmarine.TeamID != character.TeamID) { continue; } waterVolume += hull.WaterVolume; totalVolume += hull.Volume; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs index 453ad0d09..c5440d730 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs @@ -26,7 +26,7 @@ namespace Barotrauma.Abilities return; } - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + foreach (Character character in Character.GetFriendlyCrew(Character)) { JobPrefab? characterJob = character.Info?.Job?.Prefab; if (characterJob is null) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs index 1eba6a358..36d5ac73b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs @@ -4,14 +4,14 @@ { private readonly Identifier afflictionId; private readonly float strength; - private readonly string multiplyStrengthBySkill; + private readonly Identifier multiplyStrengthBySkill; private readonly bool setValue; public CharacterAbilityGiveAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { afflictionId = abilityElement.GetAttributeIdentifier("afflictionid", abilityElement.GetAttributeIdentifier("affliction", Identifier.Empty)); strength = abilityElement.GetAttributeFloat("strength", 0f); - multiplyStrengthBySkill = abilityElement.GetAttributeString("multiplystrengthbyskill", string.Empty); + multiplyStrengthBySkill = abilityElement.GetAttributeIdentifier("multiplystrengthbyskill", Identifier.Empty); setValue = abilityElement.GetAttributeBool("setvalue", false); if (afflictionId.IsEmpty) @@ -52,7 +52,7 @@ return; } float strength = this.strength; - if (!string.IsNullOrEmpty(multiplyStrengthBySkill)) + if (!multiplyStrengthBySkill.IsEmpty) { strength *= Character.GetSkillLevel(multiplyStrengthBySkill); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs index f3f2d91cc..332303a34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs @@ -20,7 +20,7 @@ namespace Barotrauma.Abilities { if (!addingFirstTime) { return; } - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + foreach (Character character in Character.GetFriendlyCrew(Character)) { if (character.Info is null) { return; } character.Info.AdditionalTalentPoints += amount; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs index b02b85e1d..2293f2759 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities { private readonly StatTypes statType; private readonly float maxValue; - private readonly string skillIdentifier; + private readonly Identifier skillIdentifier; private readonly bool useAll; private float lastValue = 0f; public override bool AllowClientSimulation => true; @@ -15,7 +15,7 @@ namespace Barotrauma.Abilities { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); - skillIdentifier = abilityElement.GetAttributeString("skillidentifier", string.Empty); + skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty); useAll = skillIdentifier == "all"; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs index 4168ec6a1..cb3e5d1da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs @@ -5,6 +5,8 @@ private readonly float addedValue; private readonly float multiplyValue; + public override bool AllowClientSimulation => true; + public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUpgradeSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUpgradeSubmarine.cs index a7387e414..4af108dd0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUpgradeSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUpgradeSubmarine.cs @@ -6,6 +6,7 @@ internal class CharacterAbilityUpgradeSubmarine : CharacterAbility private readonly UpgradePrefab? upgradePrefab; private readonly UpgradeCategory? upgradeCategory; public readonly int level; + private readonly bool giveOnAddingFirstTime; public override bool AllowClientSimulation => true; @@ -13,7 +14,8 @@ internal class CharacterAbilityUpgradeSubmarine : CharacterAbility { var prefabIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradePrefab), Identifier.Empty); var categoryIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradeCategory), Identifier.Empty); - + giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None); + if (UpgradePrefab.Find(prefabIdentifier) is not { } foundUpgradePrefab) { DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityUpgradeSubmarine)} - {nameof(upgradePrefab)} not found.", @@ -47,6 +49,14 @@ internal class CharacterAbilityUpgradeSubmarine : CharacterAbility ApplyEffectSpecific(); } + public override void InitializeAbility(bool addingFirstTime) + { + if (addingFirstTime && giveOnAddingFirstTime) + { + ApplyEffectSpecific(); + } + } + private void ApplyEffectSpecific() { if (upgradePrefab == null || upgradeCategory == null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs index b17432528..a2ad09049 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs @@ -24,7 +24,11 @@ namespace Barotrauma.Abilities foreach (Character enemyCharacter in enemyCharacters) { if (!enemyCharacter.IsHuman) { continue; } - if (enemyCharacter.Submarine == null || enemyCharacter.Submarine != Submarine.MainSub) { continue; } + if (enemyCharacter.Submarine == null || + (Submarine.MainSub != null && enemyCharacter.Submarine != Submarine.MainSub)) + { + continue; + } if (enemyCharacter.IsDead) { continue; } if (!enemyCharacter.LockHands) { continue; } Character.GiveMoney(moneyAmount); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs index 7ce696992..df658dfa7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using Barotrauma.Extensions; @@ -31,7 +31,7 @@ namespace Barotrauma.Abilities if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; } - ImmutableHashSet characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + var characters = Character.GetFriendlyCrew(Character); HashSet> talentsTrees = new HashSet>(); foreach (TalentSubTree subTree in talentTree.TalentSubTrees) @@ -60,13 +60,14 @@ namespace Barotrauma.Abilities talentsTrees.Add(identifiers.ToImmutableHashSet()); } - ImmutableHashSet selectedTalentTree = talentsTrees.GetRandomUnsynced(); - - foreach (Identifier identifier in selectedTalentTree) + ImmutableHashSet? selectedTalentTree = talentsTrees.GetRandomUnsynced(); + if (selectedTalentTree != null) { - if (Character.HasTalent(identifier)) { continue; } - - Character.GiveTalent(identifier); + foreach (Identifier identifier in selectedTalentTree) + { + if (Character.HasTalent(identifier)) { continue; } + Character.GiveTalent(identifier); + } } static bool IsShowCaseTalent(Identifier identifier, TalentOption option) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityWarStories.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityWarStories.cs index 7348428ae..fe34283b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityWarStories.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityWarStories.cs @@ -12,14 +12,20 @@ internal class CharacterAbilityWarStories : CharacterAbility { private readonly Identifier targetStat; - private readonly float minCondition; + private readonly float normalQualityThreshold; + private readonly float goodQualityThreshold; + private readonly float excellentQualityThreshold; + private readonly float masterworkQualityThreshold; private readonly ItemPrefab prefab; public CharacterAbilityWarStories(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { targetStat = abilityElement.GetAttributeIdentifier("target", Identifier.Empty); - minCondition = abilityElement.GetAttributeFloat("mincondition", 1); + normalQualityThreshold = abilityElement.GetAttributeFloat("normalqualitythreshold", 4); + goodQualityThreshold = abilityElement.GetAttributeFloat("goodqualitythreshold", 10); + excellentQualityThreshold = abilityElement.GetAttributeFloat("excellentqualitythreshold", 20); + masterworkQualityThreshold = abilityElement.GetAttributeFloat("masterworkqualitythreshold", 30); if (targetStat.IsEmpty) { @@ -37,23 +43,28 @@ internal class CharacterAbilityWarStories : CharacterAbility { if (prefab is null || Character is null) { return; } - float condition = Character.Info?.GetSavedStatValue(StatTypes.None, targetStat) ?? 0; - if (condition < minCondition) { return; } + float statValue = Character.Info?.GetSavedStatValue(StatTypes.None, targetStat) ?? 0; + + if (statValue < normalQualityThreshold) { return; } + + int quality = 0; + if (statValue >= masterworkQualityThreshold) { quality = 3; } + else if (statValue >= excellentQualityThreshold) { quality = 2; } + else if (statValue >= goodQualityThreshold) { quality = 1; } if (GameMain.GameSession?.RoundEnding ?? true) { Item item = new(prefab, Character.WorldPosition, Character.Submarine) { - Condition = condition, - HealthMultiplier = condition + Quality = quality, }; Character.Inventory.TryPutItem(item, Character, item.AllowedSlots); } else { - Entity.Spawner?.AddItemToSpawnQueue(prefab, Character.Inventory, condition: condition, onSpawned: item => + Entity.Spawner?.AddItemToSpawnQueue(prefab, Character.Inventory, quality: quality, onSpawned: item => { - item.HealthMultiplier = condition; + item.Quality = quality; }); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index bb06f8717..5af044625 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -131,7 +131,7 @@ namespace Barotrauma if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; } if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } - if (IsTalentLocked(talentIdentifier)) { return false; } + if (IsTalentLocked(talentIdentifier, Character.GetFriendlyCrew(character))) { return false; } if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier)) { @@ -163,10 +163,8 @@ namespace Barotrauma return false; } - public static bool IsTalentLocked(Identifier talentIdentifier, ImmutableHashSet characterList = null) + public static bool IsTalentLocked(Identifier talentIdentifier, IEnumerable characterList) { - characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); - foreach (Character c in characterList) { if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs index 2069fe68f..2e88b0de3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs @@ -1,11 +1,9 @@ +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Reflection; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -55,18 +53,19 @@ namespace Barotrauma return; } - if (AfflictionPrefab.Prefabs.ContainsKey(identifier)) + if (AfflictionPrefab.Prefabs.TryGet(identifier, out var existingAffliction)) { if (overriding) { DebugConsole.NewMessage( - $"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{Path}'", + $"Overriding an affliction or a buff with the identifier '{identifier}' using the version in '{element.ContentPackage.Name}'", Color.MediumPurple); } else { DebugConsole.ThrowError( - $"Duplicate affliction: '{identifier}' defined in {elementName} of '{Path}'", + $"Duplicate affliction: '{identifier}' defined in {element.ContentPackage.Name} is already defined in the previously loaded content package {existingAffliction.ContentPackage.Name}."+ + $" You may need to adjust the mod load order to make sure {element.ContentPackage.Name} is loaded first.", contentPackage: element?.ContentPackage); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs index 96dc4ca72..2222c41c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -86,23 +86,27 @@ namespace Barotrauma if (ragdollParams != null) { - HashSet texturePaths = new HashSet - { - ContentPath.FromRaw(CharacterPrefab.Prefabs[speciesName].ContentPackage, ragdollParams.Texture).Value - }; + HashSet texturePaths = new HashSet(); + AddTexturePath(ragdollParams.Texture); foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs) { - if (!string.IsNullOrEmpty(limb.normalSpriteParams?.Texture)) { texturePaths.Add(limb.normalSpriteParams.Texture); } - if (!string.IsNullOrEmpty(limb.deformSpriteParams?.Texture)) { texturePaths.Add(limb.deformSpriteParams.Texture); } - if (!string.IsNullOrEmpty(limb.damagedSpriteParams?.Texture)) { texturePaths.Add(limb.damagedSpriteParams.Texture); } + AddTexturePath(limb.normalSpriteParams?.Texture); + AddTexturePath(limb.deformSpriteParams?.Texture); + AddTexturePath(limb.damagedSpriteParams?.Texture); foreach (var decorativeSprite in limb.decorativeSpriteParams) { - if (!string.IsNullOrEmpty(decorativeSprite.Texture)) { texturePaths.Add(decorativeSprite.Texture); } + AddTexturePath(decorativeSprite.Texture); } } - foreach (string texturePath in texturePaths) + foreach (ContentPath texturePath in texturePaths) { - addPreloadedSprite(new Sprite(texturePath, Vector2.Zero)); + addPreloadedSprite(new Sprite(texturePath.Value, Vector2.Zero)); + } + + void AddTexturePath(string path) + { + if (string.IsNullOrEmpty(path)) { return; } + texturePaths.Add(ContentPath.FromRaw(characterPrefab.ContentPackage, ragdollParams.Texture)); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/DisembarkPerkFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/DisembarkPerkFile.cs new file mode 100644 index 000000000..728721e3c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/DisembarkPerkFile.cs @@ -0,0 +1,17 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + internal sealed class DisembarkPerkFile : GenericPrefabFile + { + public DisembarkPerkFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "disembarkperk"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "disembarkperks"; + protected override PrefabCollection Prefabs => DisembarkPerkPrefab.Prefabs; + protected override DisembarkPerkPrefab CreatePrefab(ContentXElement element) + { + return new DisembarkPerkPrefab(element, this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index a812a773f..4bf5b73a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Barotrauma.Extensions; using Barotrauma.Steam; using System; @@ -9,6 +9,8 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.Xml.Linq; +using System.Xml; +using Barotrauma.IO; namespace Barotrauma { @@ -34,9 +36,9 @@ namespace Barotrauma public const string FileListFileName = "filelist.xml"; public const string DefaultModVersion = "1.0.0"; - public readonly string Name; + public string Name { get; private set; } public readonly ImmutableArray AltNames; - public readonly string Path; + public string Path { get; private set; } public string Dir => Barotrauma.IO.Path.GetDirectoryName(Path) ?? ""; public readonly Option UgcId; @@ -265,7 +267,7 @@ namespace Barotrauma //The game should be able to work just fine with a completely arbitrary file load order. //To make sure we don't mess this up, debug builds randomize it so it has a higher chance //of breaking anything that's not implemented correctly. - .Randomize() + .Randomize(Rand.RandSync.Unsynced) #endif ; @@ -397,5 +399,51 @@ namespace Barotrauma static string errorToStr(LoadError error) => error.ToString(); } + + public bool TryRenameLocal(string newName) + { + if (!ContentPackageManager.LocalPackages.Contains(this)) { return false; } + + if (newName.IsNullOrWhiteSpace()) + { + DebugConsole.ThrowError($"New name is blank!"); + return false; + } + + string newDir = IO.Path.Combine(IO.Path.GetFullPath(LocalModsDir), File.SanitizeName(newName)); + if (ContentPackageManager.LocalPackages.Any(lp => lp.NameMatches(newName)) || Directory.Exists(newDir)) + { + DebugConsole.ThrowError($"A local package with the name or directory \"{newName}\" already exists!"); + return false; + } + + XDocument doc = XMLExtensions.TryLoadXml(Path); + doc.Root!.SetAttributeValue("name", newName); + using (IO.XmlWriter writer = IO.XmlWriter.Create(Path, new XmlWriterSettings { Indent = true })) + { + doc.WriteTo(writer); + writer.Flush(); + } + + Directory.Move(Dir, newDir); + return true; + } + + public bool TryDeleteLocal() => ContentPackageManager.LocalPackages.Contains(this) && Directory.TryDelete(Dir); + + public bool TryCreateLocalFromWorkshop() + { + if (!ContentPackageManager.WorkshopPackages.Contains(this)) { return false; } + + string newDir = IO.Path.Combine(IO.Path.GetFullPath(LocalModsDir), File.SanitizeName(Name)); + if (ContentPackageManager.LocalPackages.Any(lp => lp.NameMatches(Name)) || Directory.Exists(newDir)) + { + DebugConsole.ThrowError($"A local package with the name or directory \"{Name}\" already exists!"); + return false; + } + + Directory.Copy(Dir, newDir); + return true; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index 7559fe604..b0a296e7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -93,6 +93,7 @@ namespace Barotrauma public Rectangle GetAttributeRect(string key, in Rectangle def) => Element.GetAttributeRect(key, def); public Version GetAttributeVersion(string key, Version def) => Element.GetAttributeVersion(key, def); public T GetAttributeEnum(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def); + public T[] GetAttributeEnumArray(string key, T[] def) where T : struct, Enum => Element.GetAttributeEnumArray(key, def); public (T1, T2) GetAttributeTuple(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def); public (T1, T2)[] GetAttributeTupleArray(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def); public Range GetAttributeRange(string key, in Range def) => Element.GetAttributeRange(key, def); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index d3df8e41c..d9188707d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -249,7 +249,7 @@ namespace Barotrauma GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats; })); - commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)] [add to crew (true/false)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, + commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team] [add to crew (true/false)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, () => { string[] creatureAndJobNames = @@ -262,55 +262,44 @@ namespace Barotrauma { creatureAndJobNames.ToArray(), new string[] { "near", "inside", "outside", "cursor" }, - new string[] { "0", "1", "2", "3" }, + Enum.GetValues().Select(v => v.ToString()).ToArray(), new string[] { "true", "false" }, }; }, isCheat: true)); - - commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the last parameter is omitted or \"random\".", + + commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition]: Spawn an item in the inventory of the controlled character", (string[] args) => { - try + if (Character.Controlled == null) { -#if CLIENT - SpawnItem(args, Screen.Selected.Cam?.ScreenToWorld(PlayerInput.MousePosition) ?? PlayerInput.MousePosition, Character.Controlled, out string errorMsg); -#elif SERVER - SpawnItem(args, Vector2.Zero, null, out string errorMsg); -#endif - if (!string.IsNullOrWhiteSpace(errorMsg)) - { - ThrowError(errorMsg); - } + NewMessage("No character is selected!", Color.Red); } - catch (Exception e) + + var modifiedArgs = new List(args); + modifiedArgs.Insert(1, "inventory"); + + TrySpawnItem(modifiedArgs.ToArray()); + }, + getValidArgs: () => + { + return new string[][] { - string errorMsg = "Failed to spawn an item. Arguments: \"" + string.Join(" ", args) + "\"."; - ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace()); - } + GetItemNameOrIdParams().ToArray() + }; + }, isCheat: true)); + + commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".", + (string[] args) => + { + TrySpawnItem(args); }, () => { - List itemNames = new List(); - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) - { - if (!itemNames.Contains(itemPrefab.Name.Value)) - { - itemNames.Add(itemPrefab.Name.Value); - } - } - - List spawnPosParams = new List() { "cursor", "inventory" }; -#if SERVER - if (GameMain.Server != null) spawnPosParams.AddRange(GameMain.Server.ConnectedClients.Select(c => c.Name)); -#endif - spawnPosParams.AddRange(Character.CharacterList.Where(c => c.Inventory != null).Select(c => c.Name).Distinct()); - return new string[][] { - itemNames.ToArray(), - spawnPosParams.ToArray() + GetItemNameOrIdParams().ToArray(), + GetSpawnPosParams().ToArray() }; }, isCheat: true)); @@ -583,6 +572,59 @@ namespace Barotrauma }; }, isCheat: true)); + commands.Add(new Command("monstersignoreplayer", "Toggle if monsters should ignore the player character (and their equipment) when targeting.", + onExecute: (string[] args) => + { + ToggleEnemyAITargetingRestrictions(EnemyTargetingRestrictions.PlayerCharacters); + }, + getValidArgs: null, + isCheat: true)); + + commands.Add(new Command("monstersignoresub", "Toggle if monsters should ignore the player submarines when targeting.", + onExecute: (string[] args) => + { + ToggleEnemyAITargetingRestrictions(EnemyTargetingRestrictions.PlayerSubmarines); + }, + getValidArgs: null, + isCheat: true)); + + commands.Add(new Command("monstersrestoretargets", "Remove any targeting restrictions from monsters.", + onExecute: (string[] args) => + { + ToggleEnemyAITargetingRestrictions(EnemyTargetingRestrictions.None); + }, + getValidArgs: null, + isCheat: true)); + + commands.Add(new Command("monstertargetingrestrictions", "monstertargetingrestrictions [restrictions]: Set targeting restrictions for all monsters. Supports multiple options comma-separated: 'monsterargetingrestrictions PlayerCharacters,PlayerSubmarines'. Use 'None' to remove all restrictions.", + onExecute:(string[] args) => + { + if (args.Length == 0) + { + // use the set function to keep log consistent + ToggleEnemyAITargetingRestrictions(EnemyAIController.TargetingRestrictions); + return; + } + + // try parse the complete flags from first arg + if (Enum.TryParse(args[0], ignoreCase: true, out var restrictions)) + { + ToggleEnemyAITargetingRestrictions(restrictions); + } + else + { + NewMessage($"Failed to parse argument '{args[0]}'", Color.Red); + } + }, + getValidArgs: () => + { + return new string[][] + { + Enum.GetNames(typeof(EnemyTargetingRestrictions)) + }; + }, + isCheat: true)); + commands.Add(new Command("listlocations|locations", "listlocations: List all the locations in the level: subs, outposts, ruins, caves.", onExecute:(string[] args) => { @@ -612,7 +654,7 @@ namespace Barotrauma commands.Add(new Command("godmode_mainsub", "godmode_mainsub: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) => { - if (Submarine.MainSub == null) return; + if (Submarine.MainSub == null) { return; } Submarine.MainSub.GodMode = !Submarine.MainSub.GodMode; NewMessage(Submarine.MainSub.GodMode ? "Godmode on" : "Godmode off", Color.White); @@ -712,7 +754,7 @@ namespace Barotrauma { bool.TryParse(args[4], out relativeStrength); } - Character targetCharacter = args.Length <= 2 ? Character.Controlled : FindMatchingCharacter(new string[] { args[2] }); + Character targetCharacter = args.Length <= 2 ? Character.Controlled : FindMatchingCharacter(args.Skip(2).ToArray()); if (targetCharacter != null) { Limb targetLimb = targetCharacter.AnimController.MainLimb; @@ -737,6 +779,22 @@ namespace Barotrauma Enum.GetNames(typeof(LimbType)).ToArray() }; }, isCheat: true)); + + commands.Add(new Command("healme", "healme [all]: Restore controlled character to full health. By default only heals common afflictions such as physical damage and blood loss: use the \"all\" argument to heal everything, including poisonings/addictions/etc.", (string[] args) => + { + bool healAll = args.Length > 0 && args[0].Equals("all", StringComparison.OrdinalIgnoreCase); + if (Character.Controlled != null) + { + HealCharacter(Character.Controlled, healAll); + } + }, + () => + { + return new string[][] + { + new string[] { "all" } + }; + }, isCheat: true)); commands.Add(new Command("heal", "heal [character name] [all]: Restore the specified character to full health. If the name parameter is omitted, the controlled character will be healed. By default only heals common afflictions such as physical damage and blood loss: use the \"all\" argument to heal everything, including poisonings/addictions/etc.", (string[] args) => { @@ -744,21 +802,15 @@ namespace Barotrauma Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(healAll ? args.Take(args.Length - 1).ToArray() : args); if (healedCharacter != null) { - healedCharacter.SetAllDamage(0.0f, 0.0f, 0.0f); - healedCharacter.Oxygen = 100.0f; - healedCharacter.Bloodloss = 0.0f; - healedCharacter.SetStun(0.0f, true); - if (healAll) - { - healedCharacter.CharacterHealth.RemoveAllAfflictions(); - } + HealCharacter(healedCharacter, healAll); } }, () => { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray(), + new string[] { "all" } }; }, isCheat: true)); @@ -806,7 +858,7 @@ namespace Barotrauma // If killed in ironman mode, the character has been wiped from the save mid-round, so its // original data needs to be restored to the save file (without making a backup of the dead character) - if (GameMain.Server.ServerSettings.IronmanMode && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + if (GameMain.Server?.ServerSettings is { IronmanModeActive: true } && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) { if (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore) { @@ -877,32 +929,51 @@ namespace Barotrauma } }, isCheat: true)); - commands.Add(new Command("triggerevent", "triggerevent [identifier]: Created a new event.", (string[] args) => + commands.Add(new Command("triggerevent", "triggerevent [identifier]: Trigger an event based on identifier.", (string[] args) => { - List eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty).ToList(); + List allEventPrefabsWithId = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty).ToList(); if (GameMain.GameSession?.EventManager != null && args.Length > 0) { - EventPrefab eventPrefab = eventPrefabs.Find(prefab => prefab.Identifier == args[0]); - if (eventPrefab is TraitorEventPrefab) + string eventPrefabId = args[0]; + if (eventPrefabId == "all") { - ThrowError($"{eventPrefab.Identifier} is a traitor event. You need to use the 'triggertraitorevent' command to start it."); - return; - } - else if (eventPrefab != null) - { - var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); - if (newEvent == null) + foreach (var eventPrefab in allEventPrefabsWithId.Where(e => e.EventType == typeof(ScriptedEvent))) { - NewMessage($"Could not initialize event {args[0]} because level did not meet requirements"); + var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); + if (newEvent == null) + { + NewMessage($"Could not initialize event {eventPrefabId} because level did not meet requirements"); + return; + } + GameMain.GameSession.EventManager.ActivateEvent(newEvent); + } + } + else + { + EventPrefab eventPrefab = allEventPrefabsWithId.Find(prefab => prefab.Identifier == eventPrefabId); + if (eventPrefab is TraitorEventPrefab) + { + ThrowError($"{eventPrefab.Identifier} is a traitor event. You need to use the 'triggertraitorevent' command to start it."); return; } - GameMain.GameSession.EventManager.ActivateEvent(newEvent); - NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua); + else if (eventPrefab != null) + { + var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); + if (newEvent == null) + { + NewMessage($"Could not initialize event {eventPrefabId} because level did not meet requirements"); + return; + } + GameMain.GameSession.EventManager.ActivateEvent(newEvent); + NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua); + return; + } + else + { + NewMessage($"Failed to trigger event because {eventPrefabId} is not a valid event identifier.", Color.Red); return; + } } - - NewMessage($"Failed to trigger event because {args[0]} is not a valid event identifier.", Color.Red); - return; } NewMessage("Failed to trigger event", Color.Red); }, isCheat: true, getValidArgs: () => @@ -913,8 +984,8 @@ namespace Barotrauma { eventPrefabs.Select(prefab => prefab.Identifier).Distinct().Select(id => id.Value).ToArray() }; - })); - + })); + commands.Add(new Command("debugevent", "debugevent [identifier]: outputs debug info about a specific event that's currently active. Mainly intended for debugging events in multiplayer: in single player, the same information is available by enabling debugdraw.", (string[] args) => { if (args.Length == 0) @@ -1338,6 +1409,46 @@ namespace Barotrauma }; }, isCheat: true)); + commands.Add(new Command("replaceitem", "replaceitem [item name (index)] [new item]: Replaces the specified item with another one.", (string[] args) => + { + if (args.Length < 2) { return; } + + string itemName = args[0]; + int itemIndex = 0; + string newItemName = args[1]; + if (args.Length == 3) + { + if (!int.TryParse(args[1], out itemIndex)) + { + ThrowError($"Failed to parse the argument {args[1]} as an index. Please give the arguments either in the format [old_item] [new_item] or [old_item] [index] [new_item]"); + return; + } + newItemName = args[2]; + } + + var oldItem = Item.ItemList.FindAll(it => it.Name == args[0]).ElementAtOrDefault(itemIndex); + if (oldItem == null) + { + ThrowError($"Could not find an item with the name {args[0]} (index {itemIndex})."); + return; + } + if ((MapEntityPrefab.FindByIdentifier(args[1].ToIdentifier()) ?? MapEntityPrefab.FindByName(args[1])) is not ItemPrefab newItem) + { + ThrowError($"Could not find an item with the name or identifier {args[1]}."); + return; + } + oldItem.ReplaceWithLinkedItems(newItem); + NewMessage($"Replaced {oldItem.Name} with {newItem.Name}."); + }, + () => + { + return new string[][] + { + Item.ItemList.Select(it => it.Name).Distinct().ToArray(), + ItemPrefab.Prefabs.Select(it => it.Name.Value).Distinct().ToArray(), + }; + }, isCheat: true)); + commands.Add(new Command("waterphysicsparams", "waterphysicsparams [stiffness] [spread] [damping]: defaults 0.02, 0.05, 0.05", (string[] args) => { float stiffness = 0.02f, spread = 0.05f, damp = 0.01f; @@ -1349,13 +1460,104 @@ namespace Barotrauma Hull.WaveDampening = damp; }, null)); - commands.Add(new Command("testlevels", "testlevels", (string[] args) => + commands.Add(new Command("testmaps", "testmaps [amount]: generates campaign maps and checks whether there are any errors or exceptions. If the amount argument is omitted, the command will keep testing maps until it's cancelled.", (string[] args) => { - CoroutineManager.StartCoroutine(TestLevels()); + if (args.Length > 0 && int.TryParse(args[0], out int amount)) + { + CoroutineManager.StartCoroutine(TestMaps(amount: amount)); + } + else + { + CoroutineManager.StartCoroutine(TestMaps()); + } }, null)); - IEnumerable TestLevels() + commands.Add(new Command("testmap", "testmap [seed]: generates a campaign map and checks whether there are any errors or exceptions.", (string[] args) => + { + if (args.Length == 0) + { + ThrowError("Please provide the seed of the map to test."); + return; + } + CoroutineManager.StartCoroutine(TestMaps(fixedSeed: args[0], amount: 1)); + }, + null)); + + IEnumerable TestMaps(string fixedSeed = null, int? amount = null) + { + int count = 0; +#if CLIENT + while (!PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.C)) +#else + while (!input.Equals("c", StringComparison.OrdinalIgnoreCase)) +#endif + { + using var errorCatcher = DebugConsole.ErrorCatcher.Create(); + { + string seed = fixedSeed ?? ToolBox.RandomSeed(16); + Map map = new Map(campaign: MultiPlayerCampaign.StartNew(seed, new CampaignSettings()), seed: seed); + + //check path to the first end location, because there are no normal connections between the end locations + var lastLocation = map.EndLocations[0]; + int endDistance = Map.GetDistanceToClosestLocationOrConnection(map.StartLocation, maxDistance: int.MaxValue, criteria: (Location location) => location == lastLocation); + if (endDistance == int.MaxValue) + { + ThrowError($"No path to the end of the map found. Seed: {seed}"); + } + + if (map.Locations.None(l => l.Type.Identifier == "outpost" && map.GetZoneIndex(l.MapPosition.X) == 1)) + { + ThrowError($"No outpost in the first zone of the map. Seed: {seed}"); + } + + if (errorCatcher.Errors.Any()) + { + ThrowError($"Error(s) found when generating a level. Seed: {seed}"); + yield return CoroutineStatus.Success; + } + + count++; + NewMessage($"Map seed {seed} ok (test #{count}). Press C to abort."); + + map.Remove(); + + if (amount.HasValue && count >= amount) + { + NewMessage("Testing finished successfully."); + break; + } + } + + yield return CoroutineStatus.Running; + } + } + + commands.Add(new Command("testlevels", "testlevels [amount]: generates levels and checks whether there are any errors or exceptions. If the amount argument is omitted, the command will keep testing levels until it's cancelled.", (string[] args) => + { + if (args.Length > 0 && int.TryParse(args[0], out int amount)) + { + CoroutineManager.StartCoroutine(TestLevels(amount: amount)); + } + else + { + CoroutineManager.StartCoroutine(TestLevels()); + } + }, + null)); + + commands.Add(new Command("testlevel", "testlevel [seed]: generates a levels and checks whether there are any errors or exceptions.", (string[] args) => + { + if (args.Length == 0) + { + ThrowError("Please provide the seed of the level to test."); + return; + } + CoroutineManager.StartCoroutine(TestLevels(fixedSeed: args[0], amount: 1)); + }, + null)); + + IEnumerable TestLevels(string fixedSeed = null, int? amount = null) { SubmarineInfo selectedSub = null; Identifier subName = GameSettings.CurrentConfig.QuickStartSub; @@ -1365,12 +1567,17 @@ namespace Barotrauma } int count = 0; - while (true) +#if CLIENT + while (!PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.C)) +#else + while (!input.Equals("c", StringComparison.OrdinalIgnoreCase)) +#endif { var gamesession = new GameSession( SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)), + Option.None, GameModePreset.DevSandbox ?? GameModePreset.Sandbox); - string seed = ToolBox.RandomSeed(16); + string seed = fixedSeed ?? ToolBox.RandomSeed(16); gamesession.StartRound(seed); Rectangle subWorldRect = Submarine.MainSub.Borders; @@ -1410,11 +1617,17 @@ namespace Barotrauma Submarine.Unload(); count++; - NewMessage("Level seed " + seed + " ok (test #" + count + ")"); + NewMessage("Level seed " + seed + " ok (test #" + count + "). Press C to abort."); #if CLIENT //dismiss round summary and any other message boxes GUIMessageBox.CloseAll(); #endif + if (amount.HasValue && count >= amount) + { + NewMessage("Testing finished successfully."); + break; + } + yield return CoroutineStatus.Running; } } @@ -2056,6 +2269,22 @@ namespace Barotrauma commands.Sort((c1, c2) => c1.Names.First().CompareTo(c2.Names.First())); } + private static void HealCharacter(Character healedCharacter, bool healAll) + { + healedCharacter.SetAllDamage(0.0f, 0.0f, 0.0f); + healedCharacter.Oxygen = 100.0f; + healedCharacter.Bloodloss = 0.0f; + healedCharacter.SetStun(0.0f, true); + if (healAll) + { + healedCharacter.CharacterHealth.RemoveAllAfflictions(); + } + + string characterNameText = healedCharacter == Character.Controlled ? $"{healedCharacter.Name} (you)" : healedCharacter.Name; + string text = healAll ? $"Healed {characterNameText}: all afflictions" : $"Healed {characterNameText}: damage and common afflictions"; + NewMessage(text, Color.Yellow); + } + public static string AutoComplete(string command, int increment = 1) { string[] splitCommand = ToolBox.SplitCommand(command); @@ -2231,8 +2460,6 @@ namespace Barotrauma } } - private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ThenBy(c => c.Name).Select(c => c.Name).Distinct().ToArray(); - private static string[] ListAvailableLocations() { List locationNames = new(); @@ -2347,33 +2574,130 @@ namespace Barotrauma } } + + private static TFile GetSubmarineFile(string submarineName) where TFile : BaseSubFile + { + List submarineFiles = GetContentFiles(); + + foreach (var file in submarineFiles) + { + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(i => i.FilePath == file.Path.Value); + if (matchingSub != null && string.Equals(matchingSub.Name, submarineName, StringComparison.InvariantCultureIgnoreCase)) + { + return file; + } + } + + return null; + } + + private static List GetContentFiles() where TFile : ContentFile + { + var contentFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()) + .ToList(); + + return contentFiles; + } + + private static List GetSubmarineFiles() where TFile : BaseSubFile + { + var submarineFiles = GetContentFiles() + .OrderBy(f => f.UintIdentifier).ToList(); + + return submarineFiles; + } + + private static ContentFile GetContentFile(string path) + { + var contentFiles = GetContentFiles(); + return contentFiles.FirstOrDefault(file => string.Equals(file.Path.Value, path, StringComparison.InvariantCultureIgnoreCase)); + } + + private static string[] ListContentFilePaths() + { + List contentFilePaths = new(); + + var contentFiles = GetContentFiles(); + foreach (var contentFile in contentFiles) + { + contentFilePaths.Add(contentFile.Path.Value); + } + + return contentFilePaths.ToArray(); + } + + private static string[] ListSubmarineFileNames() where TFile : BaseSubFile + { + List submarineFileNames = new List(); + + var submarineFiles = GetSubmarineFiles(); + + foreach (var file in submarineFiles) + { + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(i => i.FilePath == file.Path.Value); + if (matchingSub != null) + { + submarineFileNames.Add(matchingSub.Name); + } + } + + return submarineFileNames.ToArray(); + } + + private static IOrderedEnumerable SortSpawnedSpecies(IEnumerable characterList) => characterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ThenBy(c => c.Name); + + private static string[] ListCharacterNames() => GetCharacterNames(); + + private static string[] GetCharacterNames() => SortSpawnedSpecies(Character.CharacterList).Select(c => c.Name).Distinct().ToArray(); + + private static string[] GetSpawnedSpeciesNames() => SortSpawnedSpecies(Character.CharacterList).Select(c => c.SpeciesName.Value).Distinct().ToArray(); + + private static IEnumerable FindMatchingSpecies(string[] args) + { + if (args.Length == 0) { return Array.Empty(); } + string speciesName = args[0].ToLowerInvariant(); + return FindMatchingSpecies(speciesName); + } + + private static IEnumerable FindMatchingSpecies(string speciesName) => Character.CharacterList.FindAll(c => c.SpeciesName.Value.Equals(speciesName, StringComparison.OrdinalIgnoreCase)); + private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null, bool botsOnly = false) { - if (args.Length == 0) return null; - - string characterName; - if (int.TryParse(args.Last(), out int characterIndex) && args.Length > 1) + if (args.Length == 0) { return null; } + + List matchingCharacters = null; + string characterName = null; + int characterIndex = -1; + foreach (string arg in args) { - characterName = string.Join(" ", args.Take(args.Length - 1)).ToLowerInvariant(); - } - else - { - characterName = string.Join(" ", args).ToLowerInvariant(); - characterIndex = -1; + // First try to parse the character name from all the arguments. + if (matchingCharacters == null || matchingCharacters.None()) + { + string possibleCharacterName = arg?.ToLowerInvariant(); + matchingCharacters = Character.CharacterList.FindAll(c => + c.Name.Equals(possibleCharacterName, StringComparison.OrdinalIgnoreCase) && + (!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c)); + + if (botsOnly) + { + matchingCharacters = matchingCharacters.FindAll(c => c is AICharacter); + } + if (matchingCharacters.Any()) + { + characterName = possibleCharacterName; + } + } + else if (characterName != null && int.TryParse(arg, out int possibleIndex)) + { + // If we've already found the character name, let's seek for the index. + characterIndex = possibleIndex; + } } - var matchingCharacters = Character.CharacterList.FindAll(c => - c.Name.Equals(characterName, StringComparison.OrdinalIgnoreCase) && - (!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c)); - - if (botsOnly) + if (matchingCharacters == null || matchingCharacters.None()) { - matchingCharacters = matchingCharacters.FindAll(c => c is AICharacter); - } - - if (!matchingCharacters.Any()) - { - NewMessage("Character \""+ characterName + "\" not found", Color.Red); + NewMessage("No matching character found!", Color.Red); return null; } @@ -2413,35 +2737,30 @@ namespace Barotrauma Character targetCharacter = controlledCharacter; Vector2 worldPosition = cursorWorldPos; string locationNameArgument = ""; - - var availableLocations = ListAvailableLocations(); if (args.Length > 0) { - if (args.Length > 1) + string firstArgument = args.First().ToLowerInvariant(); + string lastArgument = args.Last(); + // First seek the matching character. + if (firstArgument != "me") { - // remove location name from args - if (availableLocations.Contains(args.Last()) - || string.Equals(args.Last(), "mainsub", StringComparison.InvariantCultureIgnoreCase)) + var availableLocations = Submarine.MainSub != null ? new[] { "mainsub", "cursor" } : Array.Empty(); + availableLocations = availableLocations.Concat(ListAvailableLocations()).ToArray(); + if (args.Length > 1 || availableLocations.None(locationName => string.Equals(locationName, lastArgument, StringComparison.OrdinalIgnoreCase))) { - locationNameArgument = args.Last(); - args = args.Take(args.Length - 1).ToArray(); - } - else - { - NewMessage("Invalid arguments", color: Color.Yellow); - return; + // Try to find a matching character, if there's more than one argument or if the last argument is not a valid location argument. + // If there's only one argument, and it's a valid location argument, we shouldn't try to parse a target character from it. + targetCharacter = FindMatchingCharacter(args, ignoreRemotePlayers: false); } } - - // the remaining args should be the character name and a possible index - if (args[0].ToLowerInvariant() != "me") + // Then seek the possible location argument. + if (targetCharacter == null || !targetCharacter.Name.Equals(lastArgument, StringComparison.OrdinalIgnoreCase) && !int.TryParse(lastArgument, out _)) { - Character match = FindMatchingCharacter(args, ignoreRemotePlayers:false); - targetCharacter = match; + locationNameArgument = lastArgument; } } - if (!string.IsNullOrWhiteSpace(locationNameArgument)) + if (!string.IsNullOrWhiteSpace(locationNameArgument) && !string.Equals(locationNameArgument, "cursor", comparisonType: StringComparison.InvariantCultureIgnoreCase)) { if (TryFindTeleportPosition(locationNameArgument, out Vector2 teleportPosition)) { @@ -2485,13 +2804,12 @@ namespace Barotrauma job = JobPrefab.Prefabs[characterLowerCase]; } bool isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName; - bool addToCrew = false; if (args.Length > 1) { switch (args[1].ToLowerInvariant()) { case "inside": - spawnPoint = WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub); + spawnPoint = WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub) ?? WayPoint.GetRandom(SpawnType.Human, assignedJob: null, Submarine.MainSub); break; case "outside": spawnPoint = WayPoint.GetRandom(SpawnType.Enemy); @@ -2522,10 +2840,6 @@ namespace Barotrauma spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy); break; } - addToCrew = - args.Length > 3 ? - args[3].Equals("true", StringComparison.OrdinalIgnoreCase) : - isHuman; } else { @@ -2533,19 +2847,31 @@ namespace Barotrauma } if (string.IsNullOrWhiteSpace(args[0])) { return; } - CharacterTeamType teamType = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1; + + CharacterTeamType defaultTeamType = isHuman ? CharacterTeamType.Team1 : CharacterTeamType.None; + CharacterTeamType teamType = defaultTeamType; if (args.Length > 2) { - try + string team = args[2]; + if (!Enum.TryParse(team, ignoreCase: true, out teamType) && !string.IsNullOrWhiteSpace(team)) { - teamType = (CharacterTeamType)int.Parse(args[2]); - } - catch - { - ThrowError($"\"{args[2]}\" is not a valid team id."); + try + { + teamType = (CharacterTeamType)int.Parse(team); + } + catch + { + ThrowError($"\"{team}\" is not a valid team id."); + } } } - + + bool addToCrew = isHuman && teamType == defaultTeamType; + if (args.Length > 3) + { + addToCrew = args[3].Equals("true", StringComparison.OrdinalIgnoreCase); + } + if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; } if (isHuman) @@ -2553,10 +2879,6 @@ namespace Barotrauma var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient) : 0; CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant); spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8)); - - spawnedCharacter.GiveJobItems(spawnPoint); - spawnedCharacter.GiveIdCardTags(spawnPoint); - spawnedCharacter.Info.StartItemsGiven = true; } else { @@ -2571,12 +2893,89 @@ namespace Barotrauma spawnedCharacter = Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8), characterInfo); } } - if (addToCrew && GameMain.GameSession != null) + if (spawnedCharacter == null) { - spawnedCharacter.TeamID = teamType; + DebugConsole.ThrowError("Failed to spawn a character with the provided arguments!"); + return; + } + spawnedCharacter.TeamID = teamType; #if CLIENT - GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter); + if (addToCrew && GameMain.GameSession?.CrewManager is CrewManager crewManager) + { + crewManager.AddCharacter(spawnedCharacter); + } #endif + if (isHuman) + { + spawnedCharacter.GiveJobItems(isPvPMode: GameMain.GameSession?.GameMode is PvPMode, spawnPoint); + spawnedCharacter.GiveIdCardTags(spawnPoint); + spawnedCharacter.Info.StartItemsGiven = true; + } + } + + private static IEnumerable GetSpawnPosParams() + { + yield return "cursor"; + yield return "inventory"; + +#if SERVER + if (GameMain.Server != null) + { + foreach (var clientName in GameMain.Server.ConnectedClients.Select(c => c.Name)) + { + yield return clientName; + } + } +#endif + + foreach (var characterName in Character.CharacterList.Where(c => c.Inventory != null).Select(c => c.Name).Distinct()) + { + yield return characterName; + } + } + + private static IEnumerable GetItemNameOrIdParams() + { + HashSet seen = new HashSet(); + + foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + { + if (seen.Add(itemPrefab.Name.Value)) + { + yield return itemPrefab.Name.Value; + } + } + + seen.Clear(); + + foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + { + if (seen.Add(itemPrefab.Identifier.Value)) + { + yield return itemPrefab.Identifier.Value; + } + } + } + + private static void TrySpawnItem(string[] args) + { + try + { +#if CLIENT + SpawnItem(args, Screen.Selected.Cam?.ScreenToWorld(PlayerInput.MousePosition) ?? PlayerInput.MousePosition, Character.Controlled, out string errorMsg); +#elif SERVER + SpawnItem(args, Vector2.Zero, null, out string errorMsg); +#endif + if (!string.IsNullOrWhiteSpace(errorMsg)) + { + ThrowError(errorMsg); + } + } + catch (Exception e) + { + string errorMsg = "Failed to spawn an item. Arguments: \"" + string.Join(" ", args) + "\"."; + ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace()); } } @@ -2607,17 +3006,20 @@ namespace Barotrauma return; } - int amount = 1; - if (args.Length > 1) + bool TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex) + { + var allSpawnPosParams = GetSpawnPosParams(); + spawnLocation = args.FirstOrDefault(s => allSpawnPosParams.Contains(s)); + spawnLocationIndex = spawnLocation != null ? args.IndexOf(spawnLocation) : -1; + + return spawnLocation != null; + } + + int amount = 1; + int conditionPrc = 100; + + if (TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex)) { - string spawnLocation = args.Last(); - if (args.Length > 2) - { - spawnLocation = args[^2]; - if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; } - amount = Math.Min(amount, 100); - } - switch (spawnLocation) { case "cursor": @@ -2638,8 +3040,21 @@ namespace Barotrauma if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; } break; } + + if (args.Length > spawnLocationIndex + 1) + { + if (!int.TryParse(args[spawnLocationIndex + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; } + amount = Math.Min(amount, 100); + } + + if (args.Length > spawnLocationIndex + 2) + { + if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; } + } } + float itemCondition = itemPrefab.Health * Math.Clamp(conditionPrc / 100f, 0f, 1f); + if ((spawnPos == null || spawnPos == Vector2.Zero) && spawnInventory == null) { var wp = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); @@ -2656,7 +3071,7 @@ namespace Barotrauma } else { - Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value); + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition); } } else if (spawnInventory != null) @@ -2672,7 +3087,7 @@ namespace Barotrauma Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned); } - static void onItemSpawned(Item item) + void onItemSpawned(Item item) { if (item.ParentInventory?.Owner is Character character) { @@ -2681,6 +3096,8 @@ namespace Barotrauma wifiComponent.TeamID = character.TeamID; } } + + item.Condition = item.Health * Math.Clamp(conditionPrc / 100f, 0f, 1f); } } } @@ -2928,7 +3345,7 @@ namespace Barotrauma { try { - Directory.CreateDirectory(SavePath); + Directory.CreateDirectory(SavePath, catchUnauthorizedAccessExceptions: false); } catch (Exception e) { @@ -2964,7 +3381,7 @@ namespace Barotrauma try { - File.WriteAllLines(filePath + ".txt", unsavedMessages.Select(l => "[" + l.Time + "] " + l.Text)); + File.WriteAllLines(filePath + ".txt", unsavedMessages.Select(l => "[" + l.Time + "] " + l.Text), catchUnauthorizedAccessExceptions: false); } catch (Exception e) { @@ -2972,6 +3389,31 @@ namespace Barotrauma ThrowError("Saving debug console log to " + filePath + " failed", e); } } + + private static void ToggleEnemyAITargetingRestrictions(EnemyTargetingRestrictions restrictions) + { + if (restrictions == EnemyTargetingRestrictions.None) + { + // If restriction is None, clear all restrictions + EnemyAIController.TargetingRestrictions = EnemyTargetingRestrictions.None; + } + else + { + // Toggle the restriction + if (EnemyAIController.TargetingRestrictions.HasFlag(restrictions)) + { + // If the restriction is already set, remove it + EnemyAIController.TargetingRestrictions &= ~restrictions; + } + else + { + // If the restriction is not set, add it + EnemyAIController.TargetingRestrictions |= restrictions; + } + } + + NewMessage($"Monster targeting restrictions is now '{EnemyAIController.TargetingRestrictions}'", Color.Yellow); + } public static void DeactivateCheats() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/DisembarkPerkPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/DisembarkPerkPrefab.cs new file mode 100644 index 000000000..6c713fcc6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/DisembarkPerkPrefab.cs @@ -0,0 +1,58 @@ +using System.Collections.Immutable; +using Barotrauma.PerkBehaviors; + +namespace Barotrauma +{ + internal sealed class DisembarkPerkPrefab : PrefabWithUintIdentifier + { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + public LocalizedString Name { get; } + public LocalizedString Description { get; } + public Identifier SortCategory { get; } + + /// + /// After the perks have been sorted by category and cost, they are sorted using this key. + /// Use if you want the perks to be arranged in specific order when their cost are the same. + /// + public int SortKey { get; } + + /// + /// When set to an identifier of another perk, this perk cannot be selected unless the prerequisite perk is selected. + /// + public Identifier Prerequisite { get; } + + /// + /// When this perk is selected, the perks in this set cannot be selected at the same time. + /// + public ImmutableHashSet MutuallyExclusivePerks { get; } + + public int Cost { get; } + + public ImmutableArray PerkBehaviors { get; } + + public DisembarkPerkPrefab(ContentXElement element, DisembarkPerkFile prefabFile) : base(prefabFile, element.GetAttributeIdentifier("identifier", "")) + { + Name = TextManager.Get($"disembarkperk.{Identifier}").Fallback(Identifier.ToString()); + Description = TextManager.Get($"disembarkperkdescription.{Identifier}").Fallback(""); + Cost = element.GetAttributeInt("cost", 0); + SortCategory = element.GetAttributeIdentifier("sortcategory", Identifier); + Prerequisite = element.GetAttributeIdentifier("prerequisite", Identifier.Empty); + MutuallyExclusivePerks = element.GetAttributeIdentifierImmutableHashSet("mutuallyexclusiveperks", ImmutableHashSet.Empty); + SortKey = element.GetAttributeInt("sortkey", ToolBox.IdentifierToInt(Identifier)); + + var builder = ImmutableArray.CreateBuilder(); + foreach (var child in element.Elements()) + { + if (PerkBase.TryLoadFromXml(child, this, out var perk)) + { + builder.Add(perk); + } + } + + PerkBehaviors = builder.ToImmutable(); + } + + public override void Dispose() { } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/GiveTalentPointPerk.cs b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/GiveTalentPointPerk.cs new file mode 100644 index 000000000..1e351f45e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/GiveTalentPointPerk.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Barotrauma.PerkBehaviors +{ + internal class GiveTalentPointPerk : PerkBase + { + [Serialize(0, IsPropertySaveable.Yes)] + public int Amount { get; set; } + + public GiveTalentPointPerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { } + + public override void ApplyOnRoundStart(IReadOnlyCollection teamCharacters, Submarine teamSubmarine) + { + foreach (Character character in teamCharacters) + { + character.Info.AdditionalTalentPoints += Amount; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/PerkBase.cs b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/PerkBase.cs new file mode 100644 index 000000000..7e534b665 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/PerkBase.cs @@ -0,0 +1,84 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Barotrauma.PerkBehaviors +{ + internal enum PerkSimulation + { + /// + /// Perk is only run on the server, + /// other parts of the game handle the client-side effects. + /// Like serializable properties and affliction syncing. + /// + ServerOnly, + /// + /// Both the server and clients run the perk. + /// + ServerAndClients + } + + internal abstract class PerkBase : ISerializableEntity + { + public string Name { get; } + public Dictionary SerializableProperties { get; } + + public virtual PerkSimulation Simulation => PerkSimulation.ServerOnly; + + public readonly DisembarkPerkPrefab Prefab; + + protected PerkBase(ContentXElement element, DisembarkPerkPrefab prefab) + { + Name = element.Name.ToString(); + Prefab = prefab; + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + + public virtual bool CanApply(SubmarineInfo submarine) + { + return true; + } + + /// + /// You might notice that this function is not virtual. + /// It was at first, but there was a misunderstanding in design, + /// so I turned it into a kill switch for all perks for now. + /// If we ever want to add perks that do work when a submarine is present, + /// this function can be made virtual again and set to true in the appropriate perks. + /// + public bool CanApplyWithoutSubmarine() + => false; + + public abstract void ApplyOnRoundStart(IReadOnlyCollection teamCharacters, Submarine? teamSubmarine); + + public static bool TryLoadFromXml(ContentXElement element, DisembarkPerkPrefab prefab, [NotNullWhen(true)] out PerkBase? perk) + { + Type? type = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.PerkBehaviors", element.Name.ToString(), throwOnError: false, ignoreCase: true); + if (type is null) + { + DebugConsole.ThrowError($"Could not find a perk behavior of the type \"{element.Name}\".", contentPackage: element.ContentPackage); + perk = null; + return false; + } + + try + { + object? instance = Activator.CreateInstance(type, element, prefab); + if (instance is PerkBase perkInstance) + { + perk = perkInstance; + return true; + } + + throw new InvalidCastException($"Could not cast the instance of type \"{type}\" to a {nameof(PerkBase)}."); + } + catch (Exception e) + { + DebugConsole.ThrowError(e.InnerException != null ? e.InnerException.ToString() : e.ToString(), contentPackage: element.ContentPackage); + perk = null; + return false; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/SpawnItemPerk.cs b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/SpawnItemPerk.cs new file mode 100644 index 000000000..468bf582e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/SpawnItemPerk.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; + +namespace Barotrauma.PerkBehaviors +{ + internal class SpawnItemPerk : PerkBase + { + public SpawnItemPerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { } + + public override PerkSimulation Simulation + => PerkSimulation.ServerOnly; + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Tag { get; set; } + + [Serialize(0, IsPropertySaveable.Yes)] + public int MinAmount { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes)] + public float PerPlayer { get; set; } + + /// + /// When set to non-empty value, the perk will prioritize spawning items in containers + /// with this tag or identifier over the item's primary and secondary preferred containers. + /// + [Serialize("", IsPropertySaveable.Yes)] + public Identifier PriorityContainerTag { get; set; } + + public override void ApplyOnRoundStart(IReadOnlyCollection teamCharacters, Submarine teamSubmarine) + { + if (teamSubmarine is null) { return; } + + if (Entity.Spawner is null) + { + DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items because EntitySpawner is null."); + return; + } + + int amount = Math.Max(MinAmount, (int)MathF.Ceiling(PerPlayer * teamCharacters.Count)); + + if (Identifier.IsEmpty) + { + if (Tag.IsEmpty) + { + DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items: neither identifier or tag is set.", + contentPackage: Prefab.ContentPackage); + return; + } + var matchingItems = ItemPrefab.Prefabs.Where(ip => ip.Tags.Contains(Tag)); + if (matchingItems.None()) + { + DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items: no items found with the tag \"{Tag}\".", + contentPackage: Prefab.ContentPackage); + return; + } + for (int i = 0; i < amount; i++) + { + SpawnItem(matchingItems.GetRandomUnsynced(), amount: 1); + } + } + else + { + + ItemPrefab prefab = ItemPrefab.Find(null, Identifier); + if (prefab is null) + { + DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items because the ItemPrefab \"{Identifier}\" was not found.", + contentPackage: Prefab.ContentPackage); + return; + } + SpawnItem(prefab, amount); + } + + void SpawnItem(ItemPrefab prefab, int amount) + { + SuitableContainers suitableContainers = FindSuitableContainers(prefab, teamSubmarine); + + if (!suitableContainers.Any()) + { + SpawnItemInCrate(prefab, teamSubmarine, amount); + return; + } + SpawnInContainer(prefab, amount, suitableContainers, teamSubmarine); + } + } + + private readonly record struct SuitableContainers( + ICollection PriorityContainers, + ICollection PreferredContainers, + ICollection SecondaryContainers) + { + public bool Any() + => PriorityContainers.Count > 0 + || PreferredContainers.Count > 0 + || SecondaryContainers.Count > 0; + } + + private SuitableContainers FindSuitableContainers(ItemPrefab prefab, Submarine submarine) + { + HashSet priorityContainers = new(); + HashSet primaryContainers = new(); + HashSet secondaryContainers = new(); + + foreach (Item item in submarine.GetItems(alsoFromConnectedSubs: true)) + { + if (item.GetComponent() != null || item.GetComponent() != null) { continue; } + if (item.NonInteractable || item.NonPlayerTeamInteractable || item.IsHidden) { continue; } + + if (item.GetComponent() is { } container) + { + if (!container.CanBeContained(prefab)) { continue; } + + var tags = item.GetTags(); + + if (!PriorityContainerTag.IsEmpty && (tags.Contains(PriorityContainerTag) || item.Prefab.Identifier == PriorityContainerTag)) + { + priorityContainers.Add(container); + continue; + } + + if (prefab.PreferredContainers.Any(pc => pc.Primary.Any(tags.Contains))) + { + primaryContainers.Add(container); + continue; + } + + if (prefab.PreferredContainers.Any(pc => pc.Secondary.Any(tags.Contains))) + { + secondaryContainers.Add(container); + } + } + } + + return new SuitableContainers(priorityContainers, primaryContainers, secondaryContainers); + } + + private static void SpawnItemInCrate(ItemPrefab prefab, Submarine submarine, int amount) + { + var purchasedItem = new PurchasedItem(prefab, amount, buyer: null); + CargoManager.DeliverItemsToSub(new []{ purchasedItem }, submarine, cargoManager: null, showNotification: false); + } + + private static void SpawnInContainer(ItemPrefab prefab, int amount, SuitableContainers containers, Submarine submarine) + { + Dictionary containerAllocation = new(); + + int remaining = amount; + + TryAllocate(containers.PriorityContainers); + if (remaining > 0) + { + TryAllocate(containers.PreferredContainers); + if (remaining > 0) + { + TryAllocate(containers.SecondaryContainers); + } + } + + void TryAllocate(ICollection targetContainers) + => AllocateContainers(prefab, targetContainers, ref remaining, ref containerAllocation); + + foreach (var (container, howManyToPut) in containerAllocation) + { + for (int i = 0; i < howManyToPut; i++) + { + SpawnItem(prefab, container); + } + } + + if (remaining > 0) + { + SpawnItemInCrate(prefab, submarine, remaining); + } + + static void AllocateContainers(ItemPrefab prefab, ICollection containers, ref int remaining, ref Dictionary containerAllocation) + { + foreach (ItemContainer ic in containers) + { + int fit = ic.Inventory.HowManyCanBePut(prefab); + if (fit <= 0) { continue; } + + fit = Math.Min(fit, remaining); + + containerAllocation.Add(ic, fit); + remaining -= fit; + + if (remaining <= 0) { break; } + } + } + + static void SpawnItem(ItemPrefab itemPrefab, ItemContainer container) + { + if (container?.Item is null) { return; } + + Item item = new Item(itemPrefab, container.Item.Position, container.Item.Submarine); + container.Inventory.TryPutItem(item, user: null); + CargoManager.ItemSpawned(item); +#if SERVER + Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); +#endif + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/SubItemSwapPerk.cs b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/SubItemSwapPerk.cs new file mode 100644 index 000000000..b013eaa90 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/SubItemSwapPerk.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.PerkBehaviors +{ + internal class SubItemSwapPerk : PerkBase + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetItem { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ReplacementItem { get; set; } + + public override PerkSimulation Simulation + => PerkSimulation.ServerOnly; + + public SubItemSwapPerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { } + + public override bool CanApply(SubmarineInfo submarine) + { + XElement subElement = submarine.SubmarineElement; + + foreach (XElement element in subElement.Elements()) + { + if (!element.Name.ToString().Equals(nameof(Item), StringComparison.OrdinalIgnoreCase)) { continue; } + + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier == TargetItem) + { + return true; + } + } + + return false; + } + + public override void ApplyOnRoundStart(IReadOnlyCollection teamCharacters, Submarine teamSubmarine) + { + if (teamSubmarine is null) { return; } + + List items = teamSubmarine.GetItems(true); + + ItemPrefab itemToInstall = ItemPrefab.Find(null, ReplacementItem); + if (itemToInstall is null) + { + DebugConsole.ThrowError($"Could not find item \"{ReplacementItem}\" to swap with \"{TargetItem}\"."); + return; + } + + foreach (Item item in items) + { + if (item.Prefab.Identifier == TargetItem) + { + item.ReplaceWithLinkedItems(itemToInstall); + return; + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/UpgradeSubmarinePerk.cs b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/UpgradeSubmarinePerk.cs new file mode 100644 index 000000000..5d209f9fd --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/DisembarkPerks/PerkBehaviors/UpgradeSubmarinePerk.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; + +namespace Barotrauma.PerkBehaviors +{ + internal class UpgradeSubmarinePerk : PerkBase + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier UpgradeIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier CategoryIdentifier { get; set; } + + [Serialize(0, IsPropertySaveable.Yes)] + public int Level { get; set; } + + public override PerkSimulation Simulation + => PerkSimulation.ServerAndClients; + + public UpgradeSubmarinePerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { } + + public override void ApplyOnRoundStart(IReadOnlyCollection teamCharacters, Submarine teamSubmarine) + { + if (teamSubmarine is null) { return; } + + bool prefabFound = UpgradePrefab.Prefabs.TryGet(UpgradeIdentifier, out UpgradePrefab upgradePrefab); + bool categoryFound = UpgradeCategory.Categories.TryGet(CategoryIdentifier, out UpgradeCategory upgradeCategory); + + if (!prefabFound) + { + DebugConsole.ThrowError($"{nameof(UpgradeSubmarinePerk)}: Upgrade prefab not found"); + return; + } + + if (upgradePrefab.IsWallUpgrade) + { + foreach (Structure structure in teamSubmarine.GetWalls(UpgradeManager.UpgradeAlsoConnectedSubs)) + { + structure.AddUpgrade(new Upgrade(structure, upgradePrefab, Level), createNetworkEvent: true); + } + } + else if (categoryFound) + { + foreach (Item item in teamSubmarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs)) + { + if (upgradeCategory.CanBeApplied(item, upgradePrefab)) + { + item.AddUpgrade(new Upgrade(item, upgradePrefab, Level), createNetworkEvent: true); + } + } + } + else + { + DebugConsole.ThrowError($"{nameof(UpgradeSubmarinePerk)}: Upgrade category not found"); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 42a47e8de..62d10ff20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -12,6 +12,13 @@ namespace Barotrauma Exponential } + public enum SelectedSubType + { + Shuttle, + Sub, + EnemySub + } + /// /// ActionTypes define when a is executed. /// @@ -116,6 +123,14 @@ namespace Barotrauma /// OnAbility = 23, /// + /// Executes once when a specific Containable is placed inside an ItemContainer. Only valid for Containables defined in an ItemContainer component. + /// + OnInserted = 24, + /// + /// Executes once when a specific Containable is removed from an ItemContainer. Only valid for Containables defined in an ItemContainer component. + /// + OnRemoved = 25, + /// /// Executes when the character dies. Only valid for characters. /// OnDeath = OnBroken @@ -588,7 +603,17 @@ namespace Barotrauma /// /// Reduces the dual wielding penalty by a percentage. /// - DualWieldingPenaltyReduction + DualWieldingPenaltyReduction, + + /// + /// Multiplier bonus to melee attacks coming from a natural weapon (limb). + /// + NaturalMeleeAttackMultiplier, + + /// + /// Multiplier bonus to ranged attacks coming from a natural weapon (limb). + /// + NaturalRangedAttackMultiplier } internal enum ItemTalentStats @@ -599,12 +624,13 @@ namespace Barotrauma EngineSpeed, EngineMaxSpeed, PumpSpeed, - PumpMaxFlow, ReactorMaxOutput, ReactorFuelConsumption, DeconstructorSpeed, FabricationSpeed, - ExtraStackSize + ExtraStackSize, + [Obsolete("Use PumpSpeed instead.")] + PumpMaxFlow = PumpSpeed, } /// @@ -723,4 +749,10 @@ namespace Barotrauma Local, Radio } + + public enum PvpTeamSelectionMode + { + PlayerPreference, + PlayerChoice, + } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index 987932e55..85e65cd65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -45,7 +45,6 @@ namespace Barotrauma foreach (ContentXElement subElement in conditionalElements) { conditionalList.AddRange(PropertyConditional.FromXElement(subElement)); - break; } Conditionals = conditionalList.ToImmutableArray(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index 91f328b5e..42f159021 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -200,6 +200,10 @@ namespace Barotrauma { condition = $"{value1.ColorizeObject()} {Operator.ColorizeObject()} {value2.ColorizeObject()}"; } + else if (!Identifier.IsEmpty) + { + condition = $"{Identifier} {Condition}".ColorizeObject(); + } return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckDataAction)} -> (Data: {Identifier.ColorizeObject()}, Success: {succeeded.ColorizeObject()}, Expression: {condition})"; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index b9a819843..6d2e16e74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -66,13 +66,12 @@ namespace Barotrauma public CheckItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers(); - itemTags = ItemTags.Split(",").ToIdentifiers(); + itemIdentifierSplit = ItemIdentifiers.ToIdentifiers().ToArray(); + itemTags = ItemTags.ToIdentifiers().ToArray(); var conditionalList = new List(); foreach (ContentXElement subElement in element.GetChildElements("conditional")) { conditionalList.AddRange(PropertyConditional.FromXElement(subElement)); - break; } conditionals = conditionalList; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index dd9c5b7a0..4e5d7f065 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -113,13 +113,15 @@ namespace Barotrauma Text = elem.GetAttributeString("tag", string.Empty); textElement = elem; } - } - if (element.GetChildElement("Replace") != null) - { - DebugConsole.ThrowError( - $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" + - $" - unrecognized child element \"Replace\".", - contentPackage: element.ContentPackage); + else + { + string thisName = nameof(ConversationAction); + DebugConsole.ThrowError( + $"Error in {thisName} in the event \"{parentEvent.Prefab.Identifier}\"" + + $" - unrecognized child element \"{elem.Name}\". If it's an action intended to execute after the {thisName}, " + + $"it should be after the {thisName}, not inside it.", + contentPackage: element.ContentPackage); + } } } @@ -245,7 +247,17 @@ namespace Barotrauma public int[] GetEndingOptions() { - List endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList(); + List endings = Options + .Where(group => + group.EndConversation || + //no actions = safe to assume this must end the conversation + !group.Actions.Any() || + //no follow-up conversation and a goto makes the event jump somewhere else + //we cannot easily determine whether that goto will lead to a follow-up conversation, + //so it's safest to close this conversation to prevent it from getting stuck (the potential follow-up will open a new one) + (group.Actions.None(a => a is ConversationAction) && group.Actions.Any(a => a is GoTo { EndConversation: true }))) + .Select(group => Options.IndexOf(group)) + .ToList(); if (!ContinueConversation) { endings.Add(-1); } return endings.ToArray(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index 97e01c2b3..f1a54e742 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -11,6 +11,10 @@ namespace Barotrauma { public string Text; public List Actions; + /// + /// Should this option end the conversation (closing the conversation prompt?). By default, options that don't have any actions inside them, or that only have a GoTo action, end the conversation. + /// But if there are other actions inside the option, the game assumes there may be some kind of a follow-up coming to the conversation, and by default leaves it open. + /// public bool EndConversation; private int currentSubAction = 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs index bc83b8afd..7df0cc135 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs @@ -1,4 +1,4 @@ -namespace Barotrauma +namespace Barotrauma { /// /// Makes the event jump to a somewhere else in the event. @@ -11,6 +11,9 @@ namespace Barotrauma [Serialize(-1, IsPropertySaveable.Yes, description: "How many times can this GoTo action be repeated? Can be used to make some parts of an event repeat a limited number of times. If negative or zero, there's no limit.")] public int MaxTimes { get; set; } + [Serialize(true, IsPropertySaveable.Yes, description: "By default, jumping to another part in the event closes the active conversation prompt. Use this if if you want to keep it open instead.")] + public bool EndConversation { get; set; } + private int counter; public GoTo(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index a423ce459..bef4acc57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -213,7 +213,7 @@ namespace Barotrauma return false; } } - if (!locationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && locationTypes.Contains("AnyOutpost".ToIdentifier()))) + if (!locationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && locationTypes.Contains(Tags.AnyOutpost))) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs index 98dcfd8f7..48ef33028 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs @@ -1,4 +1,4 @@ -namespace Barotrauma +namespace Barotrauma { /// @@ -31,6 +31,11 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.", contentPackage: element.ContentPackage); } + if (Operation == OperationType.Add && State == 0) + { + DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to add 0 to the mission state, which will do nothing.", + contentPackage: element.ContentPackage); + } } public override bool IsFinished(ref string goTo) @@ -55,7 +60,7 @@ namespace Barotrauma mission.State = State; break; case OperationType.Add: - mission.State += 1; + mission.State += State; break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index 7269d169f..8be03f728 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -42,13 +42,13 @@ namespace Barotrauma { if (isFinished) { return; } - bool isPlayerTeam = TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2; + bool isPlayerTeam = TeamID is CharacterTeamType.Team1 or CharacterTeamType.Team2; - affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); - foreach (var npc in affectedNpcs) + affectedNpcs = ParentEvent.GetTargets(NPCTag).OfType().ToList(); + foreach (Character npc in affectedNpcs) { // characters will still remain on friendlyNPC team for rest of the tick - npc.SetOriginalTeam(TeamID); + npc.SetOriginalTeamAndChangeTeam(TeamID); foreach (Item item in npc.Inventory.AllItems) { var idCard = item.GetComponent(); @@ -61,26 +61,43 @@ namespace Barotrauma } } } - if (AddToCrew && isPlayerTeam) + if (GameMain.GameSession.CrewManager is CrewManager crewManager) { - npc.Info.StartItemsGiven = true; - GameMain.GameSession.CrewManager.AddCharacter(npc); - ChangeItemTeam(Submarine.MainSub, true); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (AddToCrew && isPlayerTeam) { - GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamID, npc.Inventory.AllItems)); - } - } - else if (RemoveFromCrew && (npc.TeamID == CharacterTeamType.Team1 || npc.TeamID == CharacterTeamType.Team2)) - { - npc.Info.StartItemsGiven = true; - GameMain.GameSession.CrewManager.RemoveCharacter(npc, removeInfo: true); - var sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID); - ChangeItemTeam(sub, false); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.AllItems)); + if (npc.Info is CharacterInfo info) + { + info.StartItemsGiven = true; + crewManager.AddCharacter(npc); + } + else + { + DebugConsole.AddWarning($"Attempted to change the team of a character ({npc.Name}) that doesn't have Character Info. Can't add to the crew."); + } + ChangeItemTeam(Submarine.MainSub ?? Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID), allowStealing: true); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamID, npc.Inventory.AllItems)); + } } + else if (RemoveFromCrew && npc.TeamID is CharacterTeamType.Team1 or CharacterTeamType.Team2) + { + if (npc.Info is CharacterInfo info) + { + info.StartItemsGiven = true; + crewManager.RemoveCharacter(npc, removeInfo: true); + } + else + { + DebugConsole.AddWarning($"Attempted to change the team of a character ({npc.Name}) that doesn't have Character Info. Can't remove from the crew."); + } + Submarine sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID); + ChangeItemTeam(sub, false); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.AllItems)); + } + } } void ChangeItemTeam(Submarine sub, bool allowStealing) @@ -98,7 +115,7 @@ namespace Barotrauma } } WayPoint subWaypoint = - WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human && wp.AssignedJob == npc.Info.Job?.Prefab) ?? + WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human && wp.AssignedJob == npc.Info?.Job?.Prefab) ?? WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human); if (subWaypoint != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs index 472b60461..a6488bf28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs @@ -86,7 +86,7 @@ namespace Barotrauma { foreach (var objective in humanAiController.ObjectiveManager.Objectives) { - if (objective is AIObjectiveOperateItem operateItemObjective && operateItemObjective.OperateTarget == target) + if (objective is AIObjectiveOperateItem operateItemObjective && operateItemObjective.Component.Item == target) { objective.Abandon = true; } @@ -115,7 +115,7 @@ namespace Barotrauma if (npc.Removed || npc.AIController is not HumanAIController humanAiController) { continue; } foreach (var operateItemObjective in humanAiController.ObjectiveManager.GetActiveObjectives()) { - if (operateItemObjective.OperateTarget == target) + if (operateItemObjective.Component.Item == target) { operateItemObjective.Abandon = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index 75c446b46..7b1680303 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -25,7 +25,7 @@ namespace Barotrauma { ItemIdentifiers = element.GetAttributeString("itemidentifier", element.GetAttributeString("identifier", string.Empty)); } - itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers().ToImmutableHashSet(); + itemIdentifierSplit = ItemIdentifiers.ToIdentifiers().ToImmutableHashSet(); } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 507d547bc..9206b3bb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -17,6 +17,8 @@ namespace Barotrauma MainSub, Outpost, MainPath, + Cave, + AbyssCave, Ruin, Wreck, BeaconStation, @@ -38,6 +40,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item to spawn.")] public Identifier ItemIdentifier { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item to spawn.")] + public Identifier ItemTag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "The spawned entity will be assigned this tag. The tag can be used to refer to the entity by other actions of the event.")] public Identifier TargetTag { get; set; } @@ -67,6 +72,9 @@ namespace Barotrauma [Serialize(1, IsPropertySaveable.Yes, description: "Number of entities to spawn.")] public int Amount { get; set; } + [Serialize(true, IsPropertySaveable.Yes, description: "Should the item be spawned even if the target inventory is full (just spawning it at the position of the target)? Only valid if spawning an item in an inventory.")] + public bool SpawnIfInventoryFull { get; set; } + [Serialize(100.0f, IsPropertySaveable.Yes, description: "Random offset to add to the spawn position.")] public float Offset { get; set; } @@ -94,6 +102,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, description: "If disabled, the action will choose a spawn position away from players' views if one is available.")] public bool AllowInPlayerView { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the event continue even if the entity failed to spawn for whatever reason?")] + public bool ContinueIfFailedToSpawn { get; set; } + private bool spawned; private Entity spawnedEntity; @@ -115,9 +126,9 @@ namespace Barotrauma public override bool IsFinished(ref string goTo) { - if (spawnedEntity != null) + if (spawnedEntity != null || ContinueIfFailedToSpawn) { - return true; + return spawned; } else { @@ -176,7 +187,11 @@ namespace Barotrauma { if (newCharacter == null) { return; } newCharacter.HumanPrefab = humanPrefab; - newCharacter.TeamID = TeamID; + //don't set the TeamID directly: we want to leave the character's original team untouched, + //so they can behave offensively (and otherwise act "normally") if we spawn them in a hostile team inside a sub/outpost that doesn't belong to that team + + //process the team change immediately in case the character is killed or made unconscious by the event (in which case the team change would not be processed) + newCharacter.SetOriginalTeamAndChangeTeam(TeamID, processImmediately: true); newCharacter.EnableDespawn = false; humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint); if (LootingIsStealing) @@ -233,74 +248,82 @@ namespace Barotrauma } } } - else if (!ItemIdentifier.IsEmpty) + else if (!ItemIdentifier.IsEmpty || !ItemTag.IsEmpty) { - if (MapEntityPrefab.FindByIdentifier(ItemIdentifier) is not ItemPrefab itemPrefab) + ItemPrefab itemPrefab = null; + if (!ItemIdentifier.IsEmpty) { - DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)", - contentPackage: ParentEvent.Prefab.ContentPackage); - } - else - { - Inventory spawnInventory = null; - if (!TargetInventory.IsEmpty) + itemPrefab = MapEntityPrefab.FindByIdentifier(ItemIdentifier) as ItemPrefab; + if (itemPrefab == null) { - var targets = ParentEvent.GetTargets(TargetInventory); - if (targets.Any()) - { - var target = targets.First(t => t is Item || t is Character); - if (target is Character character) - { - spawnInventory = character.Inventory; - } - else if (target is Item item) - { - spawnInventory = item.OwnInventory; - } - } + DebugConsole.ThrowError($"Error in SpawnAction (item prefab \"{ItemIdentifier}\" not found)", + contentPackage: ParentEvent.Prefab.ContentPackage); + } + } + else if (!ItemTag.IsEmpty) + { + itemPrefab = ItemPrefab.Prefabs.Where(ip => ip.Tags.Contains(ItemTag)).GetRandom(Rand.RandSync.Unsynced); + } - if (spawnInventory == null) + Inventory spawnInventory = null; + if (!TargetInventory.IsEmpty) + { + var targets = ParentEvent.GetTargets(TargetInventory); + if (targets.Any()) + { + var target = targets.First(t => t is Item || t is Character); + if (target is Character character) { - DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found.", - contentPackage: ParentEvent.Prefab.ContentPackage); + spawnInventory = character.Inventory; + } + else if (target is Item item) + { + spawnInventory = item.OwnInventory; } } if (spawnInventory == null) { - ISpatialEntity spawnPos = GetSpawnPos(); - if (spawnPos != null) - { - for (int i = 0; i < Amount; i++) - { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned); - } - } + DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found.", + contentPackage: ParentEvent.Prefab.ContentPackage); } - else + } + + if (spawnInventory == null) + { + ISpatialEntity spawnPos = GetSpawnPos(); + if (spawnPos != null) { for (int i = 0; i < Amount; i++) { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned); - + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned); } } - void onSpawned(Item newItem) - { - if (newItem != null) - { - if (!TargetTag.IsEmpty) - { - ParentEvent.AddTarget(TargetTag, newItem); - } - if (IgnoreByAI) - { - newItem.AddTag("ignorebyai"); - } - } - spawnedEntity = newItem; - } } + else + { + for (int i = 0; i < Amount; i++) + { + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, spawnIfInventoryFull: SpawnIfInventoryFull, onSpawned: onSpawned); + + } + } + void onSpawned(Item newItem) + { + if (newItem != null) + { + if (!TargetTag.IsEmpty) + { + ParentEvent.AddTarget(TargetTag, newItem); + } + if (IgnoreByAI) + { + newItem.AddTag("ignorebyai"); + } + } + spawnedEntity = newItem; + } + } spawned = true; @@ -353,8 +376,7 @@ namespace Barotrauma { SpawnLocationType.Any => true, SpawnLocationType.MainSub => submarine == Submarine.MainSub, - SpawnLocationType.NearMainSub => submarine == null, - SpawnLocationType.MainPath => submarine == null, + SpawnLocationType.NearMainSub or SpawnLocationType.MainPath or SpawnLocationType.Cave or SpawnLocationType.AbyssCave => submarine == null, SpawnLocationType.Outpost => submarine is { Info.IsOutpost: true }, SpawnLocationType.Wreck => submarine is { Info.IsWreck: true }, SpawnLocationType.Ruin => submarine is { Info.IsRuin: true }, @@ -443,10 +465,25 @@ namespace Barotrauma return potentialSpawnPoints.GetRandomUnsynced(); } - if (spawnLocation == SpawnLocationType.MainPath || spawnLocation == SpawnLocationType.NearMainSub) + switch (spawnLocation) { - validSpawnPoints = validSpawnPoints.Where(p => - Submarine.Loaded.None(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(p.WorldPosition))); + case SpawnLocationType.MainPath: + case SpawnLocationType.NearMainSub: + validSpawnPoints = validSpawnPoints.Where(p => + Submarine.Loaded.None(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(p.WorldPosition))); + if (Level.Loaded != null) + { + validSpawnPoints = validSpawnPoints.Where(p => + p.WorldPosition.Y > Level.Loaded.AbyssStart && + p.Cave == null && p.Ruin == null); + } + break; + case SpawnLocationType.Cave: + validSpawnPoints = validSpawnPoints.Where(p => p.WorldPosition.Y > Level.Loaded.AbyssStart && p.Cave != null); + break; + case SpawnLocationType.AbyssCave: + validSpawnPoints = validSpawnPoints.Where(p => p.WorldPosition.Y < Level.Loaded.AbyssStart && p.Cave != null); + break; } //avoid using waypoints if there's any actual spawnpoints available diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index e607b2b7c..d448ad904 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -11,9 +11,10 @@ namespace Barotrauma /// class TagAction : EventAction { - public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 } + public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8, Enemy = 16, Ruin = 32 } + public enum CharacterTeam { Any = 0, None = 1, Team1 = 2, Team2 = 4, FriendlyNPC = 8 } - [Serialize("", IsPropertySaveable.Yes, description: "What criteria to use to select the entities to target. Valid values are players, player, traitor, nontraitor, nontraitorplayer, bot, crew, humanprefabidentifier:[id], jobidentifier:[id], structureidentifier:[id], structurespecialtag:[tag], itemidentifier:[id], itemtag:[tag], hull, hullname:[name], submarine:[type], eventtag:[tag].")] + [Serialize("", IsPropertySaveable.Yes, description: "What criteria to use to select the entities to target. Valid values are players, player, traitor, nontraitor, nontraitorplayer, bot, crew, humanprefabidentifier:[id], jobidentifier:[id], structureidentifier:[id], structurespecialtag:[tag], itemidentifier:[id], itemtag:[tag], hull, hullname:[name], submarine:[type], eventtag:[tag], speciesname:[id].")] public string Criteria { get; set; } [Serialize("", IsPropertySaveable.Yes, description: "The tag to apply to the target.")] @@ -22,6 +23,9 @@ namespace Barotrauma [Serialize(SubType.Any, IsPropertySaveable.Yes, description: "The type of submarine the target needs to be in.")] public SubType SubmarineType { get; set; } + [Serialize(CharacterTeam.Any, IsPropertySaveable.Yes, description: "The team the target needs to be on.")] + public CharacterTeam Team { get; set; } + [Serialize("", IsPropertySaveable.Yes, "If set, the target must be in an outpost module that has this tag.")] public Identifier RequiredModuleTag { get; set; } @@ -34,6 +38,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "If there are multiple matching targets, should all of them be tagged or one chosen randomly?")] public bool ChooseRandom { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "If choosing a random target, targets with this tag can optionally be excluded.")] + public Identifier ChooseRandomExcludingTag { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the event continue if the TagAction can't find any valid targets?")] public bool ContinueIfNoTargetsFound { get; set; } @@ -78,6 +85,7 @@ namespace Barotrauma ("hullname", TagHullsByName), ("submarine", TagSubmarinesByType), ("eventtag", TagByEventTag), + ("speciesname", TagBySpeciesName) }.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary(); } @@ -87,9 +95,16 @@ namespace Barotrauma } public override void Reset() { + taggingDone = false; + cantFindTargets = false; isFinished = false; } + private void TagBySpeciesName(Identifier speciesName) + { + AddTarget(Tag, Character.CharacterList.Where(c => c.SpeciesName == speciesName && CharacterTeamMatches(c))); + } + private void TagByEventTag(Identifier eventTag) { AddTarget(Tag, ParentEvent.GetTargets(eventTag).Where(t => MatchesRequirements(t))); @@ -100,7 +115,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Character, - e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters)); + e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) && CharacterTeamMatches(c)); } private void TagTraitors() @@ -151,7 +166,7 @@ namespace Barotrauma private void TagHumansByIdentifier(Identifier identifier) { - AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier)); + AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier && CharacterTeamMatches(c))); } private void TagHumansByTag(Identifier tag) @@ -161,7 +176,7 @@ namespace Barotrauma private void TagHumansByJobIdentifier(Identifier jobIdentifier) { - AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier))); + AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier) && CharacterTeamMatches(c))); } private void TagStructuresByIdentifier(Identifier identifier) @@ -233,7 +248,7 @@ namespace Barotrauma private bool MatchesRequirements(Entity e) { - return ModuleTagMatches(e) && SubmarineTypeMatches(e.Submarine); + return ModuleTagMatches(e) && SubmarineTypeMatches(e as Submarine ?? e.Submarine); } private bool ModuleTagMatches(Entity e) @@ -267,6 +282,23 @@ namespace Barotrauma return hull != null && hull.OutpostModuleTags.Contains(RequiredModuleTag); } + private bool CharacterTeamMatches(Character character) + { + if (Team == CharacterTeam.Any) { return true; } + switch (Team) + { + case CharacterTeam.None: + return character.TeamID == CharacterTeamType.None; + case CharacterTeam.Team1: + return character.TeamID == CharacterTeamType.Team1; + case CharacterTeam.Team2: + return character.TeamID == CharacterTeamType.Team2; + case CharacterTeam.FriendlyNPC: + return character.TeamID == CharacterTeamType.FriendlyNPC; + default: + return false; + } + } private bool SubmarineTypeMatches(Submarine sub) { @@ -280,7 +312,7 @@ namespace Barotrauma switch (sub.Info.Type) { case Barotrauma.SubmarineType.Player: - return submarineType.HasFlag(SubType.Player) && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle; + return submarineType.HasFlag(SubType.Player) && !sub.IsRespawnShuttle; case Barotrauma.SubmarineType.Outpost: case Barotrauma.SubmarineType.OutpostModule: return submarineType.HasFlag(SubType.Outpost); @@ -288,6 +320,10 @@ namespace Barotrauma return submarineType.HasFlag(SubType.Wreck); case Barotrauma.SubmarineType.BeaconStation: return submarineType.HasFlag(SubType.BeaconStation); + case Barotrauma.SubmarineType.EnemySubmarine: + return submarineType.HasFlag(SubType.Enemy); + case Barotrauma.SubmarineType.Ruin: + return submarineType.HasFlag(SubType.Ruin); default: return false; } @@ -357,6 +393,11 @@ namespace Barotrauma private void TagRandom(Identifier tag, IEnumerable entities) { + if (!ChooseRandomExcludingTag.IsEmpty) + { + var excludedTargets = ParentEvent.GetTargets(ChooseRandomExcludingTag); + entities = entities.Except(excludedTargets); + } if (entities.None()) { cantFindTargets = true; @@ -414,7 +455,7 @@ namespace Barotrauma public override string ToDebugString() { - return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TagAction)} -> (Criteria: {Criteria.ColorizeObject()}, Tag: {Tag.ColorizeObject()}, Sub: {SubmarineType.ColorizeObject()})"; + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TagAction)} -> (Criteria: {Criteria.ColorizeObject()}, Tag: {Tag.ColorizeObject()}, Sub: {SubmarineType.ColorizeObject()}, Team: {Team.ColorizeObject()})"; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 69b1b1a21..071325db2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; @@ -56,7 +56,16 @@ namespace Barotrauma private float distance; - public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (element.GetAttribute(nameof(TagAction.IgnoreIncapacitatedCharacters)) != null) + { + DebugConsole.AddWarning( + $"Potential error in {nameof(TriggerAction)}, event \"{parentEvent.Prefab.Identifier}\": "+ + $"{nameof(TagAction.IgnoreIncapacitatedCharacters)} is a property of {nameof(TagAction)}, did you mean {nameof(DisableIfTargetIncapacitated)}?", + contentPackage: element.ContentPackage); + } + } private bool isFinished = false; public override bool IsFinished(ref string goTo) @@ -157,13 +166,13 @@ namespace Barotrauma Item item = null; if (e1 is Character char1) { - if (char1.IsBot) - { - npc ??= char1; + if (char1.IsPlayer) + { + player = char1; } - else - { - player = char1; + else + { + npc ??= char1; } } else @@ -172,13 +181,13 @@ namespace Barotrauma } if (e2 is Character char2) { - if (char2.IsBot) - { - npc ??= char2; - } - else + if (char2.IsPlayer) { - player = char2; + player = char2; + } + else + { + npc ??= char2; } } else @@ -190,6 +199,10 @@ namespace Barotrauma { if (npc != null) { + if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc)) + { + npcsOrItems.Add(npc); + } if (npc.CampaignInteractionType == CampaignMode.InteractionType.Talk) { //if the NPC has a conversation available, don't assign the trigger until the conversation is done @@ -197,10 +210,6 @@ namespace Barotrauma } else if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine) { - if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc)) - { - npcsOrItems.Add(npc); - } npc.CampaignInteractionType = CampaignMode.InteractionType.Examine; npc.RequireConsciousnessForCustomInteract = DisableIfTargetIncapacitated; #if CLIENT @@ -339,7 +348,7 @@ namespace Barotrauma return true; } } - else if (c.AIController is EnemyAIController enemyAI && (enemyAI.State == AIState.Aggressive || enemyAI.State == AIState.Attack)) + else if (c.AIController is EnemyAIController { State: AIState.Aggressive or AIState.Attack } enemyAI) { if (enemyAI.SelectedAiTarget?.Entity == character || c.CurrentHull == character.CurrentHull) { @@ -401,10 +410,18 @@ namespace Barotrauma { if (TargetModuleType.IsEmpty) { + string targetStr = "none"; + if (npcsOrItems.Any()) + { + targetStr = string.Join(", ", + npcsOrItems.Select(npcOrItem => + npcOrItem.TryGet(out Character character) ? character.Name : (npcOrItem.TryGet(out Item item) ? item.Name : "none"))); + } + return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (" + (WaitForInteraction ? - $"Selected non-player target: {(npcsOrItems?.ToString() ?? "").ColorizeObject()}, " : + $"Selected non-player target: {targetStr.ColorizeObject()}, " : $"Distance: {((int)distance).ColorizeObject()}, ") + $"Radius: {Radius.ColorizeObject()}, " + $"TargetTags: {Target1Tag.ColorizeObject()}, " + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs index e75d563c2..8a770ba75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs @@ -1,13 +1,16 @@ namespace Barotrauma { /// - /// Triggers another scripted event. + /// Triggers another event (can also trigger things other than scripted events, for example monster events). /// class TriggerEventAction : EventAction { [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the event to trigger.")] public Identifier Identifier { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the event to trigger.")] + public Identifier EventTag { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the event will trigger at the beginning of the next round. Useful for e.g. triggering some scripted event in the outpost after you finish a mission.")] public bool NextRound { get; set; } @@ -36,13 +39,8 @@ } else { - var eventPrefab = EventSet.GetEventPrefab(Identifier); - if (eventPrefab == null) - { - DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.", - contentPackage: ParentEvent.Prefab.ContentPackage); - } - else + EventPrefab eventPrefab = EventPrefab.FindEventPrefab(Identifier, EventTag, ParentEvent.Prefab.ContentPackage); + if (eventPrefab != null) { var ev = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); if (ev != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 83b2ff4d3..4f62d0ec0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -180,11 +180,40 @@ namespace Barotrauma random = new MTRandom(RandomSeed); bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode; - EventSet initialEventSet = SelectRandomEvents( - EventSet.Prefabs.ToList(), - requireCampaignSet: playingCampaign, - random: random); + + //ensure that the sets that have been configured to be always selected get selected if there's any available + EventSet initialEventSet = null; EventSet additiveSet = null; + var selectAlwaysEventSets = GetAllowedEventSets(EventSet.Prefabs.ToList(), requireCampaignSet: playingCampaign).Where(s => s.SelectAlways); + foreach (var eventSet in selectAlwaysEventSets) + { + if (eventSet.Additive) + { + additiveSet = eventSet; + } + else + { + if (initialEventSet == null) + { + initialEventSet = eventSet; + } + else //initial set already chosen, ignore this one + { + continue; + } + } + AddSet(eventSet); + } + + if (initialEventSet == null) + { + initialEventSet = SelectRandomEvents( + EventSet.Prefabs.ToList(), + requireCampaignSet: playingCampaign, + random: random); + } + + //we happened to choose an additive set as the initial one, choose an additive one too if (initialEventSet != null && initialEventSet.Additive) { additiveSet = initialEventSet; @@ -193,15 +222,15 @@ namespace Barotrauma requireCampaignSet: playingCampaign, random: random); } - if (initialEventSet != null) + + if (initialEventSet != null) { AddSet(initialEventSet); } + if (additiveSet != null) { AddSet(additiveSet); } + + void AddSet(EventSet eventSet) { - pendingEventSets.Add(initialEventSet); - CreateEvents(initialEventSet); - } - if (additiveSet != null) - { - pendingEventSets.Add(additiveSet); - CreateEvents(additiveSet); + if (pendingEventSets.Contains(eventSet)) { return; } + pendingEventSets.Add(eventSet); + CreateEvents(eventSet); } if (level?.LevelData != null) @@ -244,7 +273,7 @@ namespace Barotrauma while (QueuedEventsForNextRound.TryDequeue(out var id)) { - var eventPrefab = EventSet.GetEventPrefab(id); + var eventPrefab = EventSet.GetEventPrefab(id) ?? EventSet.GetAllEventPrefabs().Where(e => e.Tags.Contains(id)).GetRandomUnsynced(); if (eventPrefab == null) { DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}."); @@ -609,12 +638,11 @@ namespace Barotrauma } } - private EventSet SelectRandomEvents(IReadOnlyList eventSets, bool? requireCampaignSet = null, Random random = null) + private IEnumerable GetAllowedEventSets(IReadOnlyList eventSets, bool? requireCampaignSet = null) { - if (level == null) { return null; } - Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed)); + if (level == null) { return Enumerable.Empty(); } - var allowedEventSets = + var allowedEventSets = eventSets.Where(set => IsValidForLevel(set, level)); if (requireCampaignSet.HasValue) @@ -659,6 +687,12 @@ namespace Barotrauma // When there are no forced sets, only allow sets that aren't forced at any specific location allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr < 0 && set.ForceAtVisitedNr < 0); } + return allowedEventSets; + } + + private EventSet SelectRandomEvents(IReadOnlyList eventSets, bool? requireCampaignSet = null, Random random = null) + { + var allowedEventSets = GetAllowedEventSets(eventSets, requireCampaignSet); if (allowedEventSets.Count() == 1) { @@ -666,6 +700,7 @@ namespace Barotrauma return allowedEventSets.First(); } + Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed)); float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); float randomNumber = (float)rand.NextDouble(); randomNumber *= totalCommonness; @@ -692,6 +727,7 @@ namespace Barotrauma return (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && (e.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(e.RequiredLayer)) && + (e.RequiredSpawnPointTag.IsEmpty || WayPoint.WayPointList.Any(wp => wp.Tags.Contains(e.RequiredSpawnPointTag))) && !level.LevelData.NonRepeatableEvents.Contains(e.Identifier); } @@ -706,6 +742,7 @@ namespace Barotrauma level.IsAllowedDifficulty(eventSet.MinLevelDifficulty, eventSet.MaxLevelDifficulty) && level.LevelData.Type == eventSet.LevelType && (eventSet.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(eventSet.RequiredLayer)) && + (eventSet.RequiredSpawnPointTag.IsEmpty || WayPoint.WayPointList.Any(wp => wp.Tags.Contains(eventSet.RequiredSpawnPointTag))) && (eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier); } @@ -965,9 +1002,9 @@ namespace Barotrauma monsterStrength += enemyAI.CombatStrength; } - if (character.CurrentHull?.Submarine?.Info != null && - (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) && - character.CurrentHull.Submarine.Info.Type == SubmarineType.Player) + if (Submarine.MainSub != null && + character.CurrentHull?.Submarine.Info is { Type: SubmarineType.Player } && + (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine))) { // Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42 enemyDanger += enemyAI.CombatStrength / 500.0f; @@ -1128,6 +1165,8 @@ namespace Barotrauma } } #else + if (refEntity == null) { return null; } + foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients) { if (client.Character == null) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index afc0b5645..890d79131 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -1,4 +1,6 @@ -using System; +using Barotrauma.Extensions; +using System; +using System.Collections.Immutable; using System.Linq; using System.Reflection; @@ -11,6 +13,9 @@ namespace Barotrauma public readonly ContentXElement ConfigElement; public readonly Type EventType; + private readonly ImmutableHashSet tags; + public ImmutableHashSet Tags => tags; + /// /// The probability for the event to do something if it gets selected. For example, the probability for a MonsterEvent to spawn the monster(s). /// @@ -37,6 +42,11 @@ namespace Barotrauma /// public readonly Identifier RequiredLayer; + /// + /// If set, this spawn point tag must be present somewhere in the level. + /// + public readonly Identifier RequiredSpawnPointTag; + /// /// If set, the event set can only be chosen in locations that belong to this faction. /// @@ -93,6 +103,7 @@ namespace Barotrauma Name = TextManager.Get($"eventname.{Identifier}").Fallback(Identifier.ToString()); + tags = ConfigElement.GetAttributeIdentifierImmutableHashSet(nameof(tags), ImmutableHashSet.Empty); BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty); Faction = ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); @@ -100,6 +111,7 @@ namespace Barotrauma TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent)); RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty); + RequiredSpawnPointTag = element.GetAttributeIdentifier(nameof(RequiredSpawnPointTag), Identifier.Empty); UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false); UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip"); @@ -146,5 +158,39 @@ namespace Barotrauma unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == biomeIdentifier) ?? unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == Identifier.Empty); } + + /// + /// Finds an event prefab with the specified identifier, or if it isn't defined, a random event prefab with the specified tag. + /// + /// Which content package is trying to find the event (if any)? Only used for logging error messages. + /// + public static EventPrefab FindEventPrefab(Identifier identifier, Identifier tag, ContentPackage source) + { + EventPrefab eventPrefab = null; + if (!identifier.IsEmpty) + { + eventPrefab = EventSet.GetEventPrefab(identifier); + if (eventPrefab == null) + { + DebugConsole.ThrowError($"Failed to find an event prefab with the identifier {identifier}.", + contentPackage: source); + } + } + else if (!tag.IsEmpty) + { + eventPrefab = EventSet.GetAllEventPrefabs().Where(e => e.Tags.Contains(tag)).GetRandomUnsynced(); + if (eventPrefab == null) + { + DebugConsole.ThrowError($"Failed to find an event prefab with the tag {tag}.", + contentPackage: source); + } + } + else + { + DebugConsole.ThrowError($"Failed to find an event prefab: neither an identifier or tag were defined.", + contentPackage: source); + } + return eventPrefab; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index b1354868c..0c201b596 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -73,7 +73,7 @@ namespace Barotrauma public static void AddSetEventPrefabsToList(List list, EventSet set) { - list.AddRange(set.EventPrefabs.SelectMany(ep => ep.EventPrefabs)); + list.AddRange(set.EventPrefabs.SelectMany(ep => ep.EventPrefabs).Where(ep => !list.Contains(ep))); foreach (var childSet in set.ChildSets) { AddSetEventPrefabsToList(list, childSet); } } @@ -111,6 +111,11 @@ namespace Barotrauma /// public readonly Identifier RequiredLayer; + /// + /// If set, this spawn point tag must be present somewhere in the level. + /// + public readonly Identifier RequiredSpawnPointTag; + /// /// If set, the event set can only be chosen in locations of this type. /// @@ -207,7 +212,14 @@ namespace Barotrauma /// monsters in addition to the vanilla monsters spawned by vanilla sets, without you having to add your custom monsters to every single vanilla set. /// public readonly bool Additive; - + + /// + /// This will force the game to always choose this event set if it's suitable for the current level. + /// If the set is additive, it is guaranteed to get chosen regardless of what other sets get selected. + /// If the set is NOT additive, the game will choose the first available non-additive set that is configured to be always selected. + /// + public readonly bool SelectAlways; + /// /// The commonness of the event set (i.e. how likely it is for this specific set to be chosen). /// @@ -349,7 +361,8 @@ namespace Barotrauma MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0); MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty); - Additive = element.GetAttributeBool("additive", false); + Additive = element.GetAttributeBool(nameof(Additive), false); + SelectAlways = element.GetAttributeBool(nameof(SelectAlways), false); string levelTypeStr = element.GetAttributeString("leveltype", parentSet?.LevelType.ToString() ?? "LocationConnection"); if (!Enum.TryParse(levelTypeStr, true, out LevelType)) @@ -392,6 +405,7 @@ namespace Barotrauma CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), parentSet?.CampaignTutorialOnly ?? false); RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty); + RequiredSpawnPointTag = element.GetAttributeIdentifier(nameof(RequiredSpawnPointTag), Identifier.Empty); ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1); ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 53ead40a1..5b0a2730f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -27,9 +27,9 @@ namespace Barotrauma private const float EndDelay = 5.0f; private float endTimer; - private bool allowOrderingRescuees; + private readonly bool allowOrderingRescuees; - public override bool AllowRespawn => false; + public override bool AllowRespawning => false; public override bool AllowUndocking { @@ -233,7 +233,18 @@ namespace Barotrauma bool requiresRescue = element.GetAttributeBool("requirerescue", false); var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None); - Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos); + var originalTeam = Level.Loaded.StartOutpost?.TeamID ?? teamId; + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, originalTeam, spawnPos); + //consider the NPC to be "originally" from the team of the outpost it spawns in, and change it to the desired (hostile) team afterwards + //that allows the NPC to fight intruders and otherwise function in the outpost if the mission is configured to spawn the hostile NPCs in a friendly outpost + if (teamId != originalTeam) + { + spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId); + } + if (element.GetAttribute("color") != null) + { + spawnedCharacter.UniqueNameColor = element.GetAttributeColor("color", Color.Red); + } if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) { outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier); @@ -265,11 +276,7 @@ namespace Barotrauma .WithManualPriority(CharacterInfo.HighestManualOrderPriority); spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false); } - - if (element.GetAttributeBool("requirekill", false)) - { - requireKill.Add(spawnedCharacter); - } + InitCharacter(spawnedCharacter, element); } private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine) @@ -280,10 +287,6 @@ namespace Barotrauma spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); characters.Add(spawnedCharacter); - if (element.GetAttributeBool("requirekill", false)) - { - requireKill.Add(spawnedCharacter); - } if (spawnedCharacter.Inventory != null) { characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); @@ -297,9 +300,31 @@ namespace Barotrauma enemyAi.UnattackableSubmarines.Add(sub); } } + InitCharacter(spawnedCharacter, element); } - - + + private void InitCharacter(Character character, XElement element) + { + if (element.GetAttributeBool("requirekill", false)) + { + requireKill.Add(character); + } + float playDeadProbability = element.GetAttributeFloat("playdeadprobability", -1); + if (playDeadProbability >= 0) + { + character.EvaluatePlayDeadProbability(playDeadProbability); + } + float huskProbability = element.GetAttributeFloat("huskprobability", 0); + if (huskProbability > 0 && Rand.Value() <= huskProbability) + { + character.TurnIntoHusk(); + } + else if (element.GetAttributeBool("corpse", false)) + { + character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); + } + } + protected override void UpdateMissionSpecific(float deltaTime) { if (State != HostagesKilledState) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index f4c2a781c..64bf201c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -1,5 +1,6 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -10,12 +11,59 @@ namespace Barotrauma private readonly LocalizedString[] descriptions; private static LocalizedString[] teamNames = { "Team A", "Team B" }; - public override bool AllowRespawn + private readonly bool allowRespawning; + + enum WinCondition { - get { return false; } + /// + /// The winner is the team with the last living player(s) + /// + LastManStanding, + /// + /// The team who reaches a specific number of kills (determined by WinScore) is the winner + /// + KillCount, + /// + /// The team who controls a specific submarine (can be a ruin, outpost or a beacon station too) for some time (determined by WinScore) is the winner + /// + ControlSubmarine } - private CharacterTeamType Winner + private readonly WinCondition winCondition; + + public override bool AllowRespawning + { + get => allowRespawning; + } + + private Submarine targetSubmarine; + + private LocalizedString targetSubmarineSonarLabel; + + /// + /// Which type of submarine the team needs to stay in control of to win + /// + public TagAction.SubType TargetSubmarineType { get; set; } + + public readonly int PointsPerKill; + + /// + /// The score required to win the mission. + /// + public int WinScore => GameMain.NetworkMember?.ServerSettings.WinScorePvP ?? 10; + + /// + /// Is the winner determined by some kind of a scoring mechanism? + /// + public bool HasWinScore => + winCondition != WinCondition.LastManStanding || PointsPerKill != 0; + + /// + /// Scores of both teams. What the scoring represents depends on how the mission is configured (kills, time in control of a beacon station?) + /// + public readonly int[] Scores = new int[2]; + + public static CharacterTeamType Winner { get { @@ -46,6 +94,27 @@ namespace Barotrauma public CombatMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { + allowRespawning = prefab.ConfigElement.GetAttributeBool(nameof(AllowRespawning), false); + + winCondition = prefab.ConfigElement.GetAttributeEnum(nameof(WinCondition), + allowRespawning ? WinCondition.KillCount : WinCondition.LastManStanding); + + PointsPerKill = prefab.ConfigElement.GetAttributeInt(nameof(PointsPerKill), 0); + + TargetSubmarineType = prefab.ConfigElement.GetAttributeEnum(nameof(TargetSubmarineType), TagAction.SubType.Any); + + string sonarTag = prefab.ConfigElement.GetAttributeString(nameof(targetSubmarineSonarLabel), string.Empty); + if (!sonarTag.IsNullOrEmpty()) + { + targetSubmarineSonarLabel = TextManager.Get(sonarTag); + } + + if (allowRespawning && winCondition == WinCondition.LastManStanding) + { + DebugConsole.ThrowError($"Error in mission {prefab.Identifier}: win condition cannot be \"last man standing\" when respawning is enabled.", + contentPackage: prefab.ContentPackage); + } + descriptions = new LocalizedString[] { TextManager.Get("MissionDescriptionNeutral." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("descriptionneutral", "")), @@ -57,15 +126,23 @@ namespace Barotrauma { for (int n = 0; n < 2; n++) { - descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].DisplayName); + descriptions[i] = + descriptions[i] + .Replace($"[location{n + 1}]", locations[n].DisplayName) + .Replace("[winscore]", WinScore.ToString()); } } teamNames = new LocalizedString[] { - TextManager.Get("MissionTeam1." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname1", "Team A")), - TextManager.Get("MissionTeam2." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname2", "Team B")) + TextManager.Get("MissionTeam1." + prefab.TextIdentifier).Fallback(TextManager.Get(prefab.ConfigElement.GetAttributeString("teamname1", "missionteam1.pvpmission"))), + TextManager.Get("MissionTeam2." + prefab.TextIdentifier).Fallback(TextManager.Get(prefab.ConfigElement.GetAttributeString("teamname2", "missionteam2.pvpmission"))), }; + + if (winCondition == WinCondition.KillCount && PointsPerKill == 0) + { + DebugConsole.AddWarning($"Potential error in mission {Prefab.Identifier}: win condition is kill count, but {nameof(PointsPerKill)} is set to 0."); + } } public static LocalizedString GetTeamName(CharacterTeamType teamID) @@ -82,9 +159,9 @@ namespace Barotrauma return "Invalid Team"; } - public bool IsInWinningTeam(Character character) + public static bool IsInWinningTeam(Character character) { - return character != null && + return character != null && Winner != CharacterTeamType.None && Winner == character.TeamID; } @@ -99,19 +176,32 @@ namespace Barotrauma subs = new Submarine[] { Submarine.MainSubs[0], Submarine.MainSubs[1] }; - subs[0].NeutralizeBallast(); - subs[0].TeamID = CharacterTeamType.Team1; - subs[0].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team1); + if (Prefab.LoadSubmarines) + { + subs[0].NeutralizeBallast(); + subs[0].TeamID = CharacterTeamType.Team1; + subs[0].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team1); - subs[1].NeutralizeBallast(); - subs[1].TeamID = CharacterTeamType.Team2; - subs[1].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team2); - subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition)); - subs[1].FlipX(); + subs[1].NeutralizeBallast(); + subs[1].TeamID = CharacterTeamType.Team2; + subs[1].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team2); + GameSession.PlaceSubAtInitialPosition(subs[1], level, placeAtStart: false); + subs[1].FlipX(); + } #if SERVER crews = new List[] { new List(), new List() }; roundEndTimer = RoundEndDuration; #endif + + if (TargetSubmarineType != TagAction.SubType.Any) + { + targetSubmarine = Submarine.Loaded.FirstOrDefault(s => TagAction.SubmarineTypeMatches(s, TargetSubmarineType)); + if (targetSubmarine == null) + { + DebugConsole.ThrowError($"Error in mission {Prefab.Identifier}: could not find a submarine of the type {TargetSubmarineType}.", + contentPackage: Prefab.ContentPackage); + } + } } protected override bool DetermineCompleted() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 39838a1d6..c1548aa1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -192,7 +192,6 @@ namespace Barotrauma foreach (ContentXElement element in characterConfig.Elements()) { string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty); - string colorIdentifier = element.GetAttributeString("color", string.Empty); for (int k = 0; k < scalingCharacterCount; k++) { // for each element defined, we need to initialize that type of character equal to the scaling escorted character count diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 2e324fa0b..212cf993d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -111,7 +111,7 @@ namespace Barotrauma get { return failed; } } - public virtual bool AllowRespawn + public virtual bool AllowRespawning { get { return true; } } @@ -211,21 +211,21 @@ namespace Barotrauma public virtual void SetLevel(LevelData level) { } - public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null) + public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, IEnumerable missionTypes, bool isSinglePlayer = false, float? difficultyLevel = null) { - return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer, difficultyLevel); + return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionTypes, isSinglePlayer, difficultyLevel); } - public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null) + public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, IEnumerable missionTypes, bool isSinglePlayer = false, float? difficultyLevel = null) { List allowedMissions = new List(); - if (missionType == MissionType.None) + if (missionTypes.None()) { return null; } else { - allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => m.Type.HasAnyFlag(missionType))); + allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => missionTypes.Contains(m.Type))); } allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly); if (requireCorrectLocationType) @@ -350,11 +350,12 @@ namespace Barotrauma private void TriggerEvent(MissionPrefab.TriggerEvent trigger) { if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; } - var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier == trigger.EventIdentifier); + //clients are not allowed to trigger events, they're handled by the server + if (GameMain.NetworkMember is { IsClient: true }) { return; } + EventPrefab eventPrefab = EventPrefab.FindEventPrefab(trigger.EventIdentifier, trigger.EventTag, Prefab.ContentPackage); if (eventPrefab == null) { - DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\").", - contentPackage: Prefab.ContentPackage); + DebugConsole.ThrowError($"Mission {Prefab.Identifier} failed to trigger an event (identifier: {trigger.EventIdentifier}, tag: {trigger.EventTag}).", contentPackage: Prefab.ContentPackage); return; } if (GameMain.GameSession?.EventManager != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index e0462ed76..b5cb31820 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -1,4 +1,5 @@ -using System; +using Barotrauma.Networking; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -7,52 +8,42 @@ using System.Xml.Linq; namespace Barotrauma { - [Flags] - public enum MissionType - { - None = 0x0, - Salvage = 0x1, - Monster = 0x2, - Cargo = 0x4, - Beacon = 0x8, - Nest = 0x10, - Mineral = 0x20, - Combat = 0x40, - AbandonedOutpost = 0x80, - Escort = 0x100, - Pirate = 0x200, - GoTo = 0x400, - ScanAlienRuins = 0x800, - EliminateTargets = 0x1000, - End = 0x2000, - All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | EliminateTargets | End - } - partial class MissionPrefab : PrefabWithUintIdentifier { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - public static readonly Dictionary CoOpMissionClasses = new Dictionary() + /// + /// The keys here are for backwards compatibility, tying the old mission types to the appropriate class. + /// Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string. + /// + public static readonly Dictionary CoOpMissionClasses = new Dictionary() { - { MissionType.Salvage, typeof(SalvageMission) }, - { MissionType.Monster, typeof(MonsterMission) }, - { MissionType.Cargo, typeof(CargoMission) }, - { MissionType.Beacon, typeof(BeaconMission) }, - { MissionType.Nest, typeof(NestMission) }, - { MissionType.Mineral, typeof(MineralMission) }, - { MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) }, - { MissionType.Escort, typeof(EscortMission) }, - { MissionType.Pirate, typeof(PirateMission) }, - { MissionType.GoTo, typeof(GoToMission) }, - { MissionType.ScanAlienRuins, typeof(ScanMission) }, - { MissionType.EliminateTargets, typeof(EliminateTargetsMission) }, - { MissionType.End, typeof(EndMission) } + { "Salvage".ToIdentifier(), typeof(SalvageMission) }, + { "Monster".ToIdentifier(), typeof(MonsterMission) }, + { "Cargo".ToIdentifier(), typeof(CargoMission) }, + { "Beacon".ToIdentifier(), typeof(BeaconMission) }, + { "Nest".ToIdentifier(), typeof(NestMission) }, + { "Mineral".ToIdentifier(), typeof(MineralMission) }, + { "AbandonedOutpost".ToIdentifier(), typeof(AbandonedOutpostMission) }, + { "Escort".ToIdentifier(), typeof(EscortMission) }, + { "Pirate".ToIdentifier(), typeof(PirateMission) }, + { "GoTo".ToIdentifier(), typeof(GoToMission) }, + { "ScanAlienRuins".ToIdentifier(), typeof(ScanMission) }, + { "EliminateTargets".ToIdentifier(), typeof(EliminateTargetsMission) }, + { "End".ToIdentifier(), typeof(EndMission) } }; - public static readonly Dictionary PvPMissionClasses = new Dictionary() + + /// + /// The keys here are for backwards compatibility, tying the old mission types to the appropriate class. + /// Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string. + /// + public static readonly Dictionary PvPMissionClasses = new Dictionary() { - { MissionType.Combat, typeof(CombatMission) } + { "Combat".ToIdentifier(), typeof(CombatMission) } }; + public static readonly HashSet HiddenMissionTypes = new HashSet() { "GoTo".ToIdentifier(), "End".ToIdentifier() }; + public class ReputationReward { public readonly Identifier FactionIdentifier; @@ -67,11 +58,11 @@ namespace Barotrauma } } - public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo, MissionType.End }; - private readonly ConstructorInfo constructor; - public readonly MissionType Type; + public readonly Identifier Type; + + public readonly Type MissionClass; public readonly bool MultiplayerOnly, SingleplayerOnly; @@ -122,7 +113,21 @@ namespace Barotrauma public readonly bool AllowOtherMissionsInLevel; - public readonly bool RequireWreck, RequireRuin, RequireThalamusWreck; + public readonly bool RequireWreck, RequireRuin, RequireBeaconStation, RequireThalamusWreck; + public readonly bool SpawnBeaconStationInMiddle; + + public readonly bool AllowOutpostNPCs; + + public readonly Identifier ForceOutpostGenerationParameters; + + public readonly RespawnMode? ForceRespawnMode; + + /// + /// If set, the players can choose which outpost is used for the mission (selected from the outposts that have this tag). Only works in multiplayer. + /// + public readonly Identifier AllowOutpostSelectionFromTag; + + public readonly bool LoadSubmarines = true; /// /// If enabled, locations this mission takes place in cannot change their type @@ -157,7 +162,10 @@ namespace Barotrauma public class TriggerEvent { [Serialize("", IsPropertySaveable.Yes)] - public string EventIdentifier { get; private set; } + public Identifier EventIdentifier { get; private set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier EventTag { get; private set; } [Serialize(0, IsPropertySaveable.Yes)] public int State { get; private set; } @@ -214,12 +222,16 @@ namespace Barotrauma ShowInMenus = element.GetAttributeBool("showinmenus", true); ShowStartMessage = element.GetAttributeBool("showstartmessage", true); IsSideObjective = element.GetAttributeBool("sideobjective", false); - RequireWreck = element.GetAttributeBool("requirewreck", false); - RequireRuin = element.GetAttributeBool("requireruin", false); - RequireThalamusWreck = element.GetAttributeBool("requirethalamuswreck", false); + RequireWreck = element.GetAttributeBool(nameof(RequireWreck), false); + RequireThalamusWreck = element.GetAttributeBool(nameof(RequireThalamusWreck), false); + RequireRuin = element.GetAttributeBool(nameof(RequireRuin), false); + RequireBeaconStation = element.GetAttributeBool(nameof(RequireBeaconStation), false); + SpawnBeaconStationInMiddle = element.GetAttributeBool(nameof(SpawnBeaconStationInMiddle), false); if (RequireThalamusWreck) { RequireWreck = true; } + LoadSubmarines = element.GetAttributeBool(nameof(LoadSubmarines), true); + BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false); RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty); Commonness = element.GetAttributeInt("commonness", 1); @@ -234,6 +246,15 @@ namespace Barotrauma MinLevelDifficulty = Math.Clamp(MinLevelDifficulty, 0, Math.Min(MaxLevelDifficulty, 100)); MaxLevelDifficulty = Math.Clamp(MaxLevelDifficulty, Math.Max(MinLevelDifficulty, 0), 100); + AllowOutpostNPCs = element.GetAttributeBool(nameof(AllowOutpostNPCs), true); + ForceOutpostGenerationParameters = element.GetAttributeIdentifier(nameof(ForceOutpostGenerationParameters), Identifier.Empty); + AllowOutpostSelectionFromTag = element.GetAttributeIdentifier(nameof(AllowOutpostSelectionFromTag), Identifier.Empty); + + if (element.GetAttribute(nameof(ForceRespawnMode)) != null) + { + ForceRespawnMode = element.GetAttributeEnum(nameof(ForceRespawnMode), RespawnMode.MidRound); + } + ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false); ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false); MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1); @@ -362,47 +383,23 @@ namespace Barotrauma Messages = messages.ToImmutableArray(); ReputationRewards = reputationRewards.ToImmutableList(); - Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty); - //backwards compatibility - - if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue") - { - missionTypeName = nameof(MissionType.AbandonedOutpost).ToIdentifier(); - } - else if (missionTypeName == "clearalienruins") - { - missionTypeName = nameof(MissionType.EliminateTargets).ToIdentifier(); - } - - if (!Enum.TryParse(missionTypeName.Value, true, out Type)) - { - DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type."); - return; - } - if (Type == MissionType.None) - { - DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - mission type cannot be none."); - return; - } + MissionClass = FindMissionClass(element); + Type = element.GetAttributeIdentifier(nameof(Type), Identifier.Empty); + #if DEBUG - if (Type == MissionType.Monster && SonarLabel.IsNullOrEmpty()) + if (MissionClass == typeof(MonsterMission) && SonarLabel.IsNullOrEmpty()) { DebugConsole.AddWarning($"Potential error in mission prefab \"{Identifier}\" - sonar label not set."); } #endif - if (CoOpMissionClasses.ContainsKey(Type)) + if (!LoadSubmarines && MissionClass != typeof(CombatMission)) { - constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) }); - } - else if (PvPMissionClasses.ContainsKey(Type)) - { - constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) }); - } - else - { - DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\""); + DebugConsole.AddWarning($"Potential error in mission {Identifier}: Disabling submarines is only intended for combat missions taking place in an outpost, and may lead to issues in other types of missions.", + contentPackage: element.ContentPackage); } + + constructor = FindMissionConstructor(element, MissionClass); if (constructor == null) { DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!", @@ -411,6 +408,67 @@ namespace Barotrauma InitProjSpecific(element); } + + private Type FindMissionClass(ContentXElement element) + { + Type type; + Identifier typeName = element.NameAsIdentifier(); + type = TryGetClass(typeName.RemoveFromEnd("Mission")); + + if (type == null) + { + //backwards compatibility: the actual mission class used to be defined by the "type" attribute, + //Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string, + //but if we failed to find the class based on the name, let's try the type attribute. + Identifier typeNameLegacy = (element.GetAttributeIdentifier("type", Identifier.Empty)).ToIdentifier(); + if (typeNameLegacy == "OutpostDestroy" || typeNameLegacy == "OutpostRescue") + { + typeNameLegacy = "AbandonedOutpost".ToIdentifier(); + } + else if (typeNameLegacy == "clearalienruins") + { + typeNameLegacy = "EliminateTargets".ToIdentifier(); + } + type = TryGetClass(typeNameLegacy) ?? TryGetClass(typeNameLegacy.AppendIfMissing("Mission")); + if (type == null) + { + DebugConsole.ThrowError($"Failed to find the mission type \"{typeNameLegacy}\" for the mission {Identifier}.", + contentPackage: element.ContentPackage); + return null; + } + } + + static Type TryGetClass(Identifier typeName) + { + if (CoOpMissionClasses.TryGetValue(typeName, out Type coOpMissionClass)) + { + return coOpMissionClass; + } + else if (PvPMissionClasses.TryGetValue(typeName, out Type pvpMissionClass)) + { + return pvpMissionClass; + } + return null; + } + + return type; + } + + private ConstructorInfo FindMissionConstructor(ContentXElement element, Type missionClass) + { + ConstructorInfo constructor; + if (missionClass == null) { return null; } + if (missionClass != typeof(Mission) && !missionClass.IsSubclassOf(typeof(Mission))) { return null; } + constructor = missionClass.GetConstructor(new Type[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) }); + if (constructor == null) + { + DebugConsole.ThrowError( + $"Could not find the constructor of the mission type \"{missionClass}\" for the mission {Identifier}", + contentPackage: element.ContentPackage); + return null; + } + return constructor; + } partial void InitProjSpecific(ContentXElement element); @@ -424,7 +482,7 @@ namespace Barotrauma } return AllowedLocationTypes.Any(lt => lt == "any") || - AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) || + AllowedLocationTypes.Any(lt => lt == Barotrauma.Tags.AnyOutpost && from.HasOutpost() && from.Type.IsAnyOutpost) || AllowedLocationTypes.Any(lt => lt == from.Type.Identifier); } @@ -432,11 +490,11 @@ namespace Barotrauma { if (fromType == "any" || fromType == from.Type.Identifier || - (fromType == "anyoutpost" && from.HasOutpost() && from.Type.Identifier != "abandoned")) + (fromType == Barotrauma.Tags.AnyOutpost && from.HasOutpost() && from.Type.IsAnyOutpost && from.Type.Identifier != "abandoned")) { if (toType == "any" || toType == to.Type.Identifier || - (toType == "anyoutpost" && to.HasOutpost() && to.Type.Identifier != "abandoned")) + (toType == Barotrauma.Tags.AnyOutpost && to.HasOutpost() && to.Type.IsAnyOutpost && to.Type.Identifier != "abandoned")) { return true; } @@ -461,5 +519,28 @@ namespace Barotrauma { DisposeProjectSpecific(); } + + /// + /// Returns all mission types that can be selected e.g. in the server lobby, excluding any special, hidden ones like EndMission + /// (the mission at the end of the campaign) + /// + public static IEnumerable GetAllMultiplayerSelectableMissionTypes() + { + List missionTypes = new List(); + foreach (var missionPrefab in Prefabs) + { + if (missionPrefab.Commonness <= 0.0f) { continue; } + if (missionPrefab.SingleplayerOnly) { continue; } + if (HiddenMissionTypes.Contains(missionPrefab.Type)) + { + continue; + } + if (!missionTypes.Contains(missionPrefab.Type)) + { + missionTypes.Add(missionPrefab.Type); + } + } + return missionTypes.OrderBy(t => t.Value); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 1819a821b..4144f8c34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -25,6 +25,8 @@ namespace Barotrauma private readonly List characters = new List(); private readonly Dictionary> characterItems = new Dictionary>(); + private readonly Dictionary> characterStatusEffects = new Dictionary>(); + // Update the last sighting periodically so that the players can find the pirate sub even if they have lost the track of it. private readonly float pirateSightingUpdateFrequency = 30; private float pirateSightingUpdateTimer; @@ -96,14 +98,26 @@ namespace Barotrauma characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes"); addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0); + factionIdentifier = prefab.ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty); + //make sure all referenced character types are defined foreach (XElement characterElement in characterConfig.Elements()) { - var characterId = characterElement.GetAttributeString("typeidentifier", string.Empty); - var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId); + Identifier typeId = characterElement.GetAttributeIdentifier("typeidentifier", Identifier.Empty); + if (typeId.IsEmpty) + { + if (characterElement.GetAttributeIdentifier("identifier", Identifier.Empty).IsEmpty) + { + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character element with neither a typeidentifier or identifier ({characterElement.ToString()}).", + contentPackage: Prefab.ContentPackage); + } + continue; + } + var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e => + e.GetAttributeIdentifier("typeidentifier", Identifier.Empty) == typeId); if (characterTypeElement == null) { - DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\".", + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{typeId}\".", contentPackage: Prefab.ContentPackage); } } @@ -143,37 +157,47 @@ namespace Barotrauma levelData = level; missionDifficulty = level?.Difficulty ?? 0; - XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier); - alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward); - factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", Identifier.Empty); + //no specific sub configured, choose a random one + if (submarineTypeConfig == null) + { + submarineInfo = GetRandomDifficultyModifiedSubmarine(missionDifficulty, ShipRandomnessModifier); + alternateReward = (int)submarineInfo.EnemySubmarineInfo.Reward; + } + else + { + XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier); + alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward); + factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", factionIdentifier); + + ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage); + if (submarinePath.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!", + contentPackage: Prefab.ContentPackage); + return; + } + + BaseSubFile contentFile = + GetSubFile(submarinePath) ?? + GetSubFile(submarinePath); + BaseSubFile GetSubFile(ContentPath path) where T : BaseSubFile + { + return ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()).FirstOrDefault(f => f.Path == submarinePath); + } + + if (contentFile == null) + { + DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!", + contentPackage: Prefab.ContentPackage); + return; + } + + submarineInfo = new SubmarineInfo(contentFile.Path.Value); + } string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } - ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage); - if (submarinePath.IsNullOrEmpty()) - { - DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!", - contentPackage: Prefab.ContentPackage); - return; - } - - BaseSubFile contentFile = - GetSubFile(submarinePath) ?? - GetSubFile(submarinePath); - BaseSubFile GetSubFile(ContentPath path) where T : BaseSubFile - { - return ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()).FirstOrDefault(f => f.Path == submarinePath); - } - - if (contentFile == null) - { - DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!", - contentPackage: Prefab.ContentPackage); - return; - } - - submarineInfo = new SubmarineInfo(contentFile.Path.Value); } private static float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand) @@ -185,6 +209,33 @@ namespace Barotrauma return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-RandomnessModifier, RandomnessModifier, (float)rand.NextDouble())) / MaxDifficulty), minAmount); } + private SubmarineInfo GetRandomDifficultyModifiedSubmarine(float levelDifficulty, float randomnessModifier) + { + Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); + // look for the saved submarine that is closest to our difficulty, with some randomness + SubmarineInfo bestSubmarine = null; + float bestValue = float.MaxValue; + var submarineInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsEnemySubmarine); + foreach (SubmarineInfo submarineInfo in submarineInfos) + { + if (!Prefab.Tags.Any(t => submarineInfo.EnemySubmarineInfo.MissionTags.Contains(t))) { continue; } + float applicabilityValue = GetDifficultyModifiedValue(submarineInfo.EnemySubmarineInfo.PreferredDifficulty, levelDifficulty, randomnessModifier, rand); + if (applicabilityValue < bestValue) + { + bestSubmarine = submarineInfo; + bestValue = applicabilityValue; + } + } + + if (bestSubmarine == null) + { + DebugConsole.ThrowError("No EnemySubmarine found that matches the mission's tags!"); + return SubmarineInfo.SavedSubmarines.First(i => i.IsEnemySubmarine); + } + + return bestSubmarine; + } + private XElement GetRandomDifficultyModifiedElement(XElement parentElement, float levelDifficulty, float randomnessModifier) { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); @@ -300,30 +351,54 @@ namespace Barotrauma bool commanderAssigned = false; foreach (ContentXElement element in characterConfig.Elements()) { + //there's two ways to define the characters in pirate missions + //1. "the normal way", referring to a human prefab + Identifier humanPrefabId = element.GetAttributeIdentifier("identifier", Identifier.Empty); + //2. the strange way it was initially implemented and the way the vanilla missions work: using a reference to a "character type" in the mission, which refers to a human prefab + Identifier characterTypeId = element.GetAttributeIdentifier("typeidentifier", Identifier.Empty); + + int minAmount = element.GetAttributeInt("minamount", 0); + int maxAmount = element.GetAttributeInt("maxamount", 0); // it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional // if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns - int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty, rand); - var characterId = element.GetAttributeString("typeidentifier", string.Empty); + int amountCreated = minAmount == 0 && maxAmount == 0 ? + //default to 1 character if amount is not defined + 1 : + //otherwise choose a value between min and max based on difficulty + GetDifficultyModifiedAmount(minAmount, maxAmount, enemyCreationDifficulty, rand); + for (int i = 0; i < amountCreated; i++) { - XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId).FirstOrDefault(); - - if (characterType == null) + HumanPrefab humanPrefab = null; + bool isCommander = false; + if (!characterTypeId.IsEmpty) { - DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".", - contentPackage: element.ContentPackage); - return; + XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeIdentifier("typeidentifier", Identifier.Empty) == characterTypeId).FirstOrDefault(); + if (characterType == null) + { + DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".", + contentPackage: element.ContentPackage); + return; + } + XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier); + humanPrefab = GetHumanPrefabFromElement(variantElement); + isCommander = variantElement.GetAttributeBool("iscommander", false); + } + else if (!humanPrefabId.IsEmpty) + { + humanPrefab = GetHumanPrefabFromElement(element); + isCommander = element.GetAttributeBool("iscommander", false); } - XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier); - - var humanPrefab = GetHumanPrefabFromElement(variantElement); if (humanPrefab == null) { continue; } Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, enemySub, CharacterTeamType.None, null); + if (element.GetAttribute("color") != null) + { + spawnedCharacter.UniqueNameColor = element.GetAttributeColor("color", Color.Red); + } if (!commanderAssigned) { - bool isCommander = variantElement.GetAttributeBool("iscommander", false); if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController) { humanAIController.InitShipCommandManager(); @@ -335,6 +410,15 @@ namespace Barotrauma } } + foreach (var subElement in element.Elements()) + { + if (subElement.NameAsIdentifier() == "statuseffect") + { + var newEffect = StatusEffect.Load(subElement, parentDebugName: Prefab.Name.Value); + newEffect?.Apply(newEffect.type, 1.0f, spawnedCharacter, spawnedCharacter); + } + } + foreach (Item item in spawnedCharacter.Inventory.AllItems) { if (item?.GetComponent() != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index f4152f676..4b368c37b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -311,7 +311,11 @@ namespace Barotrauma targets.Add(target); foreach (ContentXElement subElement in chosenElement.Elements()) { - LoadTarget(subElement, parentTarget: target); + if (subElement.NameAsIdentifier() == "target" || + subElement.NameAsIdentifier() == "chooserandom") + { + LoadTarget(subElement, parentTarget: target); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index e2c58bb74..12f3748c9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -72,7 +72,9 @@ namespace Barotrauma /// Maximum number of the specific type of monster in the entire level. Can be used to prevent the event from spawning more monsters if there's /// already enough of that type of monster, e.g. spawned by another event or by a mission. /// - public readonly int MaxAmountPerLevel = int.MaxValue; + public readonly int MaxAmountPerLevel; + + private readonly float? overridePlayDeadProbability; public IReadOnlyList Monsters => monsters; public Vector2? SpawnPos => spawnPos; @@ -137,6 +139,11 @@ namespace Barotrauma scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000); delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f); resetTime = prefab.ConfigElement.GetAttributeFloat("resettime", 0); + float playDeadProbability = prefab.ConfigElement.GetAttributeFloat("playdeadprobability", -1f); + if (playDeadProbability >= 0) + { + overridePlayDeadProbability = playDeadProbability; + } if (GameMain.NetworkMember != null) { @@ -175,6 +182,18 @@ namespace Barotrauma protected override void InitEventSpecific(EventSet parentSet) { + // apply pvp stun resistance (reduce stun amount via resist multiplier) + if (GameMain.NetworkMember is { } networkMember && GameMain.GameSession?.GameMode is PvPMode && !networkMember.ServerSettings.PvPSpawnMonsters) + { + if (GameSettings.CurrentConfig.VerboseLogging) + { + DebugConsole.NewMessage($"PvP setting: disabling monster event ({SpeciesName})", Color.Yellow); + } + + disallowed = true; + return; + } + if (parentSet != null && resetTime == 0) { // Use the parent reset time only if there's no reset time defined for the event. @@ -200,6 +219,10 @@ namespace Barotrauma disallowed = true; continue; } + if (overridePlayDeadProbability.HasValue) + { + createdCharacter.EvaluatePlayDeadProbability(overridePlayDeadProbability); + } if (GameMain.GameSession.IsCurrentLocationRadiated()) { AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness; @@ -299,7 +322,7 @@ namespace Barotrauma { if (sub.Info.Type != SubmarineType.Player && sub.Info.Type != SubmarineType.EnemySubmarine && - sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) + !sub.IsRespawnShuttle) { continue; } @@ -606,7 +629,7 @@ namespace Barotrauma bool anyInAbyss = false; foreach (Submarine submarine in Submarine.Loaded) { - if (submarine.Info.Type != SubmarineType.Player || submarine == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; } + if (submarine.Info.Type != SubmarineType.Player || submarine.IsRespawnShuttle) { continue; } if (submarine.WorldPosition.Y < 0) { anyInAbyss = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 68218dc1b..f9918224d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -37,9 +37,11 @@ namespace Barotrauma public readonly OnRoundEndAction OnRoundEndAction; - private readonly string[] requiredDestinationTypes; + private readonly Identifier[] requiredDestinationTypes; public readonly bool RequireBeaconStation; + public readonly Identifier RequiredDestinationFaction; + public int CurrentActionIndex { get; private set; } public List Actions { get; } = new List(); public Dictionary> Targets { get; } = new Dictionary>(); @@ -78,17 +80,84 @@ namespace Barotrauma contentPackage: prefab.ContentPackage); } - requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null); + requiredDestinationTypes = prefab.ConfigElement.GetAttributeIdentifierArray("requireddestinationtypes", Array.Empty()); RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false); + RequiredDestinationFaction = prefab.ConfigElement.GetAttributeIdentifier(nameof(RequiredDestinationFaction), Identifier.Empty); + + var allActionsWithIndent = GetAllActions(); + var allActions = allActionsWithIndent.Select(a => a.action); + + //attempt to check if the event has ConversationActions with options that don't close the prompt and don't lead to any follow-up conversation + foreach (var action in allActions) + { + if (action is ConversationAction conversationAction && conversationAction.Options.Any()) + { + int thisActionIndex = allActionsWithIndent.FindIndex(a => a.action == action); + int thisIndentationLevel = allActionsWithIndent[thisActionIndex].indent; + bool isLast = true; + + //go through all the actions after this one + foreach (var actionWithIndent in allActionsWithIndent.Skip(thisActionIndex + 1)) + { + //if it's an action with the same indentation level, it means it's a ConversationAction coming after this one + if (actionWithIndent.action is ConversationAction && actionWithIndent.indent == thisIndentationLevel) + { + isLast = false; + break; + } + //if the indentation level went back down, we've already searched everything inside this ConversationAction + if (actionWithIndent.indent < thisIndentationLevel) { break; } + } + if (isLast) + { + foreach (var option in conversationAction.Options) + { + if (!conversationAction.GetEndingOptions().Contains(conversationAction.Options.IndexOf(option)) && + option.Actions.None(a => + a is ConversationAction || HasConversationSubAction(a) || + /* if there's a goto action explicitly set to end the conversation, assume it's intentional*/ + a is GoTo { EndConversation: false })) + { + DebugConsole.AddWarning($"Potential error in event \"{prefab.Identifier}\": {nameof(ConversationAction)} ({conversationAction.Text}) has an option ({option.Text}) that doesn't end the conversation, but could not find any follow-ups to the conversation."); + } + } + } + } + + static bool HasConversationSubAction(EventAction action) + { + foreach (var subAction in action.GetSubActions()) + { + if (subAction is ConversationAction) { return true; } + if (HasConversationSubAction(subAction)) { return true; } + } + return false; + } + } + + foreach (var label in allActions.OfType