From 9470edead3ffebbcc3bef2e3a189793f9577c62b Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Fri, 31 Mar 2023 18:40:44 +0300 Subject: [PATCH] Build 1.1.4.0 --- .../BarotraumaClient/ClientSource/Camera.cs | 12 +- .../ClientSource/CameraTransition.cs | 40 +- .../Characters/AI/Wreck/WreckAI.cs | 16 +- .../Characters/Animation/Ragdoll.cs | 14 +- .../ClientSource/Characters/Character.cs | 44 +- .../ClientSource/Characters/CharacterHUD.cs | 211 +++- .../ClientSource/Characters/CharacterInfo.cs | 30 +- .../Characters/CharacterNetworking.cs | 1 + .../Characters/Health/CharacterHealth.cs | 43 +- .../ClientSource/Characters/Limb.cs | 22 +- .../ClientSource/DebugConsole.cs | 92 +- .../Events/EventActions/ConversationAction.cs | 55 +- .../ClientSource/Events/EventManager.cs | 104 +- .../Missions/AbandonedOutpostMission.cs | 7 +- .../Events/Missions/CargoMission.cs | 3 +- .../Events/Missions/EndMission.cs | 138 +++ .../Events/Missions/GoToMission.cs | 2 +- .../Events/Missions/MineralMission.cs | 4 +- .../ClientSource/Events/Missions/Mission.cs | 76 +- .../Events/Missions/MissionMode.cs | 1 + .../Events/Missions/MissionPrefab.cs | 58 +- .../Events/Missions/MonsterMission.cs | 11 +- .../Events/Missions/SalvageMission.cs | 69 +- .../ClientSource/GUI/ComponentStyle.cs | 8 +- .../ClientSource/GUI/CrewManagement.cs | 120 ++- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 165 ++-- .../ClientSource/GUI/GUICanvas.cs | 42 +- .../ClientSource/GUI/GUIDropDown.cs | 14 +- .../ClientSource/GUI/GUIListBox.cs | 1 + .../ClientSource/GUI/GUIMessageBox.cs | 2 +- .../ClientSource/GUI/GUINumberInput.cs | 4 +- .../ClientSource/GUI/GUIPrefab.cs | 31 +- .../ClientSource/GUI/GUITextBox.cs | 2 +- .../ClientSource/GUI/HUDLayoutSettings.cs | 39 +- .../ClientSource/GUI/LoadingScreen.cs | 91 +- .../ClientSource/GUI/MedicalClinicUI.cs | 125 ++- .../ClientSource/GUI/Store.cs | 92 +- .../ClientSource/GUI/SubmarineSelection.cs | 80 +- .../ClientSource/GUI/TabMenu.cs | 47 +- .../ClientSource/GUI/TalentMenu.cs | 2 +- .../ClientSource/GUI/UpgradeStore.cs | 35 +- .../ClientSource/GUI/VotingInterface.cs | 55 +- .../BarotraumaClient/ClientSource/GameMain.cs | 20 +- .../ClientSource/GameSession/CrewManager.cs | 15 +- .../GameSession/Data/CampaignMetadata.cs | 39 +- .../ClientSource/GameSession/Data/Wallet.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 119 ++- .../GameModes/MultiPlayerCampaign.cs | 188 ++-- .../GameModes/SinglePlayerCampaign.cs | 256 +++-- .../ClientSource/GameSession/HintManager.cs | 2 +- .../ClientSource/GameSession/MedicalClinic.cs | 15 + .../ClientSource/GameSession/ReadyCheck.cs | 2 +- .../ClientSource/GameSession/RoundSummary.cs | 197 ++-- .../ClientSource/Items/CharacterInventory.cs | 158 +-- .../ClientSource/Items/Components/Door.cs | 26 +- .../Items/Components/ElectricalDischarger.cs | 5 + .../Items/Components/Holdable/RangedWeapon.cs | 9 +- .../Items/Components/ItemComponent.cs | 23 +- .../Items/Components/ItemContainer.cs | 104 +- .../Items/Components/LightComponent.cs | 31 +- .../Items/Components/Machines/Fabricator.cs | 14 +- .../Items/Components/Machines/MiniMap.cs | 18 +- .../Items/Components/Machines/Sonar.cs | 106 +- .../Items/Components/Machines/Steering.cs | 8 +- .../Items/Components/Projectile.cs | 1 + .../Items/Components/Repairable.cs | 12 +- .../ClientSource/Items/Components/Rope.cs | 1 + .../Items/Components/Signal/Connection.cs | 2 +- .../Components/Signal/ConnectionPanel.cs | 6 +- .../Components/Signal/CustomInterface.cs | 81 +- .../Items/Components/Signal/Terminal.cs | 57 +- .../Items/Components/StatusHUD.cs | 15 +- .../ClientSource/Items/Components/Turret.cs | 23 +- .../ClientSource/Items/Inventory.cs | 59 +- .../ClientSource/Items/Item.cs | 68 +- .../ClientSource/Items/ItemPrefab.cs | 32 +- .../BarotraumaClient/ClientSource/Map/Gap.cs | 41 + .../BarotraumaClient/ClientSource/Map/Hull.cs | 17 +- .../ClientSource/Map/Levels/Level.cs | 6 +- .../Map/Levels/LevelObjects/LevelObject.cs | 9 +- .../Levels/LevelObjects/LevelObjectManager.cs | 107 +- .../ClientSource/Map/Levels/LevelRenderer.cs | 95 +- .../ClientSource/Map/Lights/ConvexHull.cs | 617 ++++-------- .../ClientSource/Map/Lights/LightManager.cs | 144 ++- .../ClientSource/Map/Lights/LightSource.cs | 603 ++++++----- .../ClientSource/Map/LinkedSubmarine.cs | 4 +- .../ClientSource/Map/Map/Map.cs | 496 +++++++--- .../ClientSource/Map/MapEntity.cs | 36 +- .../ClientSource/Map/Structure.cs | 27 +- .../ClientSource/Map/Submarine.cs | 7 + .../ClientSource/Map/SubmarinePreview.cs | 2 +- .../ClientSource/Map/WayPoint.cs | 37 +- .../ClientSource/Networking/Client.cs | 2 +- .../Networking/FileTransfer/FileReceiver.cs | 2 - .../ClientSource/Networking/GameClient.cs | 96 +- .../Networking/Primitives/Peers/ClientPeer.cs | 4 +- .../Primitives/Peers/SteamP2POwnerPeer.cs | 2 +- .../Networking/ServerList/ServerInfo.cs | 50 +- .../SteamDedicatedServerProvider.cs | 2 +- .../ServerProviders/SteamP2PServerProvider.cs | 4 +- .../ClientSource/Networking/ServerSettings.cs | 78 +- .../Networking/Voip/VoipCapture.cs | 2 + .../Networking/Voip/VoipClient.cs | 6 +- .../ClientSource/Networking/Voting.cs | 79 +- .../ClientSource/Particles/ParticleEmitter.cs | 5 +- .../ClientSource/Particles/ParticleManager.cs | 26 +- .../ClientSource/Particles/ParticlePrefab.cs | 3 + .../ClientSource/Physics/PhysicsBody.cs | 26 +- .../ClientSource/PlayerInput.cs | 111 +-- .../BarotraumaClient/ClientSource/Program.cs | 5 +- .../ClientSource/Screens/CampaignEndScreen.cs | 82 +- .../CampaignSetupUI/CampaignSetupUI.cs | 83 +- .../SinglePlayerCampaignSetupUI.cs | 27 +- .../ClientSource/Screens/CampaignUI.cs | 147 ++- .../CharacterEditor/CharacterEditorScreen.cs | 131 +-- .../ClientSource/Screens/CreditsPlayer.cs | 10 +- .../ClientSource/Screens/GameScreen.cs | 55 +- .../ClientSource/Screens/LevelEditorScreen.cs | 10 +- .../ClientSource/Screens/MainMenuScreen.cs | 163 +-- .../ClientSource/Screens/ModDownloadScreen.cs | 5 +- .../ClientSource/Screens/NetLobbyScreen.cs | 18 +- .../Screens/ParticleEditorScreen.cs | 6 +- .../ServerListScreen/ServerListScreen.cs | 252 ++++- .../ClientSource/Screens/SlideshowPlayer.cs | 173 ++++ .../ClientSource/Screens/SubEditorScreen.cs | 77 +- .../ClientSource/Screens/TestScreen.cs | 2 +- .../Settings/ServerListFilters.cs | 24 +- .../ClientSource/Sounds/SoundPlayer.cs | 55 +- .../ClientSource/Sounds/SoundPrefab.cs | 17 +- .../ClientSource/Sprite/ConditionalSprite.cs | 6 +- .../ClientSource/Sprite/DecorativeSprite.cs | 9 +- .../StatusEffects/StatusEffect.cs | 36 +- .../ClientSource/Steam/BulkDownloader.cs | 2 +- .../ClientSource/Steam/Lobby.cs | 4 +- .../ClientSource/Steam/SteamManager.cs | 6 +- .../ClientSource/Steam/Workshop.cs | 2 +- .../Steam/WorkshopMenu/Mutable/ItemList.cs | 20 +- .../WorkshopMenu/Mutable/ModListPreset.cs | 2 +- .../ClientSource/Utils/WikiImage.cs | 16 +- .../Content/Effects/damageshader.xnb | Bin 1355 -> 2340 bytes .../Content/Effects/damageshader_opengl.xnb | Bin 1359 -> 2274 bytes .../Content/Effects/losshader.xnb | Bin 1470 -> 1962 bytes .../Content/Effects/wearableclip.xnb | Bin 2056 -> 2416 bytes .../Content/Effects/wearableclip_opengl.xnb | Bin 1842 -> 2380 bytes .../BarotraumaClient/LinuxClient.csproj | 4 +- Barotrauma/BarotraumaClient/MacClient.csproj | 4 +- .../BarotraumaClient/Shaders/damageshader.fx | 23 +- .../Shaders/damageshader_opengl.fx | 23 +- .../BarotraumaClient/Shaders/losshader.fx | 13 +- .../BarotraumaClient/Shaders/wearableclip.fx | 8 + .../Shaders/wearableclip_opengl.fx | 8 + .../BarotraumaClient/WindowsClient.csproj | 4 +- .../BarotraumaServer/LinuxServer.csproj | 4 +- Barotrauma/BarotraumaServer/MacServer.csproj | 4 +- .../ServerSource/Characters/CharacterInfo.cs | 7 +- .../Characters/CharacterNetworking.cs | 1 + .../ServerSource/DebugConsole.cs | 83 +- .../Events/EventActions/ConversationAction.cs | 16 +- .../Events/EventActions/MissionAction.cs | 43 + .../ServerSource/Events/EventManager.cs | 12 +- .../Events/Missions/EndMission.cs | 19 + .../Events/Missions/MineralMission.cs | 12 +- .../Events/Missions/SalvageMission.cs | 67 +- .../BarotraumaServer/ServerSource/GameMain.cs | 15 +- .../GameModes/MultiPlayerCampaign.cs | 151 ++- .../ServerSource/GameSession/MedicalClinic.cs | 81 +- .../Items/Components/Projectile.cs | 5 +- .../Components/Signal/ConnectionPanel.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 2 +- .../ServerSource/Items/Inventory.cs | 26 + .../ServerSource/Items/Item.cs | 4 +- .../ServerSource/Networking/BanList.cs | 3 +- .../ServerSource/Networking/ChatMessage.cs | 6 +- .../ServerSource/Networking/GameServer.cs | 229 ++--- .../ServerSource/Networking/KarmaManager.cs | 10 +- .../Peers/Server/LidgrenServerPeer.cs | 6 +- .../Primitives/Peers/Server/ServerPeer.cs | 4 +- .../ServerSource/Networking/RespawnManager.cs | 5 +- .../ServerSource/Networking/ServerSettings.cs | 19 +- .../ServerSource/Networking/Voting.cs | 31 +- .../BarotraumaServer/ServerSource/Program.cs | 5 +- .../ServerSource/Screens/NetLobbyScreen.cs | 26 +- .../ServerSource/Steam/SteamManager.cs | 21 +- .../ServerSource/Utils/DoSProtection.cs | 232 +++++ .../ServerSource/Utils/RateLimiter.cs | 135 +++ .../BarotraumaServer/WindowsServer.csproj | 4 +- .../BarotraumaShared/Data/languageoptions.xml | 22 + .../Data/permissionpresets.xml | 2 +- .../Characters/AI/AIController.cs | 3 +- .../Characters/AI/EnemyAIController.cs | 379 ++++--- .../Characters/AI/HumanAIController.cs | 210 ++-- .../Characters/AI/IndoorsSteeringManager.cs | 24 +- .../SharedSource/Characters/AI/LatchOntoAI.cs | 2 +- .../Characters/AI/MentalStateManager.cs | 1 - .../Characters/AI/NPCConversation.cs | 11 +- .../Characters/AI/Objectives/AIObjective.cs | 4 +- .../AI/Objectives/AIObjectiveCombat.cs | 21 +- .../AI/Objectives/AIObjectiveContainItem.cs | 4 +- .../Objectives/AIObjectiveExtinguishFire.cs | 68 +- .../Objectives/AIObjectiveFightIntruders.cs | 10 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 43 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 24 +- .../AI/Objectives/AIObjectiveGetItem.cs | 25 +- .../AI/Objectives/AIObjectiveGoTo.cs | 81 +- .../AI/Objectives/AIObjectiveIdle.cs | 6 +- .../AI/Objectives/AIObjectiveLoadItem.cs | 2 +- .../AI/Objectives/AIObjectiveOperateItem.cs | 11 +- .../AI/Objectives/AIObjectiveRepairItem.cs | 34 +- .../AI/Objectives/AIObjectiveRescue.cs | 4 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 4 +- .../AI/Objectives/AIObjectiveReturn.cs | 3 + .../SharedSource/Characters/AI/Order.cs | 4 + .../SharedSource/Characters/AI/PathFinder.cs | 1 + .../AI/ShipCommand/ShipIssueWorker.cs | 3 +- .../ShipIssueWorkerOperateWeapons.cs | 15 +- .../Characters/AI/ShipCommandManager.cs | 15 +- .../Characters/AI/SwarmBehavior.cs | 8 +- .../Characters/AI/Wreck/WreckAI.cs | 257 +++-- .../Characters/Animation/AnimController.cs | 4 +- .../Animation/FishAnimController.cs | 135 ++- .../Animation/HumanoidAnimController.cs | 92 +- .../Characters/Animation/Ragdoll.cs | 138 ++- .../SharedSource/Characters/Attack.cs | 25 +- .../SharedSource/Characters/Character.cs | 364 +++---- .../SharedSource/Characters/CharacterInfo.cs | 75 +- .../Characters/CharacterPrefab.cs | 10 +- .../Health/Afflictions/Affliction.cs | 103 +- .../Health/Afflictions/AfflictionBleeding.cs | 3 + .../Health/Afflictions/AfflictionHusk.cs | 11 +- .../Health/Afflictions/AfflictionPrefab.cs | 620 +++++++++--- .../Health/Afflictions/AfflictionPsychosis.cs | 3 + .../Afflictions/AfflictionSpaceHerpes.cs | 7 +- .../Health/Buffs/BuffDurationIncrease.cs | 18 +- .../Characters/Health/CharacterHealth.cs | 110 ++- .../SharedSource/Characters/HumanPrefab.cs | 62 +- .../SharedSource/Characters/Jobs/JobPrefab.cs | 37 +- .../SharedSource/Characters/Jobs/Skill.cs | 2 +- .../SharedSource/Characters/Limb.cs | 49 +- .../Params/Animation/FishAnimations.cs | 3 + .../Characters/Params/CharacterParams.cs | 44 +- .../Params/Ragdoll/RagdollParams.cs | 25 +- .../AbilityConditionCharacter.cs | 10 +- .../AbilityConditionMission.cs | 42 +- .../AbilityConditionHasPermanentStat.cs | 1 - ...erAbilityUnlockApprenticeshipTalentTree.cs | 35 +- .../Characters/Talents/TalentTree.cs | 14 + .../ContentFile/SlideshowsFile.cs | 15 + .../ContentPackageManager.cs | 7 +- .../ContentManagement/ContentXElement.cs | 14 +- .../SharedSource/DebugConsole.cs | 44 +- .../BarotraumaShared/SharedSource/Enums.cs | 495 +++++++++- .../SharedSource/Events/ArtifactEvent.cs | 2 +- .../SharedSource/Events/Event.cs | 2 + .../EventActions/CheckConditionalAction.cs | 12 +- .../Events/EventActions/CheckDataAction.cs | 42 +- .../Events/EventActions/CheckItemAction.cs | 10 +- .../Events/EventActions/ConversationAction.cs | 2 +- .../Events/EventActions/MissionAction.cs | 117 ++- .../Events/EventActions/MissionStateAction.cs | 66 ++ .../EventActions/ModifyLocationAction.cs | 91 ++ .../EventActions/NPCChangeTeamAction.cs | 33 +- .../Events/EventActions/NPCFollowAction.cs | 5 +- .../Events/EventActions/NPCWaitAction.cs | 31 +- .../Events/EventActions/RemoveItemAction.cs | 21 +- .../Events/EventActions/ReputationAction.cs | 50 +- .../Events/EventActions/SpawnAction.cs | 77 +- .../Events/EventActions/StatusEffectAction.cs | 36 +- .../Events/EventActions/TagAction.cs | 12 +- .../Events/EventActions/TriggerEventAction.cs | 26 +- .../Events/EventActions/WaitAction.cs | 3 - .../SharedSource/Events/EventManager.cs | 273 +++-- .../SharedSource/Events/EventPrefab.cs | 19 +- .../SharedSource/Events/EventSet.cs | 55 +- .../Missions/AbandonedOutpostMission.cs | 64 +- .../Events/Missions/AlienRuinMission.cs | 10 +- .../Events/Missions/BeaconMission.cs | 17 +- .../Events/Missions/EndMission.cs | 295 ++++++ .../Events/Missions/EscortMission.cs | 40 +- .../Events/Missions/GoToMission.cs | 6 +- .../Events/Missions/MineralMission.cs | 52 +- .../SharedSource/Events/Missions/Mission.cs | 126 ++- .../Events/Missions/MissionPrefab.cs | 45 +- .../Events/Missions/MonsterMission.cs | 9 +- .../Events/Missions/NestMission.cs | 26 +- .../Events/Missions/PirateMission.cs | 85 +- .../Events/Missions/SalvageMission.cs | 549 ++++++---- .../Events/Missions/ScanMission.cs | 19 +- .../SharedSource/Events/MonsterEvent.cs | 7 +- .../SharedSource/Events/ScriptedEvent.cs | 4 +- .../Extensions/IEnumerableExtensions.cs | 19 +- .../Extensions/StringExtensions.cs | 7 + .../GameSession/AutoItemPlacer.cs | 2 + .../SharedSource/GameSession/CargoManager.cs | 48 +- .../SharedSource/GameSession/CrewManager.cs | 38 +- .../GameSession/Data/CampaignMetadata.cs | 13 +- .../SharedSource/GameSession/Data/Factions.cs | 130 ++- .../GameSession/Data/Reputation.cs | 55 +- .../SharedSource/GameSession/Data/Wallet.cs | 42 +- .../GameSession/GameModes/CampaignMode.cs | 404 ++++++-- .../GameSession/GameModes/CampaignSettings.cs | 10 +- .../GameModes/MultiPlayerCampaign.cs | 26 +- .../SharedSource/GameSession/GameSession.cs | 86 +- .../SharedSource/GameSession/HireManager.cs | 20 +- .../SharedSource/GameSession/MedicalClinic.cs | 40 +- .../GameSession/SlideshowPrefab.cs | 60 ++ .../SharedSource/Items/CharacterInventory.cs | 35 +- .../Items/Components/DockingPort.cs | 8 +- .../SharedSource/Items/Components/Door.cs | 73 +- .../Items/Components/ElectricalDischarger.cs | 2 +- .../Components/EntitySpawnerComponent.cs | 46 +- .../Items/Components/Holdable/Holdable.cs | 13 +- .../Components/Holdable/LevelResource.cs | 2 +- .../Items/Components/Holdable/MeleeWeapon.cs | 2 +- .../Items/Components/Holdable/RangedWeapon.cs | 68 +- .../Items/Components/Holdable/RepairTool.cs | 22 +- .../Items/Components/Holdable/Throwable.cs | 5 + .../Items/Components/ItemComponent.cs | 28 +- .../Items/Components/ItemContainer.cs | 137 +-- .../Items/Components/Machines/Engine.cs | 2 +- .../Items/Components/Machines/Fabricator.cs | 2 +- .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 2 +- .../Items/Components/Machines/Sonar.cs | 2 +- .../Items/Components/Machines/Steering.cs | 4 +- .../Items/Components/Power/PowerContainer.cs | 2 +- .../Items/Components/Projectile.cs | 110 ++- .../Items/Components/Repairable.cs | 57 +- .../Items/Components/Signal/Connection.cs | 34 +- .../Components/Signal/ConnectionPanel.cs | 4 +- .../Components/Signal/CustomInterface.cs | 18 +- .../Items/Components/Signal/LightComponent.cs | 42 +- .../Items/Components/Signal/MotionSensor.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 23 +- .../Items/Components/Signal/Wire.cs | 167 ++-- .../Items/Components/TriggerComponent.cs | 28 +- .../SharedSource/Items/Components/Turret.cs | 417 +++++--- .../SharedSource/Items/Components/Wearable.cs | 11 +- .../SharedSource/Items/Inventory.cs | 13 +- .../SharedSource/Items/Item.cs | 105 +- .../SharedSource/Items/ItemEventData.cs | 7 + .../SharedSource/Items/ItemPrefab.cs | 105 +- .../SharedSource/Items/RelatedItem.cs | 333 ++++--- .../SharedSource/Map/Entity.cs | 4 + .../SharedSource/Map/Explosion.cs | 18 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 118 ++- .../BarotraumaShared/SharedSource/Map/Hull.cs | 2 +- .../SharedSource/Map/IDamageable.cs | 2 +- .../SharedSource/Map/Levels/Biome.cs | 6 + .../Map/Levels/CaveGenerationParams.cs | 8 +- .../SharedSource/Map/Levels/Level.cs | 287 ++++-- .../SharedSource/Map/Levels/LevelData.cs | 89 +- .../Map/Levels/LevelGenerationParams.cs | 139 ++- .../Levels/LevelObjects/LevelObjectManager.cs | 59 +- .../Levels/LevelObjects/LevelObjectPrefab.cs | 6 +- .../Map/Levels/LevelObjects/LevelTrigger.cs | 24 +- .../SharedSource/Map/LinkedSubmarine.cs | 2 +- .../SharedSource/Map/Map/Location.cs | 294 ++++-- .../SharedSource/Map/Map/LocationType.cs | 88 +- .../Map/Map/LocationTypeChange.cs | 104 +- .../SharedSource/Map/Map/Map.cs | 518 +++++++--- .../SharedSource/Map/MapEntity.cs | 72 +- .../Map/Outposts/BeaconStationInfo.cs | 3 + .../SharedSource/Map/Outposts/NPCSet.cs | 14 +- .../Map/Outposts/OutpostGenerationParams.cs | 115 ++- .../Map/Outposts/OutpostGenerator.cs | 159 ++- .../Map/Outposts/OutpostModuleInfo.cs | 2 - .../SharedSource/Map/PriceInfo.cs | 24 +- .../SharedSource/Map/Structure.cs | 53 +- .../SharedSource/Map/StructurePrefab.cs | 38 +- .../SharedSource/Map/Submarine.cs | 229 +++-- .../SharedSource/Map/SubmarineBody.cs | 226 +++-- .../SharedSource/Map/SubmarineInfo.cs | 41 +- .../SharedSource/Map/WayPoint.cs | 52 +- .../SharedSource/Networking/Client.cs | 6 + .../Networking/ClientPermissions.cs | 18 +- .../Networking/INetSerializableStruct.cs | 9 +- .../Networking/OrderChatMessage.cs | 4 +- .../SharedSource/Networking/ServerLog.cs | 3 + .../SharedSource/Networking/ServerSettings.cs | 62 +- .../SharedSource/Networking/Voting.cs | 1 - .../SharedSource/Physics/PhysicsBody.cs | 78 +- .../Prefabs/IImplementsVariants.cs | 24 +- .../SharedSource/Prefabs/PrefabCollection.cs | 111 ++- .../SharedSource/ProcGen/VoronoiElements.cs | 19 +- .../SharedSource/Screens/GameScreen.cs | 10 +- .../Serialization/SerializableProperty.cs | 56 +- .../Serialization/XMLExtensions.cs | 45 +- .../SharedSource/Settings/CreatureMetrics.cs | 133 ++- .../SharedSource/Settings/GameSettings.cs | 11 +- .../Settings/ServerLanguageOptions.cs | 63 ++ .../SharedSource/Sprite/ConditionalSprite.cs | 21 +- .../StatusEffects/DelayedEffect.cs | 12 +- .../StatusEffects/PropertyConditional.cs | 725 ++++++++------ .../StatusEffects/StatusEffect.cs | 935 +++++++++++------- .../SharedSource/Steam/SteamManager.cs | 3 +- .../SharedSource/Steam/Workshop.cs | 10 +- .../SharedSource/SteamAchievementManager.cs | 22 +- .../Text/LocalizedString/ConcatLString.cs | 1 + .../BarotraumaShared/SharedSource/Timing.cs | 3 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 60 +- .../SharedSource/Utils/CrossThread.cs | 2 +- .../SharedSource/Utils/MathUtils.cs | 60 +- .../SharedSource/Utils/Md5Hash.cs | 50 +- .../SharedSource/Utils/Option/None.cs | 17 - .../SharedSource/Utils/Option/Option.cs | 125 ++- .../SharedSource/Utils/Option/Some.cs | 25 - .../SharedSource/Utils/ReflectionUtils.cs | 6 +- .../Utils/SerializableDateTime.cs | 5 +- .../SharedSource/Utils/ToolBox.cs | 3 +- Barotrauma/BarotraumaShared/changelog.txt | 416 ++++++++ .../BarotraumaTest/EndpointParseTests.cs | 15 +- Barotrauma/BarotraumaTest/MathUtilsTests.cs | 67 ++ .../PropertyConditionalTests.cs | 67 ++ .../SerializableDateTimeTests.cs | 25 +- .../Callbacks/CallResult.cs | 6 +- .../Classes/AuthTicket.cs | 4 +- .../Facepunch.Steamworks/Classes/Dispatch.cs | 6 +- .../Facepunch.Steamworks.Posix.csproj | 7 +- .../Facepunch.Steamworks.Win64.csproj | 5 + .../Generated/Interfaces/ISteamApps.cs | 2 +- .../Generated/Interfaces/ISteamInventory.cs | 16 +- .../Networking/Connection.cs | 20 +- .../Networking/ConnectionManager.cs | 4 +- .../Networking/NetIdentity.cs | 5 +- .../Networking/NetPingLocation.cs | 9 +- .../Facepunch.Steamworks/Networking/Socket.cs | 6 +- .../Networking/SocketManager.cs | 12 +- .../Facepunch.Steamworks/ServerList/Base.cs | 20 +- .../ServerList/Favourites.cs | 1 + .../ServerList/Friends.cs | 1 + .../ServerList/History.cs | 1 + .../ServerList/Internet.cs | 2 +- .../ServerList/LocalNetwork.cs | 1 + Libraries/Facepunch.Steamworks/SteamApps.cs | 66 +- Libraries/Facepunch.Steamworks/SteamClient.cs | 8 +- .../Facepunch.Steamworks/SteamFriends.cs | 82 +- Libraries/Facepunch.Steamworks/SteamInput.cs | 16 +- .../Facepunch.Steamworks/SteamInventory.cs | 50 +- .../Facepunch.Steamworks/SteamMatchmaking.cs | 36 +- .../SteamMatchmakingServers.cs | 2 +- Libraries/Facepunch.Steamworks/SteamMusic.cs | 24 +- .../Facepunch.Steamworks/SteamNetworking.cs | 26 +- .../SteamNetworkingSockets.cs | 36 +- .../SteamNetworkingUtils.cs | 28 +- .../Facepunch.Steamworks/SteamParental.cs | 16 +- .../Facepunch.Steamworks/SteamParties.cs | 10 +- .../Facepunch.Steamworks/SteamRemotePlay.cs | 12 +- .../SteamRemoteStorage.cs | 38 +- .../Facepunch.Steamworks/SteamScreenshots.cs | 18 +- Libraries/Facepunch.Steamworks/SteamServer.cs | 70 +- .../Facepunch.Steamworks/SteamServerStats.cs | 18 +- Libraries/Facepunch.Steamworks/SteamUgc.cs | 23 +- Libraries/Facepunch.Steamworks/SteamUser.cs | 88 +- .../Facepunch.Steamworks/SteamUserStats.cs | 38 +- Libraries/Facepunch.Steamworks/SteamUtils.cs | 58 +- Libraries/Facepunch.Steamworks/SteamVideo.cs | 10 +- .../SteamMatchmakingResponses.cs | 23 +- .../Structs/Achievement.cs | 15 +- .../Facepunch.Steamworks/Structs/Clan.cs | 18 +- .../Structs/Controller.cs | 14 +- .../Facepunch.Steamworks/Structs/Friend.cs | 33 +- .../Structs/InventoryDef.cs | 56 +- .../Structs/InventoryItem.cs | 18 +- .../Structs/InventoryRecipe.cs | 2 +- .../Structs/InventoryResult.cs | 16 +- .../Structs/Leaderboard.cs | 32 +- .../Structs/LeaderboardEntry.cs | 2 +- .../Facepunch.Steamworks/Structs/Lobby.cs | 54 +- .../Structs/LobbyQuery.cs | 6 +- .../Structs/PartyBeacon.cs | 19 +- .../Structs/RemotePlaySession.cs | 6 +- .../Structs/Screenshot.cs | 6 +- .../Facepunch.Steamworks/Structs/Server.cs | 28 +- .../Structs/ServerInit.cs | 2 +- .../Facepunch.Steamworks/Structs/Stat.cs | 32 +- .../Facepunch.Steamworks/Structs/UgcEditor.cs | 25 +- .../Facepunch.Steamworks/Structs/UgcItem.cs | 48 +- .../Facepunch.Steamworks/Structs/UgcQuery.cs | 42 +- .../Structs/UgcResultPage.cs | 5 +- .../Utility/SourceServerQuery.cs | 6 +- .../Utility/SteamInterface.cs | 10 +- .../Utility/Utf8String.cs | 8 +- .../Facepunch.Steamworks/Utility/Utility.cs | 4 +- 483 files changed, 17487 insertions(+), 8548 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EndMission.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs create mode 100644 Barotrauma/BarotraumaShared/Data/languageoptions.xml create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SlideshowsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSession/SlideshowPrefab.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs create mode 100644 Barotrauma/BarotraumaTest/MathUtilsTests.cs create mode 100644 Barotrauma/BarotraumaTest/PropertyConditionalTests.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 839d52d50..187b87f7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -36,8 +36,8 @@ namespace Barotrauma private float minZoom = 0.1f; public float MinZoom { - get { return minZoom;} - set { minZoom = MathHelper.Clamp(value, 0.001f, 10.0f); } + get { return minZoom; } + set { minZoom = MathHelper.Clamp(value, 0.001f, 10.0f); } } private float maxZoom = 2.0f; @@ -63,7 +63,7 @@ namespace Barotrauma private float prevZoom; public float Shake; - private Vector2 shakePosition; + public Vector2 ShakePosition { get; private set; } private float shakeTimer; private float globalZoomScale = 1.0f; @@ -371,7 +371,7 @@ namespace Barotrauma if (Shake < 0.01f) { - shakePosition = Vector2.Zero; + ShakePosition = Vector2.Zero; shakeTimer = 0.0f; } else @@ -379,11 +379,11 @@ namespace Barotrauma shakeTimer += deltaTime * 5.0f; Vector2 noisePos = new Vector2((float)PerlinNoise.CalculatePerlin(shakeTimer, shakeTimer, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(shakeTimer, shakeTimer, 0.5f) - 0.5f); - shakePosition = noisePos * Shake * 2.0f; + ShakePosition = noisePos * Shake * 2.0f; Shake = MathHelper.Lerp(Shake, 0.0f, deltaTime * 2.0f); } - Translate(moveCam + shakePosition); + Translate(moveCam + ShakePosition); Freeze = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs index b1392f9cb..7adc4fb99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs @@ -6,6 +6,8 @@ namespace Barotrauma { class CameraTransition { + private static List activeTransitions = new List(); + public bool Running { get; @@ -19,6 +21,7 @@ namespace Barotrauma private readonly float? endZoom; public readonly float WaitDuration; + public float EndWaitDuration = 0.1f; public readonly float PanDuration; public readonly bool FadeOut; public readonly bool LosFadeIn; @@ -29,6 +32,8 @@ namespace Barotrauma public bool AllowInterrupt = false; public bool RemoveControlFromCharacter = true; + + public bool RunWhilePaused = true; public CameraTransition(ISpatialEntity targetEntity, Camera cam, Alignment? cameraStartPos, Alignment? cameraEndPos, bool fadeOut = true, bool losFadeIn = false, float waitDuration = 0f, float panDuration = 10.0f, float? startZoom = null, float? endZoom = null) { @@ -45,8 +50,19 @@ namespace Barotrauma if (targetEntity == null) { return; } Running = true; - CoroutineManager.StopCoroutines("CameraTransition"); + + prevControlled = Character.Controlled; + activeTransitions.RemoveAll(a => !CoroutineManager.IsCoroutineRunning(a.updateCoroutine)); + foreach (var activeTransition in activeTransitions) + { + if (activeTransition.prevControlled != null) + { + prevControlled ??= activeTransition.prevControlled; + } + activeTransition.Stop(); + } updateCoroutine = CoroutineManager.StartCoroutine(Update(targetEntity, cam), "CameraTransition"); + activeTransitions.Add(this); } public void Stop() @@ -62,11 +78,13 @@ namespace Barotrauma #endif } + private float DeltaTime => CoroutineManager.Paused && !RunWhilePaused ? 0 : CoroutineManager.DeltaTime; + private IEnumerable Update(ISpatialEntity targetEntity, Camera cam) { if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; } - prevControlled = Character.Controlled; + prevControlled ??= Character.Controlled; if (RemoveControlFromCharacter) { #if CLIENT @@ -80,6 +98,7 @@ namespace Barotrauma float endZoom = this.endZoom ?? 0.5f; Vector2 initialCameraPos = cam.Position; Vector2? initialTargetPos = targetEntity?.WorldPosition; + Vector2 endPos = cam.Position; float timer = -WaitDuration; @@ -137,13 +156,13 @@ namespace Barotrauma { startPos += targetEntity.WorldPosition - initialTargetPos.Value; } - Vector2 endPos = cameraEndPos.HasValue ? + endPos = cameraEndPos.HasValue ? new Vector2( MathHelper.Lerp(minPos.X, maxPos.X, (cameraEndPos.Value.ToVector2().X + 1.0f) / 2.0f), MathHelper.Lerp(maxPos.Y, minPos.Y, (cameraEndPos.Value.ToVector2().Y + 1.0f) / 2.0f)) : prevControlled?.WorldPosition ?? targetEntity.WorldPosition; - Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, clampedTimer / PanDuration); + Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, clampedTimer / PanDuration) + cam.ShakePosition; cam.Translate(cameraPos - cam.Position); #if CLIENT @@ -162,14 +181,21 @@ namespace Barotrauma Lights.LightManager.ViewTarget = prevControlled ?? (targetEntity as Entity); } #endif - timer += CoroutineManager.DeltaTime; + timer += DeltaTime; yield return CoroutineStatus.Running; } - Running = false; + float endTimer = 0.0f; + while (endTimer <= EndWaitDuration) + { + cam.Translate(endPos - cam.Position); + cam.Zoom = endZoom; + endTimer += DeltaTime; + yield return CoroutineStatus.Running; + } - yield return new WaitForSeconds(0.1f); + Running = false; #if CLIENT GUI.ScreenOverlayColor = Color.TransparentBlack; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs index cf8c7c628..44032309e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs @@ -19,6 +19,17 @@ namespace Barotrauma private IEnumerable FadeOutColors(float time) { + + Dictionary originalColors = new Dictionary(); + foreach (var item in thalamusItems) + { + originalColors.Add(item, item.SpriteColor); + } + foreach (var structure in thalamusStructures) + { + originalColors.Add(structure, structure.SpriteColor); + } + float timer = 0; while (timer < time) { @@ -26,15 +37,16 @@ namespace Barotrauma float m = MathHelper.Lerp(1, Config.DeadEntityColorMultiplier, MathUtils.InverseLerp(0, time, timer)); foreach (var item in thalamusItems) { + if (item.Color.A == 0) { continue; } if (item.Prefab.BrokenSprites.None()) { - Color c = item.Prefab.SpriteColor; + Color c = originalColors[item]; item.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); } } foreach (var structure in thalamusStructures) { - Color c = structure.Prefab.SpriteColor; + Color c = originalColors[structure]; structure.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); } yield return CoroutineStatus.Running; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index a9fa83ac4..6f3eda991 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -99,12 +99,14 @@ namespace Barotrauma float newAngularVelocity = Collider.AngularVelocity; Collider.CorrectPosition(character.MemState, out newPosition, out newVelocity, out newRotation, out newAngularVelocity); - newVelocity = newVelocity.ClampLength(100.0f); - if (!MathUtils.IsValid(newVelocity)) { newVelocity = Vector2.Zero; } - overrideTargetMovement = newVelocity.LengthSquared() > 0.01f ? newVelocity : Vector2.Zero; - - Collider.LinearVelocity = newVelocity; - Collider.AngularVelocity = newAngularVelocity; + if (Collider.BodyType == BodyType.Dynamic) + { + newVelocity = newVelocity.ClampLength(100.0f); + if (!MathUtils.IsValid(newVelocity)) { newVelocity = Vector2.Zero; } + overrideTargetMovement = newVelocity.LengthSquared() > 0.01f ? newVelocity : Vector2.Zero; + Collider.LinearVelocity = newVelocity; + Collider.AngularVelocity = newAngularVelocity; + } float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); float errorTolerance = character.CanMove && !character.IsRagdolled ? 0.01f : 0.2f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index d20050fe4..0f86bcc9c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -109,6 +109,26 @@ namespace Barotrauma set => grainStrength = Math.Max(0, value); } + /// + /// Can be used by status effects + /// + public float CollapseEffectStrength + { + get { return Level.Loaded?.Renderer?.CollapseEffectStrength ?? 0.0f; } + set + { + if (Level.Loaded?.Renderer == null) { return; } + if (Controlled == this) + { + float strength = MathHelper.Clamp(value, 0.0f, 1.0f); + Level.Loaded.Renderer.CollapseEffectStrength = strength; + Level.Loaded.Renderer.CollapseEffectOrigin = Submarine?.WorldPosition ?? WorldPosition; + Screen.Selected.Cam.Shake = Math.Max(MathF.Pow(strength, 3) * 100.0f, Screen.Selected.Cam.Shake); + Screen.Selected.Cam.Rotation = strength * (PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.01f, (float)Timing.TotalTime * 0.05f) - 0.5f); + Level.Loaded.Renderer.ChromaticAberrationStrength = value * 50.0f; + } + } + } /// /// Can be used to set camera shake from status effects /// @@ -278,6 +298,21 @@ namespace Barotrauma { keys[i].SetState(); } + + if (CharacterInventory.IsMouseOnInventory && CharacterHUD.ShouldDrawInventory(this)) + { + ResetInputIfPrimaryMouse(InputType.Use); + ResetInputIfPrimaryMouse(InputType.Shoot); + ResetInputIfPrimaryMouse(InputType.Select); + void ResetInputIfPrimaryMouse(InputType inputType) + { + if (GameSettings.CurrentConfig.KeyMap.Bindings[inputType].MouseButton == MouseButton.PrimaryMouse) + { + keys[(int)inputType].Reset(); + } + } + } + //if we were firing (= pressing the aim and shoot keys at the same time) //and the fire key is the same as Select or Use, reset the key to prevent accidentally selecting/using items if (wasFiring && !keys[(int)InputType.Shoot].Held) @@ -296,8 +331,7 @@ namespace Barotrauma float targetOffsetAmount = 0.0f; if (moveCam) { - if (NeedsAir && !IsProtectedFromPressure() && - (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) + if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) { float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; if (pressure > 0.0f) @@ -911,7 +945,7 @@ namespace Barotrauma { name += " " + TextManager.Get("Disguised"); } - else if (Info.Title != null) + else if (Info.Title != null && TeamID != CharacterTeamType.Team1) { name += '\n' + Info.Title; } @@ -987,13 +1021,13 @@ namespace Barotrauma } } - if (CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) + if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) { hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f)); Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y); GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f), - CharacterHealth.DisplayedVitality / MaxVitality, + CharacterHealth.DisplayedVitality / MaxVitality, Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 80b153e28..83c669f6e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -13,9 +13,8 @@ namespace Barotrauma { const float BossHealthBarDuration = 120.0f; - class BossHealthBar + abstract class BossProgressBar { - public readonly Character Character; public float FadeTimer; public readonly GUIComponent TopContainer; @@ -24,9 +23,18 @@ namespace Barotrauma public readonly GUIProgressBar TopHealthBar; public readonly GUIProgressBar SideHealthBar; - public BossHealthBar(Character character) + public abstract bool Completed { get; } + + public abstract bool Interrupted { get; } + + public abstract float State { get; } + + public abstract string NumberToDisplay { get; } + + public abstract Color Color { get; } + + public BossProgressBar(LocalizedString label) { - Character = character; FadeTimer = BossHealthBarDuration; TopContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.18f, 0.03f), HUDFrame.RectTransform, Anchor.TopCenter) @@ -34,7 +42,7 @@ namespace Barotrauma MinSize = new Point(100, 50), RelativeOffset = new Vector2(0.0f, 0.01f) }, isHorizontal: false, childAnchor: Anchor.TopCenter); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), character.DisplayName, textAlignment: Alignment.Center, textColor: GUIStyle.Red); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), label, textAlignment: Alignment.Center, textColor: GUIStyle.Red); TopHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.6f), TopContainer.RectTransform) { MinSize = new Point(100, HUDLayoutSettings.HealthBarArea.Size.Y) @@ -42,22 +50,95 @@ namespace Barotrauma { Color = GUIStyle.Red }; + CreateNumberText(TopHealthBar); SideContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), bossHealthContainer.RectTransform) { MinSize = new Point(80, 60) }, isHorizontal: false, childAnchor: Anchor.TopRight); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), character.DisplayName, textAlignment: Alignment.CenterRight, textColor: GUIStyle.Red); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), label, textAlignment: Alignment.CenterRight, textColor: GUIStyle.Red); SideHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.7f), SideContainer.RectTransform), barSize: 0.0f, style: "CharacterHealthBar") { Color = GUIStyle.Red }; + CreateNumberText(SideHealthBar); TopContainer.Visible = SideContainer.Visible = false; TopContainer.CanBeFocused = false; TopContainer.Children.ForEach(c => c.CanBeFocused = false); SideContainer.CanBeFocused = false; SideContainer.Children.ForEach(c => c.CanBeFocused = false); + + void CreateNumberText(GUIComponent parent) + { + new GUITextBlock(new RectTransform(Vector2.One, parent.RectTransform) + { AbsoluteOffset = new Point(2) }, + string.Empty, textAlignment: Alignment.Center, textColor: GUIStyle.TextColorDark) + { + TextGetter = () => NumberToDisplay + }; + new GUITextBlock(new RectTransform(Vector2.One, parent.RectTransform), + string.Empty, textAlignment: Alignment.Center, textColor: GUIStyle.TextColorBright) + { + TextGetter = () => NumberToDisplay + }; + } + } + + public abstract bool IsDuplicate(object targetObject); + } + + class BossHealthBar : BossProgressBar + { + public readonly Character Character; + + public override float State => Character.Vitality / Character.MaxVitality; + + public override bool Completed => Character.IsDead; + + public override bool Interrupted => Character.Removed || !Character.Enabled; + + public override Color Color => + Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.PoisonType) > 0 || Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.ParalysisType) > 0 ? + GUIStyle.HealthBarColorPoisoned : GUIStyle.Red; + + public override string NumberToDisplay => string.Empty; + + public BossHealthBar(Character character) : base(character.DisplayName) + { + Character = character; + } + + public override bool IsDuplicate(object targetObject) + { + return targetObject is Character character && Character == character; + } + } + + class MissionProgressBar : BossProgressBar + { + public readonly Mission Mission; + + public override float State => Mission.State / (float)Mission.Prefab.MaxProgressState; + + public override bool Completed => Mission.State >= Mission.Prefab.MaxProgressState; + + public override bool Interrupted => Mission.Failed || GameMain.GameSession?.Missions == null || !GameMain.GameSession.Missions.Contains(Mission); + + public override Color Color => GUIStyle.Red; + + public override string NumberToDisplay => Mission.Prefab.ShowProgressInNumbers ? + $"{Mission.State}/{Mission.Prefab.MaxProgressState}" : + string.Empty; + + public MissionProgressBar(Mission mission) : base(mission.Prefab.ProgressBarLabel) + { + Mission = mission; + } + + public override bool IsDuplicate(object targetObject) + { + return targetObject is Mission mission && Mission == mission; } } @@ -69,9 +150,10 @@ namespace Barotrauma private static readonly List brokenItems = new List(); private static float brokenItemsCheckTimer; - private static readonly List bossHealthBars = new List(); + private static readonly List bossProgressBars = new List(); private static readonly Dictionary cachedHudTexts = new Dictionary(); + private static LanguageIdentifier cachedHudTextLanguage = LanguageIdentifier.None; private static GUILayoutGroup bossHealthContainer; @@ -107,7 +189,7 @@ namespace Barotrauma GameMain.GameSession?.Campaign != null && (GameMain.GameSession.Campaign.ShowCampaignUI || GameMain.GameSession.Campaign.ForceMapUI); - private static bool ShouldDrawInventory(Character character) + public static bool ShouldDrawInventory(Character character) { var controller = character.SelectedItem?.GetComponent(); @@ -121,10 +203,15 @@ namespace Barotrauma public static LocalizedString GetCachedHudText(string textTag, InputType keyBind) { + if (cachedHudTextLanguage != GameSettings.CurrentConfig.Language) + { + cachedHudTexts.Clear(); + } Identifier key = (textTag + keyBind).ToIdentifier(); if (cachedHudTexts.TryGetValue(key, out LocalizedString text)) { return text; } text = TextManager.GetWithVariable(textTag, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).Value; cachedHudTexts.Add(key, text); + cachedHudTextLanguage = GameSettings.CurrentConfig.Language; return text; } @@ -159,7 +246,7 @@ namespace Barotrauma public static void Update(float deltaTime, Character character, Camera cam) { - UpdateBossHealthBars(deltaTime); + UpdateBossProgressBars(deltaTime); if (GUI.DisableHUD) { @@ -623,7 +710,9 @@ namespace Barotrauma GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); textPos.Y += GUIStyle.SubHeadingFont.MeasureString(focusName).Y; - if (character.FocusedCharacter.Info?.Title != null && !character.FocusedCharacter.Info.Title.IsNullOrEmpty()) + if (character.FocusedCharacter.Info?.Title != null && + !character.FocusedCharacter.Info.Title.IsNullOrEmpty() && + character.FocusedCharacter.TeamID != CharacterTeamType.Team1) { GUI.DrawString(spriteBatch, textPos, character.FocusedCharacter.Info.Title, nameColor, Color.Black * 0.7f, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); textPos.Y += GUIStyle.SubHeadingFont.MeasureString(character.FocusedCharacter.Info.Title.Value).Y; @@ -648,7 +737,8 @@ namespace Barotrauma if (!character.DisableHealthWindow && character.IsFriendly(character.FocusedCharacter) && character.FocusedCharacter.CharacterHealth.UseHealthWindow && - character.CanInteractWith(character.FocusedCharacter, 160f, false)) + character.CanInteractWith(character.FocusedCharacter, 160f, false) && + !character.IsClimbing) { GUI.DrawString(spriteBatch, textPos, GetCachedHudText("HealHint", InputType.Health), GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); @@ -661,27 +751,47 @@ namespace Barotrauma } } - public static void ShowBossHealthBar(Character character) + public static void ShowBossHealthBar(Character character, float damage) { if (character == null || character.IsDead || character.Removed) { return; } + if (bossProgressBars.Any(b => b.IsDuplicate(character))) { return; } + AddBossProgressBar(new BossHealthBar(character)); + } + public static void ShowMissionProgressBar(Mission mission) + { + if (mission == null || mission.Completed || mission.Failed) { return; } + if (bossProgressBars.Any(b => b.IsDuplicate(mission))) { return; } + AddBossProgressBar(new MissionProgressBar(mission)); + } + + public static void ClearBossProgressBars() + { + for (int i = bossProgressBars.Count - 1; i>= 0; i--) + { + RemoveBossProgressBar(bossProgressBars[i]); + } + bossProgressBars.Clear(); + } + + private static void RemoveBossProgressBar(BossProgressBar progressBar) + { + progressBar.SideContainer.Parent?.RemoveChild(progressBar.SideContainer); + progressBar.TopContainer.Parent?.RemoveChild(progressBar.TopContainer); + bossProgressBars.Remove(progressBar); + } + + private static void AddBossProgressBar(BossProgressBar progressBar) + { var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; if (healthBarMode == EnemyHealthBarMode.HideAll) { return; } - - var existingBar = bossHealthBars.Find(b => b.Character == character); - if (existingBar != null) + if (bossProgressBars.Count > 5) { - existingBar.FadeTimer = BossHealthBarDuration; - return; - } - - if (bossHealthBars.Count > 5) - { - BossHealthBar oldestHealthBar = bossHealthBars.First(); - foreach (var bar in bossHealthBars) + BossProgressBar oldestHealthBar = bossProgressBars.First(); + foreach (var bar in bossProgressBars) { if (bar.TopHealthBar.BarSize < oldestHealthBar.TopHealthBar.BarSize) { @@ -690,62 +800,69 @@ namespace Barotrauma } oldestHealthBar.FadeTimer = Math.Min(oldestHealthBar.FadeTimer, 1.0f); } - - bossHealthBars.Add(new BossHealthBar(character)); + bossProgressBars.Add(progressBar); } - public static void UpdateBossHealthBars(float deltaTime) + public static void UpdateBossProgressBars(float deltaTime) { var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; - for (int i = 0; i < bossHealthBars.Count; i++) + for (int i = 0; i < bossProgressBars.Count; i++) { - var bossHealthBar = bossHealthBars[i]; + var bossHealthBar = bossProgressBars[i]; - bool showTopBar = i == 0; - if (showTopBar != bossHealthBar.TopContainer.Visible) + bool showTopBar = i == bossProgressBars.Count - 1; + if (showTopBar && !bossHealthBar.TopContainer.Visible) { - bossHealthContainer.Recalculate(); + bossHealthBar.SideContainer.SetAsLastChild(); + SetColor(bossHealthBar, bossHealthBar.SideContainer, 0); } bossHealthBar.TopContainer.Visible = showTopBar; bossHealthBar.SideContainer.Visible = !bossHealthBar.TopContainer.Visible; - float health = bossHealthBar.Character.Vitality / bossHealthBar.Character.MaxVitality; - + bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = bossHealthBar.State; float alpha = Math.Min(bossHealthBar.FadeTimer, 1.0f); - foreach (var c in bossHealthBar.SideContainer.GetAllChildren().Concat(bossHealthBar.TopContainer.GetAllChildren())) + + if (bossHealthBar.TopContainer.Visible) { - c.Color = new Color(c.Color, (byte)(alpha * 255)); - if (c is GUITextBlock textBlock) + SetColor(bossHealthBar, bossHealthBar.TopContainer, alpha); + } + if (bossHealthBar.SideContainer.Visible) + { + SetColor(bossHealthBar, bossHealthBar.SideContainer, alpha); + } + + static void SetColor(BossProgressBar bossHealthBar, GUIComponent container, float alpha) + { + foreach (var component in container.GetAllChildren()) { - textBlock.TextColor = new Color(bossHealthBar.Character.IsDead ? Color.Gray : textBlock.TextColor, (byte)(alpha * 255)); + component.Color = new Color(bossHealthBar.Color, (byte)(alpha * 255)); + if (component is GUITextBlock textBlock) + { + textBlock.TextColor = new Color(bossHealthBar.Completed ? Color.Gray : textBlock.TextColor, (byte)(alpha * 255)); + } } } - bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = health; - Color color = bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("poison") > 0 || bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("paralysis") > 0 ? GUIStyle.HealthBarColorPoisoned : GUIStyle.Red; - bossHealthBar.TopHealthBar.Color = bossHealthBar.SideHealthBar.Color = color; - - if (bossHealthBar.Character.Removed || !bossHealthBar.Character.Enabled) + if (bossHealthBar.Interrupted) { bossHealthBar.FadeTimer = Math.Min(bossHealthBar.FadeTimer, 1.0f); } - else if (bossHealthBar.Character.IsDead) + else if (bossHealthBar.Completed) { bossHealthBar.FadeTimer = Math.Min(bossHealthBar.FadeTimer, 5.0f); } bossHealthBar.FadeTimer -= deltaTime; } - - for (int i = bossHealthBars.Count - 1; i >= 0 ; i--) + for (int i = bossProgressBars.Count - 1; i >= 0 ; i--) { - var bossHealthBar = bossHealthBars[i]; + var bossHealthBar = bossProgressBars[i]; if (bossHealthBar.FadeTimer <= 0 || healthBarMode == EnemyHealthBarMode.HideAll) { bossHealthBar.SideContainer.Parent?.RemoveChild(bossHealthBar.SideContainer); bossHealthBar.TopContainer.Parent?.RemoveChild(bossHealthBar.TopContainer); - bossHealthBars.RemoveAt(i); + bossProgressBars.RemoveAt(i); bossHealthContainer.Recalculate(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index c4bdcaba9..47d87e1ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -540,37 +540,45 @@ namespace Barotrauma string ragdollFile = inc.ReadString(); Identifier npcId = inc.ReadIdentifier(); + + Identifier factionId = inc.ReadIdentifier(); + float minReputationToHire = 0.0f; + if (factionId != default) + { + minReputationToHire = inc.ReadSingle(); + } + uint jobIdentifier = inc.ReadUInt32(); int variant = inc.ReadByte(); - JobPrefab jobPrefab = null; Dictionary skillLevels = new Dictionary(); if (jobIdentifier > 0) - { + { jobPrefab = JobPrefab.Prefabs.Find(jp => jp.UintIdentifier == jobIdentifier); + if (jobPrefab == null) + { + throw new Exception($"Error while reading {nameof(CharacterInfo)} received from the server: could not find a job prefab with the identifier \"{jobIdentifier}\"."); + } foreach (SkillPrefab skillPrefab in jobPrefab.Skills.OrderBy(s => s.Identifier)) { float skillLevel = inc.ReadSingle(); skillLevels.Add(skillPrefab.Identifier, skillLevel); - } - } + } + } - // TODO: animations CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant, npcIdentifier: npcId) { - ID = infoID + ID = infoID, + MinReputationToHire = (factionId, minReputationToHire) }; ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); ch.Head.SkinColor = skinColor; ch.Head.HairColor = hairColor; ch.Head.FacialHairColor = facialHairColor; ch.SetPersonalityTrait(); - if (ch.Job != null) - { - ch.Job.OverrideSkills(skillLevels); - } + ch.Job?.OverrideSkills(skillLevels); - ch.ExperiencePoints = inc.ReadUInt16(); + ch.ExperiencePoints = inc.ReadInt32(); ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints); return ch; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 23db24f69..8139f283c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -593,6 +593,7 @@ namespace Barotrauma { character.MerchantIdentifier = inc.ReadIdentifier(); } + character.Faction = inc.ReadIdentifier(); character.HumanPrefabHealthMultiplier = humanPrefabHealthMultiplier; character.Wallet.Balance = balance; character.Wallet.RewardDistribution = rewardDistribution; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 6fe86291d..107816307 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -87,6 +87,8 @@ namespace Barotrauma /// Container for the icons above the health bar /// private GUIComponent afflictionIconContainer; + private float afflictionIconRefreshTimer; + const float AfflictionIconRefreshInterval = 1.0f; private GUIButton showHiddenAfflictionsButton; @@ -699,10 +701,11 @@ namespace Barotrauma blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); + float afflictionGrainStrength = affliction.GetScreenGrainStrength(); if (afflictionGrainStrength > 0.0f) { - grainStrength = Math.Max(grainStrength, affliction.GetScreenGrainStrength()); + grainStrength = Math.Max(grainStrength, afflictionGrainStrength); Color afflictionGrainColor = affliction.GetActiveEffect()?.GrainColor ?? Color.White; grainColor = Color.Lerp(grainColor, afflictionGrainColor, (float)Math.Pow(1.0f - oxygenLowStrength, 2)); } @@ -861,7 +864,7 @@ namespace Barotrauma { treatmentButton.ToolTip = RichString.Rich( - $"‖color:gui.green‖[{TextManager.Get(PlayerInput.MouseButtonsSwapped() ? "input.rightmouse" : "input.leftmouse")}] " + $"‖color:gui.green‖[{PlayerInput.PrimaryMouseLabel}] " + $"{TextManager.Get("quickuseaction.usetreatment")}‖color:end‖" + '\n' + treatmentButton.ToolTip.NestedStr); } @@ -1018,12 +1021,8 @@ namespace Barotrauma foreach (KeyValuePair kvp in afflictions) { var affliction = kvp.Key; - if (affliction.Prefab.AfflictionOverlay != null) - { - Sprite ScreenAfflictionOverlay = affliction.Prefab.AfflictionOverlay; - ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f, - new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y)); - } + 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)); } float damageOverlayAlpha = DamageOverlayTimer; @@ -1157,15 +1156,20 @@ namespace Barotrauma } } - afflictionIconContainer.RectTransform.SortChildren((r1, r2) => + afflictionIconRefreshTimer -= deltaTime; + if (afflictionIconRefreshTimer <= 0.0f) { - if (r1.GUIComponent.UserData is not AfflictionPrefab prefab1) { return -1; } - if (r2.GUIComponent.UserData is not AfflictionPrefab prefab2) { return 1; } - var index1 = statusIcons.IndexOf(s => s.Prefab == prefab1); - var index2 = statusIcons.IndexOf(s => s.Prefab == prefab2); - return index1.CompareTo(index2); - }); - (afflictionIconContainer as GUILayoutGroup).NeedsToRecalculate = true; + afflictionIconContainer.RectTransform.SortChildren((r1, r2) => + { + if (r1.GUIComponent.UserData is not AfflictionPrefab prefab1) { return -1; } + if (r2.GUIComponent.UserData is not AfflictionPrefab prefab2) { return 1; } + var index1 = statusIcons.IndexOf(s => s.Prefab == prefab1); + var index2 = statusIcons.IndexOf(s => s.Prefab == prefab2); + return index1.CompareTo(index2); + }); + (afflictionIconContainer as GUILayoutGroup).NeedsToRecalculate = true; + afflictionIconRefreshTimer = AfflictionIconRefreshInterval; + } Rectangle hiddenAfflictionHoverArea = showHiddenAfflictionsButton.Rect; foreach (GUIComponent child in hiddenAfflictionIconContainer.Children) @@ -1983,6 +1987,7 @@ namespace Barotrauma { newAfflictions.Clear(); newPeriodicEffects.Clear(); + bool newAdded = false; byte afflictionCount = inc.ReadByte(); for (int i = 0; i < afflictionCount; i++) { @@ -2062,6 +2067,7 @@ namespace Barotrauma { existingAffliction = afflictionPrefab.Instantiate(strength); afflictions.Add(existingAffliction, limb); + newAdded = true; } existingAffliction.SetStrength(strength); if (existingAffliction == stunAffliction) @@ -2088,6 +2094,11 @@ namespace Barotrauma CalculateVitality(); DisplayedVitality = Vitality; + + if (newAdded) + { + MedicalClinic.OnAfflictionCountChanged(Character); + } } partial void UpdateSkinTint() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 5fe12e73e..1acc8736a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -554,7 +554,7 @@ namespace Barotrauma float damage = 0; foreach (var affliction in result.Afflictions) { - if (affliction.Prefab.DamageParticles && affliction.Prefab.AfflictionType == "damage") + if (affliction.Prefab.DamageParticles && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType) { damage += affliction.GetVitalityDecrease(null); } @@ -563,11 +563,11 @@ namespace Barotrauma float bleedingDamageMultiplier = 1; foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { - if (damageModifier.MatchesAfflictionType("damage")) + if (damageModifier.MatchesAfflictionType(AfflictionPrefab.DamageType)) { damageMultiplier *= damageModifier.DamageMultiplier; } - else if (damageModifier.MatchesAfflictionType("bleeding")) + else if (damageModifier.MatchesAfflictionType(AfflictionPrefab.BleedingType)) { bleedingDamageMultiplier *= damageModifier.DamageMultiplier; } @@ -599,7 +599,7 @@ namespace Barotrauma { if (damageModifier.DamageMultiplier > 0 && !string.IsNullOrWhiteSpace(damageModifier.DamageParticle)) { - overrideParticle = GameMain.ParticleManager?.FindPrefab(damageModifier.DamageParticle); + overrideParticle = ParticleManager.FindPrefab(damageModifier.DamageParticle); break; } } @@ -646,7 +646,7 @@ namespace Barotrauma dripParticleTimer += wetTimer * deltaTime * Mass * (wetTimer > 0.9f ? 50.0f : 5.0f); if (dripParticleTimer > 1.0f) { - float dropRadius = body.BodyShape == PhysicsBody.Shape.Rectangle ? Math.Min(body.width, body.height) : body.radius; + float dropRadius = body.BodyShape == PhysicsBody.Shape.Rectangle ? Math.Min(body.Width, body.Height) : body.Radius; GameMain.ParticleManager.CreateParticle( "waterdrop", WorldPosition + Rand.Vector(Rand.Range(0.0f, ConvertUnits.ToDisplayUnits(dropRadius))), @@ -683,10 +683,10 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null, bool disableDeformations = false) { - float brightness = Math.Max(1.0f - burnOverLayStrength, 0.2f); var spriteParams = Params.GetSprite(); if (spriteParams == null) { return; } - + float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength; + float brightness = Math.Max(1.0f - burn, 0.2f); Color clr = spriteParams.Color; if (!spriteParams.IgnoreTint) { @@ -727,7 +727,7 @@ namespace Barotrauma } } - float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); bool hideLimb = Hide || OtherWearables.Any(w => w.HideLimb) || @@ -1245,6 +1245,12 @@ namespace Barotrauma }; paramsToPass.Params["wearableUvToClipperUv"] = wearableUvToClipperUv; + paramsToPass.Params["stencilUVmin"] = new Vector2( + (float)alphaClipper.Sprite.SourceRect.X / alphaClipper.Sprite.Texture.Width, + (float)alphaClipper.Sprite.SourceRect.Y / alphaClipper.Sprite.Texture.Height); + paramsToPass.Params["stencilUVmax"] = new Vector2( + (float)alphaClipper.Sprite.SourceRect.Right / alphaClipper.Sprite.Texture.Width, + (float)alphaClipper.Sprite.SourceRect.Bottom / alphaClipper.Sprite.Texture.Height); paramsToPass.Params["clipperTexelSize"] = 2f / alphaClipper.Sprite.Texture.Width; paramsToPass.Params["aCutoff"] = 2f / 255f; paramsToPass.Params["xTexture"] = wearable.Sprite.Texture; diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 19d98e5e7..1fe714a9b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -425,6 +425,10 @@ namespace Barotrauma { CheatsEnabled = true; SteamAchievementManager.CheatsEnabled = true; + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + campaign.CheatsEnabled = true; + } NewMessage("Enabled cheat commands.", Color.Red); #if USE_STEAM NewMessage("Steam achievements have been disabled during this play session.", Color.Red); @@ -639,7 +643,7 @@ namespace Barotrauma { if (Submarine.MainSub == null) { return; } MapEntity.SelectedList.Clear(); - MapEntity.mapEntityList.ForEach(me => me.IsHighlighted = false); + MapEntity.ClearHighlightedEntities(); WikiImage.Create(Submarine.MainSub); })); @@ -752,7 +756,7 @@ namespace Barotrauma state = !GameMain.LightManager.LosEnabled; } GameMain.LightManager.LosEnabled = state; - NewMessage("Line of sight effect " + (GameMain.LightManager.LosEnabled ? "enabled" : "disabled"), Color.White); + NewMessage("Line of sight effect " + (GameMain.LightManager.LosEnabled ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("los", false); @@ -763,7 +767,7 @@ namespace Barotrauma state = !GameMain.LightManager.LightingEnabled; } GameMain.LightManager.LightingEnabled = state; - NewMessage("Lighting " + (GameMain.LightManager.LightingEnabled ? "enabled" : "disabled"), Color.White); + NewMessage("Lighting " + (GameMain.LightManager.LightingEnabled ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("lighting|lights", false); @@ -781,7 +785,7 @@ namespace Barotrauma hull.OriginalAmbientLight = null; } } - NewMessage("Restored all hull ambient lights", Color.White); + NewMessage("Restored all hull ambient lights", Color.Yellow); return; } @@ -803,11 +807,11 @@ namespace Barotrauma if (add) { - NewMessage($"Set ambient light color to {color}.", Color.White); + NewMessage($"Set ambient light color to {color}.", Color.Yellow); } else { - NewMessage($"Increased ambient light by {color}.", Color.White); + NewMessage($"Increased ambient light by {color}.", Color.Yellow); } }); AssignRelayToServer("ambientlight", false); @@ -1124,7 +1128,18 @@ namespace Barotrauma state = !GameMain.DebugDraw; } GameMain.DebugDraw = state; - NewMessage("Debug draw mode " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("Debug draw mode " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.Yellow); + }); + AssignRelayToServer("debugdraw", false); + + AssignOnExecute("debugdrawlos", (string[] args) => + { + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !GameMain.LightManager.DebugLos; + } + GameMain.LightManager.DebugLos = state; + NewMessage("Los debug draw mode " + (GameMain.LightManager.DebugLos ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("debugdraw", false); @@ -1146,7 +1161,7 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosAlpha = 1f; } - NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.White); + NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("devmode", false); @@ -1157,7 +1172,7 @@ namespace Barotrauma state = !TextManager.DebugDraw; } TextManager.DebugDraw = state; - NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("debugdraw", false); @@ -1170,19 +1185,19 @@ namespace Barotrauma var config = GameSettings.CurrentConfig; config.Audio.DisableVoiceChatFilters = state; GameSettings.SetCurrentConfig(config); - NewMessage("Voice chat filters " + (GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.White); + NewMessage("Voice chat filters " + (GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.Yellow); }); AssignRelayToServer("togglevoicechatfilters", false); commands.Add(new Command("fpscounter", "fpscounter: Toggle the FPS counter.", (string[] args) => { GameMain.ShowFPS = !GameMain.ShowFPS; - NewMessage("FPS counter " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("FPS counter " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.Yellow); })); commands.Add(new Command("showperf", "showperf: Toggle performance statistics on/off.", (string[] args) => { GameMain.ShowPerf = !GameMain.ShowPerf; - NewMessage("Performance statistics " + (GameMain.ShowPerf ? "enabled" : "disabled"), Color.White); + NewMessage("Performance statistics " + (GameMain.ShowPerf ? "enabled" : "disabled"), Color.Yellow); })); AssignOnClientExecute("netstats", (string[] args) => @@ -1194,55 +1209,55 @@ namespace Barotrauma commands.Add(new Command("hudlayoutdebugdraw|debugdrawhudlayout", "hudlayoutdebugdraw: Toggle the debug drawing mode of HUD layout areas on/off.", (string[] args) => { HUDLayoutSettings.DebugDraw = !HUDLayoutSettings.DebugDraw; - NewMessage("HUD layout debug draw mode " + (HUDLayoutSettings.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("HUD layout debug draw mode " + (HUDLayoutSettings.DebugDraw ? "enabled" : "disabled"), Color.Yellow); })); commands.Add(new Command("interactdebugdraw|debugdrawinteract", "interactdebugdraw: Toggle the debug drawing mode of item interaction ranges on/off.", (string[] args) => { Character.DebugDrawInteract = !Character.DebugDrawInteract; - NewMessage("Interact debug draw mode " + (Character.DebugDrawInteract ? "enabled" : "disabled"), Color.White); + NewMessage("Interact debug draw mode " + (Character.DebugDrawInteract ? "enabled" : "disabled"), Color.Yellow); }, isCheat: true)); AssignOnExecute("togglehud|hud", (string[] args) => { GUI.DisableHUD = !GUI.DisableHUD; GameMain.Instance.IsMouseVisible = !GameMain.Instance.IsMouseVisible; - NewMessage(GUI.DisableHUD ? "Disabled HUD" : "Enabled HUD", Color.White); + NewMessage(GUI.DisableHUD ? "Disabled HUD" : "Enabled HUD", Color.Yellow); }); AssignRelayToServer("togglehud|hud", false); AssignOnExecute("toggleupperhud", (string[] args) => { GUI.DisableUpperHUD = !GUI.DisableUpperHUD; - NewMessage(GUI.DisableUpperHUD ? "Disabled upper HUD" : "Enabled upper HUD", Color.White); + NewMessage(GUI.DisableUpperHUD ? "Disabled upper HUD" : "Enabled upper HUD", Color.Yellow); }); AssignRelayToServer("toggleupperhud", false); AssignOnExecute("toggleitemhighlights", (string[] args) => { GUI.DisableItemHighlights = !GUI.DisableItemHighlights; - NewMessage(GUI.DisableItemHighlights ? "Disabled item highlights" : "Enabled item highlights", Color.White); + NewMessage(GUI.DisableItemHighlights ? "Disabled item highlights" : "Enabled item highlights", Color.Yellow); }); AssignRelayToServer("toggleitemhighlights", false); AssignOnExecute("togglecharacternames", (string[] args) => { GUI.DisableCharacterNames = !GUI.DisableCharacterNames; - NewMessage(GUI.DisableCharacterNames ? "Disabled character names" : "Enabled character names", Color.White); + NewMessage(GUI.DisableCharacterNames ? "Disabled character names" : "Enabled character names", Color.Yellow); }); AssignRelayToServer("togglecharacternames", false); AssignOnExecute("followsub", (string[] args) => { Camera.FollowSub = !Camera.FollowSub; - NewMessage(Camera.FollowSub ? "Set the camera to follow the closest submarine" : "Disabled submarine following.", Color.White); + NewMessage(Camera.FollowSub ? "Set the camera to follow the closest submarine" : "Disabled submarine following.", Color.Yellow); }); AssignRelayToServer("followsub", false); AssignOnExecute("toggleaitargets|aitargets", (string[] args) => { AITarget.ShowAITargets = !AITarget.ShowAITargets; - NewMessage(AITarget.ShowAITargets ? "Enabled AI target drawing" : "Disabled AI target drawing", Color.White); + NewMessage(AITarget.ShowAITargets ? "Enabled AI target drawing" : "Disabled AI target drawing", Color.Yellow); }); AssignRelayToServer("toggleaitargets|aitargets", false); @@ -1264,10 +1279,36 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosAlpha = 1f; } - NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.White); + NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.Yellow); }); AssignRelayToServer("debugai", false); + AssignOnExecute("showmonsters", (string[] args) => + { + CreatureMetrics.UnlockAll = true; + CreatureMetrics.Save(); + NewMessage("All monsters are now visible in the character editor.", Color.Yellow); + if (Screen.Selected == GameMain.CharacterEditorScreen) + { + GameMain.CharacterEditorScreen.Deselect(); + GameMain.CharacterEditorScreen.Select(); + } + }); + AssignRelayToServer("showmonsters", false); + + AssignOnExecute("hidemonsters", (string[] args) => + { + CreatureMetrics.UnlockAll = false; + CreatureMetrics.Save(); + NewMessage("All monsters that haven't yet been encountered in the game are now hidden in the character editor.", Color.Yellow); + if (Screen.Selected == GameMain.CharacterEditorScreen) + { + GameMain.CharacterEditorScreen.Deselect(); + GameMain.CharacterEditorScreen.Select(); + } + }); + AssignRelayToServer("hidemonsters", false); + AssignRelayToServer("water|editwater", false); AssignRelayToServer("fire|editfire", false); @@ -2833,7 +2874,7 @@ namespace Barotrauma NewMessage("Valid ranks are:", Color.White); foreach (PermissionPreset permissionPreset in PermissionPreset.List) { - NewMessage(" - " + permissionPreset.Name, Color.White); + NewMessage(" - " + permissionPreset.DisplayName, Color.White); } ShowQuestionPrompt("Rank to grant to client " + args[0] + "?", (rank) => { @@ -2991,7 +3032,7 @@ namespace Barotrauma ThrowError($"Could not find the location type \"{args[0]}\"."); return; } - GameMain.GameSession.Campaign.Map.CurrentLocation.ChangeType(locationType); + GameMain.GameSession.Campaign.Map.CurrentLocation.ChangeType(GameMain.GameSession.Campaign, locationType); }, () => { @@ -3319,6 +3360,11 @@ namespace Barotrauma else { NewMessage("Level seed: " + Level.Loaded.Seed); + NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier); + NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier())); + NewMessage("Mirrored: " + Level.Loaded.Mirrored); + NewMessage("Level size: " + Level.Loaded.Size.X + "x" + Level.Loaded.Size.Y); + NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown")); } }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 18e93bfbd..0d7bb2e49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -83,6 +83,7 @@ namespace Barotrauma GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; Debug.Assert(conversationList != null); + DisableButtons(conversationList.Content.GetAllChildren(), selectedButton: null); // gray out the last text block if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) { @@ -269,14 +270,7 @@ namespace Barotrauma if (actionInstance != null) { actionInstance.selectedOption = selectedOption; - foreach (GUIButton otherButton in optionButtons) - { - otherButton.CanBeFocused = false; - if (otherButton != btn) - { - otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); - } - } + DisableButtons(optionButtons, btn); btn.ExternalHighlight = true; return true; } @@ -286,14 +280,7 @@ namespace Barotrauma SendResponse(actionId.Value, selectedOption); btn.CanBeFocused = false; btn.ExternalHighlight = true; - foreach (GUIButton otherButton in optionButtons) - { - otherButton.CanBeFocused = false; - if (otherButton != btn) - { - otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); - } - } + DisableButtons(optionButtons, btn); return true; } //should not happen @@ -305,6 +292,18 @@ namespace Barotrauma } } + public static void SelectOption(ushort actionId, int option) + { + if (lastMessageBox.UserData is Pair userData) + { + if (userData.Second != actionId) { return; } + + GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; + Debug.Assert(conversationList != null); + DisableButtons(conversationList.Content.GetAllChildren(), (btn) => btn.UserData is int i && i == option); + } + } + private static Tuple GetSizes(DialogTypes dialogTypes) { return dialogTypes switch @@ -383,6 +382,30 @@ namespace Barotrauma return buttons; } + private static void DisableButtons(IEnumerable buttons, GUIButton selectedButton) + { + DisableButtons(buttons, (btn) => btn == selectedButton); + } + + private static void DisableButtons(IEnumerable buttons, Func isSelectedButton) + { + foreach (GUIButton btn in buttons) + { + if (btn.CanBeFocused) + { + btn.CanBeFocused = false; + if (isSelectedButton(btn)) + { + btn.Selected = true; + } + else + { + btn.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); + } + } + } + } + private static void SendResponse(UInt16 actionId, int selectedOption) { IWriteMessage outmsg = new WriteOnlyMessage(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 9093450ec..ef02adad7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -608,58 +608,90 @@ namespace Barotrauma } break; case NetworkEventType.CONVERSATION: - UInt16 identifier = msg.ReadUInt16(); - string eventSprite = msg.ReadString(); - byte dialogType = msg.ReadByte(); - bool continueConversation = msg.ReadBoolean(); - UInt16 speakerId = msg.ReadUInt16(); - string text = msg.ReadString(); - bool fadeToBlack = msg.ReadBoolean(); - byte optionCount = msg.ReadByte(); - List options = new List(); - for (int i = 0; i < optionCount; i++) { - options.Add(msg.ReadString()); - } - - byte endCount = msg.ReadByte(); - int[] endings = new int[endCount]; - for (int i = 0; i < endCount; i++) - { - endings[i] = msg.ReadByte(); - } - - if (string.IsNullOrEmpty(text) && optionCount == 0) - { - GUIMessageBox.MessageBoxes.ForEachMod(mb => + UInt16 identifier = msg.ReadUInt16(); + string eventSprite = msg.ReadString(); + byte dialogType = msg.ReadByte(); + bool continueConversation = msg.ReadBoolean(); + UInt16 speakerId = msg.ReadUInt16(); + string text = msg.ReadString(); + bool fadeToBlack = msg.ReadBoolean(); + byte optionCount = msg.ReadByte(); + List options = new List(); + for (int i = 0; i < optionCount; i++) { - if (mb.UserData is Pair pair && pair.First == "ConversationAction" && pair.Second == identifier) + options.Add(msg.ReadString()); + } + + byte endCount = msg.ReadByte(); + int[] endings = new int[endCount]; + for (int i = 0; i < endCount; i++) + { + endings[i] = msg.ReadByte(); + } + + if (string.IsNullOrEmpty(text) && optionCount == 0) + { + GUIMessageBox.MessageBoxes.ForEachMod(mb => { - (mb as GUIMessageBox)?.Close(); - } - }); + if (mb.UserData is Pair pair && pair.First == "ConversationAction" && pair.Second == identifier) + { + (mb as GUIMessageBox)?.Close(); + } + }); + } + else + { + ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation); + } + if (Entity.FindEntityByID(speakerId) is Character speaker) + { + speaker.CampaignInteractionType = CampaignMode.InteractionType.None; + speaker.SetCustomInteract(null, null); + } + break; } - else + case NetworkEventType.CONVERSATION_SELECTED_OPTION: { - ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation); + UInt16 identifier = msg.ReadUInt16(); + int selectedOption = msg.ReadByte() - 1; + ConversationAction.SelectOption(identifier, selectedOption); + break; } - if (Entity.FindEntityByID(speakerId) is Character speaker) - { - speaker.CampaignInteractionType = CampaignMode.InteractionType.None; - speaker.SetCustomInteract(null, null); - } - break; case NetworkEventType.MISSION: Identifier missionIdentifier = msg.ReadIdentifier(); + int locationIndex = msg.ReadInt32(); + int destinationIndex = msg.ReadInt32(); + string missionName = msg.ReadString(); MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier); if (prefab != null) { - new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), + new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) { IconColor = prefab.IconColor }; + if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count) + { + Location location = map.Locations[locationIndex]; + map.Discover(location, checkTalents: false); + + LocationConnection? connection = null; + if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count) + { + Location destination = map.Locations[destinationIndex]; + connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination)); + } + if (connection != null) + { + location.UnlockMission(prefab, connection); + } + else + { + location.UnlockMission(prefab); + } + } } break; case NetworkEventType.UNLOCKPATH: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs index fcd12b072..fa15748e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -8,7 +8,7 @@ namespace Barotrauma public override int State { get { return base.State; } - protected set + set { if (state != value) { @@ -45,7 +45,10 @@ namespace Barotrauma { requireRescue.Add(character); #if CLIENT - GameMain.GameSession.CrewManager.AddCharacterToCrewList(character); + if (allowOrderingRescuees) + { + GameMain.GameSession.CrewManager.AddCharacterToCrewList(character); + } #endif } ushort itemCount = msg.ReadUInt16(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 08356c60c..1a9941f42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -10,8 +10,7 @@ namespace Barotrauma public override RichString GetMissionRewardText(Submarine sub) { - LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); - + LocalizedString rewardText = GetRewardAmountText(sub); LocalizedString retVal; if (rewardPerCrate.HasValue) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs new file mode 100644 index 000000000..5becd9ba2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs @@ -0,0 +1,138 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + partial class EndMission : Mission + { + public override bool DisplayAsCompleted => false; + + public override bool DisplayAsFailed => false; + + partial void OnStateChangedProjSpecific() + { + SoundPlayer.ForceMusicUpdate(); + if (Phase == MissionPhase.NoItemsDestroyed) + { + CoroutineManager.Invoke(() => + { + if (boss != null && !boss.Removed) + { + new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 8, fadeOut: false, startZoom: 1.0f, endZoom: 0.3f * GUI.yScale) + { + RunWhilePaused = false, + EndWaitDuration = 3.0f + }; + } + }, delay: 3.0f); + } + else if (Phase == MissionPhase.AllItemsDestroyed) + { + CoroutineManager.StartCoroutine(wakeUpCoroutine(), name: "EndMission.wakeUpCoroutine"); + } + else if (Phase == MissionPhase.BossKilled) + { + if (!string.IsNullOrEmpty(endCinematicSound)) + { + SoundPlayer.PlaySound(endCinematicSound); + } + CoroutineManager.Invoke(() => + { + new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 3, fadeOut: false, endZoom: 0.1f * GUI.yScale) + { + RunWhilePaused = false, + EndWaitDuration = float.PositiveInfinity + }; + }, delay: 3.0f); + } + + IEnumerable wakeUpCoroutine() + { + yield return new WaitForSeconds(wakeUpCinematicDelay); + if (boss != null && !boss.Removed) + { + new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 5.0f, fadeOut: false, losFadeIn: false, startZoom: 1.0f, endZoom: 0.4f * GUI.yScale) + { + RunWhilePaused = false, + EndWaitDuration = cameraWaitDuration + }; + } + yield return new WaitForSeconds(bossWakeUpDelay); + if (boss != null && !boss.Removed) + { + foreach (var limb in boss.AnimController.Limbs) + { + if (!limb.FreezeBlinkState) { continue; } + limb.FreezeBlinkState = false; + if (limb.LightSource is Lights.LightSource light) + { + light.Enabled = true; + } + } + } + } + } + + partial void UpdateProjSpecific() + { + if (boss == null || boss.Removed) { return; } + if (Phase is MissionPhase.Initial or MissionPhase.NoItemsDestroyed or MissionPhase.SomeItemsDestroyed) + { + // Put asleep. + // Have to set the light every frame (or at least periodically), because light.Enabled is changed when Character.IsVisible changes (off/on screen). See GameScreen.Draw(). + foreach (var limb in boss.AnimController.Limbs) + { + if (limb.Params.BlinkFrequency > 0) + { + limb.FreezeBlinkState = true; + limb.BlinkPhase = -limb.Params.BlinkHoldTime; + if (limb.LightSource is Lights.LightSource light) + { + light.Enabled = false; + } + } + } + } + +#if DEBUG + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.O)) + { + State = 0; + } + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Y)) + { + destructibleItems.ForEach(it => it.Condition = 0.0f); + } + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.U)) + { + boss?.SetAllDamage(20000.0f, 0.0f, 0.0f); + } +#endif + } + + public override void ClientReadInitial(IReadMessage msg) + { + base.ClientReadInitial(msg); + + boss = Character.ReadSpawnData(msg); + + byte minionCount = msg.ReadByte(); + List minionList = new List(); + for (int i = 0; i < minionCount; i++) + { + var minion = Character.ReadSpawnData(msg); + if (minion == null) + { + throw new System.Exception($"Error in EndMission.ClientReadInitial: failed to create a minion (mission: {Prefab.Identifier}, index: {i})"); + } + minionList.Add(minion); + } + minions = minionList.ToImmutableArray(); + if (minions.Length != minionCount) + { + throw new System.Exception("Error in EndMission.ClientReadInitial: minion count does not match the server count (" + minionCount + " != " + minions.Length + "mission: " + Prefab.Identifier + ")"); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs index 6036c0586..a7a45837c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs @@ -2,7 +2,7 @@ { partial class GoToMission : Mission { - public override bool DisplayAsCompleted => false; + public override bool DisplayAsCompleted => State >= Prefab.MaxProgressState; public override bool DisplayAsFailed => false; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs index bb573b27d..41a7758b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -29,7 +29,7 @@ namespace Barotrauma } } - for (int i = 0; i < resourceClusters.Count; i++) + for (int i = 0; i < resourceAmounts.Count; i++) { var amount = msg.ReadByte(); var rotation = msg.ReadSingle(); @@ -54,7 +54,7 @@ namespace Barotrauma CalculateMissionClusterPositions(); - for(int i = 0; i < resourceClusters.Count; i++) + for(int i = 0; i < resourceAmounts.Count; i++) { var identifier = msg.ReadIdentifier(); var count = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 28eafbc6c..4a0e51172 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -32,44 +32,73 @@ namespace Barotrauma return ToolBox.GradientLerp(t, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } - public virtual RichString GetMissionRewardText(Submarine sub) + /// + /// Returns the amount of marks you get from the reward (e.g. "3,000 mk") + /// + protected LocalizedString GetRewardAmountText(Submarine sub) { - LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); - return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖"+rewardText+"‖end‖")); + int baseReward = GetReward(sub); + int finalReward = GetFinalReward(sub); + string rewardAmountText = string.Format(CultureInfo.InvariantCulture, "{0:N0}", baseReward); + if (finalReward > baseReward) + { + rewardAmountText += $" + {string.Format(CultureInfo.InvariantCulture, "{0:N0}", finalReward - baseReward)}"; + } + return TextManager.GetWithVariable("currencyformat", "[credits]", rewardAmountText); } - public RichString GetReputationRewardText(Location currLocation) + /// + /// Returns the full reward text of the mission (e.g. "Reward: 2,000 mk" or "Reward: 500 mk x 2 (out of max 5) = 1,000 mk") + /// + public virtual RichString GetMissionRewardText(Submarine sub) + { + LocalizedString rewardText = GetRewardAmountText(sub); + return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖" + rewardText + "‖end‖")); + } + + public RichString GetReputationRewardText() { List reputationRewardTexts = new List(); foreach (var reputationReward in ReputationRewards) { - LocalizedString name = ""; - - if (reputationReward.Key == "location") + FactionPrefab targetFactionPrefab; + if (reputationReward.Key == "location" ) { - name = $"‖color:gui.orange‖{currLocation.Name}‖end‖"; + targetFactionPrefab = OriginLocation.Faction?.Prefab; } else { - var faction = FactionPrefab.Prefabs.Find(f => f.Identifier == reputationReward.Key); - if (faction != null) - { - name = $"‖color:{XMLExtensions.ColorToString(faction.IconColor)}‖{faction.Name}‖end‖"; - } - else - { - name = TextManager.Get(reputationReward.Key); - } + FactionPrefab.Prefabs.TryGet(reputationReward.Key, out targetFactionPrefab); + } + + if (targetFactionPrefab == null) + { + return string.Empty; } - float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, reputationReward.Value); - string formattedValue = ((int)reputationReward.Value).ToString("+#;-#;0"); //force plus sign for positive numbers + + float totalReputationChange = reputationReward.Value; + if (GameMain.GameSession?.Campaign?.Factions.Find(f => f.Prefab == targetFactionPrefab) is Faction faction) + { + totalReputationChange = reputationReward.Value * faction.Reputation.GetReputationChangeMultiplier(reputationReward.Value); + } + + LocalizedString name = $"‖color:{XMLExtensions.ToStringHex(targetFactionPrefab.IconColor)}‖{targetFactionPrefab.Name}‖end‖"; + float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, totalReputationChange); + string formattedValue = ((int)Math.Round(totalReputationChange)).ToString("+#;-#;0"); //force plus sign for positive numbers LocalizedString rewardText = TextManager.GetWithVariables( "reputationformat", ("[reputationname]", name), - ("[reputationvalue]", $"‖color:{XMLExtensions.ColorToString(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" )); + ("[reputationvalue]", $"‖color:{XMLExtensions.ToStringHex(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" )); reputationRewardTexts.Add(rewardText.Value); } - return RichString.Rich(TextManager.AddPunctuation(':', TextManager.Get("reputation"), LocalizedString.Join(", ", reputationRewardTexts))); + if (reputationRewardTexts.Any()) + { + return RichString.Rich(TextManager.AddPunctuation(':', TextManager.Get("reputation"), LocalizedString.Join(", ", reputationRewardTexts))); + } + else + { + return string.Empty; + } } partial void ShowMessageProjSpecific(int missionState) @@ -107,6 +136,11 @@ namespace Barotrauma }; } + public Identifier GetOverrideMusicType() + { + return Prefab.GetOverrideMusicType(State); + } + public virtual void ClientRead(IReadMessage msg) { State = msg.ReadInt16(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs index 3ec2386cf..b360f67ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs @@ -8,6 +8,7 @@ namespace Barotrauma { foreach (Mission mission in missions) { + if (!mission.Prefab.ShowStartMessage) { continue; } new GUIMessageBox(RichString.Rich(mission.Name), RichString.Rich(mission.Description), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) { IconColor = mission.Prefab.IconColor, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index c8172bfaa..0bddcc04f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -1,11 +1,16 @@ using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma { partial class MissionPrefab : PrefabWithUintIdentifier { + private ImmutableArray portraits = new ImmutableArray(); + + public bool HasPortraits => portraits.Length > 0; + public Sprite Icon { get; @@ -49,24 +54,57 @@ namespace Barotrauma private Sprite hudIcon; private Color? hudIconColor; + private ImmutableDictionary overrideMusicOnState; + partial void InitProjSpecific(ContentXElement element) { DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false); HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f); + Dictionary overrideMusic = new Dictionary(); + List portraits = new List(); foreach (var subElement in element.Elements()) { - string name = subElement.Name.ToString(); - if (name.Equals("icon", StringComparison.OrdinalIgnoreCase)) + switch (subElement.Name.ToString().ToLowerInvariant()) { - Icon = new Sprite(subElement); - IconColor = subElement.GetAttributeColor("color", Color.White); - } - else if (name.Equals("hudicon", StringComparison.OrdinalIgnoreCase)) - { - hudIcon = new Sprite(subElement); - hudIconColor = subElement.GetAttributeColor("color"); + case "icon": + Icon = new Sprite(subElement); + IconColor = subElement.GetAttributeColor("color", Color.White); + break; + case "hudicon": + hudIcon = new Sprite(subElement); + hudIconColor = subElement.GetAttributeColor("color"); + break; + case "overridemusic": + overrideMusic.Add( + subElement.GetAttributeInt("state", 0), + subElement.GetAttributeIdentifier("type", Identifier.Empty)); + break; + case "portrait": + var portrait = new Sprite(subElement, lazyLoad: true); + if (portrait != null) + { + portraits.Add(portrait); + } + break; } } + this.portraits = portraits.ToImmutableArray(); + overrideMusicOnState = overrideMusic.ToImmutableDictionary(); + } + + public Identifier GetOverrideMusicType(int state) + { + if (overrideMusicOnState.TryGetValue(state, out Identifier id)) + { + return id; + } + return Identifier.Empty; + } + + public Sprite GetPortrait(int randomSeed) + { + if (portraits.Length == 0) { return null; } + return portraits[Math.Abs(randomSeed) % portraits.Length]; } partial void DisposeProjectSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs index 3033769f1..d964fadc6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs @@ -13,11 +13,12 @@ namespace Barotrauma byte monsterCount = msg.ReadByte(); for (int i = 0; i < monsterCount; i++) { - monsters.Add(Character.ReadSpawnData(msg)); - } - if (monsters.Contains(null)) - { - throw new System.Exception("Error in MonsterMission.ClientReadInitial: monster list contains null (mission: " + Prefab.Identifier + ")"); + var monster = Character.ReadSpawnData(msg); + if (monster == null) + { + throw new System.Exception($"Error in MonsterMission.ClientReadInitial: failed to create a monster (mission: {Prefab.Identifier}, index: {i})"); + } + monsters.Add(monster); } if (monsters.Count != monsterCount) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index 12c80c496..be7a49430 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -11,38 +11,59 @@ namespace Barotrauma public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); - bool usedExistingItem = msg.ReadBoolean(); - if (usedExistingItem) + + foreach (var target in targets) { - ushort id = msg.ReadUInt16(); - item = Entity.FindEntityByID(id) as Item; - if (item == null) + bool targetFound = msg.ReadBoolean(); + if (!targetFound) { continue; } + + bool usedExistingItem = msg.ReadBoolean(); + if (usedExistingItem) { - throw new System.Exception("Error in SalvageMission.ClientReadInitial: failed to find item " + id + " (mission: " + Prefab.Identifier + ")"); + ushort id = msg.ReadUInt16(); + target.Item = Entity.FindEntityByID(id) as Item; + if (target.Item == null) + { + throw new System.Exception("Error in SalvageMission.ClientReadInitial: failed to find item " + id + " (mission: " + Prefab.Identifier + ")"); + } + } + else + { + target.Item = Item.ReadSpawnData(msg); + if (target.Item == null) + { + throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); + } + } + + int executedEffectCount = msg.ReadByte(); + for (int i = 0; i < executedEffectCount; i++) + { + int listIndex = msg.ReadByte(); + int effectIndex = msg.ReadByte(); + var selectedEffect = target.StatusEffects[listIndex][effectIndex]; + target.Item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: target.Item.Position); + } + + if (target.Item.body != null) + { + target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; } } - else + } + + public override void ClientRead(IReadMessage msg) + { + base.ClientRead(msg); + int targetCount = msg.ReadByte(); + for (int i = 0; i < targetCount; i++) { - item = Item.ReadSpawnData(msg); - if (item == null) + var state = (Target.RetrievalState)msg.ReadByte(); + if (i < targets.Count) { - throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); + targets[i].State = state; } } - - int executedEffectCount = msg.ReadByte(); - for (int i = 0; i < executedEffectCount; i++) - { - int index1 = msg.ReadByte(); - int index2 = msg.ReadByte(); - var selectedEffect = statusEffects[index1][index2]; - item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position); - } - - if (item.body != null) - { - item.body.FarseerBody.BodyType = BodyType.Kinematic; - } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index 9a33f0ce9..16ea7caa6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -1,9 +1,9 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -199,8 +199,8 @@ namespace Barotrauma if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) { size = new Point( - subElement.GetAttributeInt("width", 0), - subElement.GetAttributeInt("height", 0)); + ParseSize(subElement, "width"), + ParseSize(subElement, "height")); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 85afac3c9..5af8c6e02 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -3,7 +3,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement; @@ -29,6 +28,8 @@ namespace Barotrauma private Point resolutionWhenCreated; + private bool needsHireableRefresh; + private enum SortingMethod { AlphabeticalAsc, @@ -50,6 +51,8 @@ namespace Barotrauma campaignUI.Campaign.Map.OnLocationChanged.RegisterOverwriteExisting( "CrewManagement.UpdateLocationView".ToIdentifier(), (locationChangeInfo) => UpdateLocationView(locationChangeInfo.NewLocation, true, locationChangeInfo.PrevLocation)); + Reputation.OnAnyReputationValueChanged.RegisterOverwriteExisting( + "CrewManagement.UpdateLocationView".ToIdentifier(), _ => needsHireableRefresh = true); } public void RefreshPermissions() @@ -68,7 +71,13 @@ namespace Barotrauma { if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton) { - buyButton.Enabled = HasPermission; + CharacterInfo characterInfo = buyButton.UserData as CharacterInfo; + bool enoughReputationToHire = EnoughReputationToHire(characterInfo); + buyButton.Enabled = HasPermission && enoughReputationToHire; + foreach (GUITextBlock text in child.GetAllChildren()) + { + text.TextColor = new Color(text.TextColor, buyButton.Enabled ? 1.0f : 0.6f); + } } } } @@ -294,18 +303,21 @@ namespace Barotrauma if (sortingMethod == SortingMethod.AlphabeticalAsc) { list.Content.RectTransform.SortChildren((x, y) => + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Name.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Name)); } else if (sortingMethod == SortingMethod.JobAsc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => - String.Compare(((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Job.Name.Value, ((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Job.Name.Value, StringComparison.Ordinal)); + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? + string.Compare(((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Job.Name.Value, ((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Job.Name.Value, StringComparison.Ordinal)); } else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Salary.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Salary)); if (sortingMethod == SortingMethod.PriceDesc) { list.Content.RectTransform.ReverseChildren(); } } @@ -313,9 +325,26 @@ namespace Barotrauma { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).SkillLevel.CompareTo(((InfoSkill)y.GUIComponent.UserData).SkillLevel)); if (sortingMethod == SortingMethod.SkillDesc) { list.Content.RectTransform.ReverseChildren(); } } + + int? CompareReputationRequirement(GUIComponent c1, GUIComponent c2) + { + CharacterInfo info1 = ((InfoSkill)c1.UserData).CharacterInfo; + CharacterInfo info2 = ((InfoSkill)c2.UserData).CharacterInfo; + float requirement1 = EnoughReputationToHire(info1) ? 0 : info1.MinReputationToHire.reputation; + float requirement2 = EnoughReputationToHire(info2) ? 0 : info2.MinReputationToHire.reputation; + if (MathUtils.NearlyEqual(requirement1, 0.0f) && MathUtils.NearlyEqual(requirement2, 0.0f)) + { + return null; + } + else + { + return requirement1.CompareTo(requirement2); + } + } } private readonly struct InfoSkill @@ -367,12 +396,25 @@ namespace Barotrauma nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width); GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), - characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft) + characterInfo.Title ?? characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft) { CanBeFocused = false }; - jobBlock.Text = ToolBox.LimitString(jobBlock.Text, jobBlock.Font, jobBlock.Rect.Width); - + if (!characterInfo.MinReputationToHire.factionId.IsEmpty) + { + var faction = campaign.Factions.Find(f => f.Prefab.Identifier == characterInfo.MinReputationToHire.factionId); + if (faction != null) + { + jobBlock.TextColor = faction.Prefab.IconColor; + } + } + var fullJobText = jobBlock.Text; + jobBlock.Text = ToolBox.LimitString(fullJobText, jobBlock.Font, jobBlock.Rect.Width); + if (jobBlock.Text != fullJobText) + { + jobBlock.ToolTip = fullJobText; + jobBlock.CanBeFocused = true; + } float width = 0.6f / 3; if (characterInfo.Job != null && skill != null) { @@ -410,7 +452,7 @@ namespace Barotrauma { ClickSound = GUISoundType.Cart, UserData = characterInfo, - Enabled = HasPermission, + Enabled = CanHire(characterInfo), OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) }; hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) => @@ -426,10 +468,9 @@ namespace Barotrauma else if (!btn.Enabled) { btn.ToolTip = string.Empty; - btn.Enabled = HasPermission; + btn.Enabled = CanHire(characterInfo); } }; - } else if (listBox == pendingList) { @@ -437,7 +478,7 @@ namespace Barotrauma { ClickSound = GUISoundType.Cart, UserData = characterInfo, - Enabled = HasPermission, + Enabled = CanHire(characterInfo), OnClicked = (b, o) => RemovePendingHire(o as CharacterInfo) }; } @@ -474,12 +515,30 @@ namespace Barotrauma size = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width, mainGroup.Rect.Height); new GUIButton(new RectTransform(size, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null) { - Enabled = HasPermission, - ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", TextManager.Get($"input.{(PlayerInput.MouseButtonsSwapped() ? "rightmouse" : "leftmouse")}")), + Enabled = CanHire(characterInfo), + ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel), UserData = characterInfo, OnClicked = CreateRenamingComponent }; } + + bool CanHire(CharacterInfo characterInfo) + { + if (!HasPermission) { return false; } + return EnoughReputationToHire(characterInfo); + } + } + + private bool EnoughReputationToHire(CharacterInfo characterInfo) + { + if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) + { + if (campaign.GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation) + { + return false; + } + } + return true; } private void CreateCharacterPreviewFrame(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo) @@ -488,13 +547,13 @@ namespace Barotrauma Point absoluteOffset = new Point( pivot == Pivot.TopLeft ? listBox.Parent.Parent.Rect.Right + 5 : listBox.Parent.Parent.Rect.Left - 5, characterFrame.Rect.Top); - int frameSize = (int)(GUI.Scale * 300); - if (GameMain.GraphicsHeight - (absoluteOffset.Y + frameSize) < 0) + Point frameSize = new Point(GUI.IntScale(300), GUI.IntScale(350)); + if (GameMain.GraphicsHeight - (absoluteOffset.Y + frameSize.Y) < 0) { pivot = listBox == hireableList ? Pivot.BottomLeft : Pivot.BottomRight; absoluteOffset.Y = characterFrame.Rect.Bottom; } - characterPreviewFrame = new GUIFrame(new RectTransform(new Point(frameSize), parent: campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Parent.RectTransform, pivot: pivot) + characterPreviewFrame = new GUIFrame(new RectTransform(frameSize, parent: campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Parent.RectTransform, pivot: pivot) { AbsoluteOffset = absoluteOffset }, style: "InnerFrame") @@ -503,7 +562,8 @@ namespace Barotrauma }; GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), characterPreviewFrame.RectTransform, anchor: Anchor.Center)) { - RelativeSpacing = 0.01f + RelativeSpacing = 0.01f, + Stretch = true }; // Character info @@ -545,9 +605,23 @@ namespace Barotrauma blockHeight = 1.0f / characterSkills.Count(); foreach (Skill skill in characterSkills) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillNameGroup.RectTransform), TextManager.Get("SkillName." + skill.Identifier)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillNameGroup.RectTransform), TextManager.Get("SkillName." + skill.Identifier), font: GUIStyle.SmallFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillLevelGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.Right); } + + if (characterInfo.MinReputationToHire.reputation > 0.0f) + { + var repStr = TextManager.GetWithVariables( + "campaignstore.reputationrequired", + ("[amount]", ((int)characterInfo.MinReputationToHire.reputation).ToString()), + ("[faction]", TextManager.Get("faction." + characterInfo.MinReputationToHire.factionId).Value)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), mainGroup.RectTransform), + repStr, textColor: !EnoughReputationToHire(characterInfo) ? GUIStyle.Orange : GUIStyle.Green, + font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.Center); + } + mainGroup.Recalculate(); + characterPreviewFrame.RectTransform.MinSize = + new Point(0, (int)(mainGroup.Children.Sum(c => c.Rect.Height + mainGroup.Rect.Height * mainGroup.RelativeSpacing) / mainGroup.RectTransform.RelativeSize.Y)); } private bool SelectCharacter(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo) @@ -636,7 +710,7 @@ namespace Barotrauma List nonDuplicateHires = new List(); hires.ForEach(hireInfo => { - if(campaign.CrewManager.GetCharacterInfos().None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName())) + if (campaign.CrewManager.GetCharacterInfos().None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName())) { nonDuplicateHires.Add(hireInfo); } @@ -791,6 +865,16 @@ namespace Barotrauma playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement); } + if (needsHireableRefresh) + { + RefreshCrewFrames(hireableList); + if (sortingDropDown?.SelectedItemData != null) + { + SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData); + } + needsHireableRefresh = false; + } + (GUIComponent highlightedFrame, CharacterInfo highlightedInfo) = FindHighlightedCharacter(GUI.MouseOn); if (highlightedFrame != null && highlightedInfo != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 9a84a1b59..838c2ee27 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -106,6 +106,11 @@ namespace Barotrauma public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth; public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y); public static float RelativeVerticalAspectRatio => VerticalAspectRatio / (ReferenceResolution.Y / ReferenceResolution.X); + /// + /// A horizontal scaling factor for low aspect ratios (small width relative to height) + /// + public static float AspectRatioAdjustment => HorizontalAspectRatio < 1.4f ? (1.0f - (1.4f - HorizontalAspectRatio)) : 1.0f; + public static bool IsUltrawide => HorizontalAspectRatio > 2.0f; public static int UIWidth @@ -140,13 +145,20 @@ namespace Barotrauma public static Texture2D WhiteTexture => solidWhiteTexture; private static GUICursor MouseCursorSprites => GUIStyle.CursorSprite; - private static bool debugDrawSounds, debugDrawEvents, debugDrawMetadata; - private static int debugDrawMetadataOffset; - private static readonly string[] ignoredMetadataInfo = { string.Empty, string.Empty, string.Empty, string.Empty }; + private static bool debugDrawSounds, debugDrawEvents; + + private static DebugDrawMetaData debugDrawMetaData; + + public struct DebugDrawMetaData + { + public bool Enabled; + public bool FactionMetadata, UpgradeLevels, UpgradePrices; + public int Offset; + } public static GraphicsDevice GraphicsDevice => GameMain.Instance.GraphicsDevice; - private static List messages = new List(); + private static readonly List messages = new List(); public static GUIFrame PauseMenu { get; private set; } public static GUIFrame SettingsMenuContainer { get; private set; } @@ -195,8 +207,9 @@ namespace Barotrauma SettingsMenuOpen || DebugConsole.IsOpen || GameSession.IsTabMenuOpen || - (GameMain.GameSession?.GameMode?.Paused ?? false) || - CharacterHUD.IsCampaignInterfaceOpen; + GameMain.GameSession?.GameMode is { Paused: true } || + CharacterHUD.IsCampaignInterfaceOpen || + GameMain.GameSession?.Campaign is { SlideshowPlayer: { Finished: false, Visible: true } }; } } @@ -533,18 +546,17 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) { // TODO: TEST THIS - if (debugDrawMetadata) + if (debugDrawMetaData.Enabled) { string text = "Ctrl+M to hide campaign metadata debug info\n\n" + - $"Ctrl+1 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[0]) ? "hide" : "show")} outpost reputations, \n" + - $"Ctrl+2 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[1]) ? "hide" : "show")} faction reputations, \n" + - $"Ctrl+3 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[2]) ? "hide" : "show")} upgrade levels, \n" + - $"Ctrl+4 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[3]) ? "hide" : "show")} upgrade prices"; + $"Ctrl+1 to {(debugDrawMetaData.FactionMetadata ? "hide" : "show")} faction reputations, \n" + + $"Ctrl+2 to {(debugDrawMetaData.UpgradeLevels ? "hide" : "show")} upgrade levels, \n" + + $"Ctrl+3 to {(debugDrawMetaData.UpgradePrices ? "hide" : "show")} upgrade prices"; Vector2 textSize = GUIStyle.SmallFont.MeasureString(text); Vector2 pos = new Vector2(GameMain.GraphicsWidth - (textSize.X + 10), 300); DrawString(spriteBatch, pos, text, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); pos.Y += textSize.Y + 8; - campaignMode.CampaignMetadata?.DebugDraw(spriteBatch, pos, debugDrawMetadataOffset, ignoredMetadataInfo); + campaignMode.CampaignMetadata?.DebugDraw(spriteBatch, pos, campaignMode, debugDrawMetaData); } else { @@ -684,37 +696,24 @@ namespace Barotrauma } } - public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, float aberrationStrength = 1.0f) + public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, Color color, Rectangle? drawArea = null, SpriteEffects spriteEffects = SpriteEffects.None) { - double aberrationT = (Timing.TotalTime * 0.5f); - GameMain.GameScreen.PostProcessEffect.Parameters["blurDistance"].SetValue(0.001f * aberrationStrength); - GameMain.GameScreen.PostProcessEffect.Parameters["chromaticAberrationStrength"].SetValue(new Vector3(-0.025f, -0.01f, -0.05f) * - (float)(PerlinNoise.CalculatePerlin(aberrationT, aberrationT, 0) + 0.5f) * aberrationStrength); - - Matrix.CreateOrthographicOffCenter(0, GameMain.GraphicsWidth, GameMain.GraphicsHeight, 0, 0, -1, out Matrix projection); - - GameMain.GameScreen.PostProcessEffect.Parameters["MatrixTransform"].SetValue(projection); - GameMain.GameScreen.PostProcessEffect.CurrentTechnique = GameMain.GameScreen.PostProcessEffect.Techniques["BlurChromaticAberration"]; - GameMain.GameScreen.PostProcessEffect.CurrentTechnique.Passes[0].Apply(); - - spriteBatch.Begin(SpriteSortMode.Immediate, effect: GameMain.GameScreen.PostProcessEffect); + Rectangle area = drawArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); float scale = Math.Max( - (float)GameMain.GraphicsWidth / backgroundSprite.SourceRect.Width, - (float)GameMain.GraphicsHeight / backgroundSprite.SourceRect.Height) * 1.1f; - float paddingX = backgroundSprite.SourceRect.Width * scale - GameMain.GraphicsWidth; - float paddingY = backgroundSprite.SourceRect.Height * scale - GameMain.GraphicsHeight; + (float)area.Width / backgroundSprite.SourceRect.Width, + (float)area.Height / backgroundSprite.SourceRect.Height) * 1.1f; + float paddingX = backgroundSprite.SourceRect.Width * scale - area.Width; + float paddingY = backgroundSprite.SourceRect.Height * scale - area.Height; - double noiseT = (Timing.TotalTime * 0.02f); + double noiseT = Timing.TotalTime * 0.02f; Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f); pos = new Vector2(pos.X * paddingX, pos.Y * paddingY); spriteBatch.Draw(backgroundSprite.Texture, - new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, - null, Color.White, 0.0f, backgroundSprite.size / 2, - scale, SpriteEffects.None, 0.0f); - - spriteBatch.End(); + area.Center.ToVector2() + pos, + null, color, 0.0f, backgroundSprite.size / 2, + scale, spriteEffects, 0.0f); } #region Update list @@ -1206,48 +1205,37 @@ namespace Barotrauma } if (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.M)) { - debugDrawMetadata = !debugDrawMetadata; + debugDrawMetaData.Enabled = !debugDrawMetaData.Enabled; } - if (debugDrawMetadata) + if (debugDrawMetaData.Enabled) { if (PlayerInput.KeyHit(Keys.Up)) { - debugDrawMetadataOffset--; + debugDrawMetaData.Offset--; } - if (PlayerInput.KeyHit(Keys.Down)) { - debugDrawMetadataOffset++; + debugDrawMetaData.Offset++; } - if (PlayerInput.IsCtrlDown()) { if (PlayerInput.KeyHit(Keys.D1)) { - ignoredMetadataInfo[0] = ignoredMetadataInfo[0] == string.Empty ? "reputation.location" : string.Empty; - debugDrawMetadataOffset = 0; + debugDrawMetaData.FactionMetadata = !debugDrawMetaData.FactionMetadata; + debugDrawMetaData.Offset = 0; } - if (PlayerInput.KeyHit(Keys.D2)) { - ignoredMetadataInfo[1] = ignoredMetadataInfo[1] == string.Empty ? "reputation.faction" : string.Empty; - debugDrawMetadataOffset = 0; + debugDrawMetaData.UpgradeLevels = !debugDrawMetaData.UpgradeLevels; + debugDrawMetaData.Offset = 0; } - if (PlayerInput.KeyHit(Keys.D3)) { - ignoredMetadataInfo[2] = ignoredMetadataInfo[2] == string.Empty ? "upgrade." : string.Empty; - debugDrawMetadataOffset = 0; - } - - if (PlayerInput.KeyHit(Keys.D4)) - { - ignoredMetadataInfo[3] = ignoredMetadataInfo[3] == string.Empty ? "upgradeprice." : string.Empty; - debugDrawMetadataOffset = 0; + debugDrawMetaData.UpgradePrices = !debugDrawMetaData.UpgradePrices; + debugDrawMetaData.Offset = 0; } } - } HandlePersistingElements(deltaTime); @@ -2599,8 +2587,12 @@ namespace Barotrauma public static void AddMessage(string message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null) { - if (messages.Any(msg => msg.Text == message)) { return; } - messages.Add(new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? GUIStyle.LargeFont)); + var guiMessage = new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? GUIStyle.LargeFont); + lock (mutex) + { + if (messages.Any(msg => msg.Text == message)) { return; } + messages.Add(guiMessage); + } if (playSound) { SoundPlayer.PlayUISound(GUISoundType.UIMessage); } } @@ -2610,34 +2602,37 @@ namespace Barotrauma var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, GUIStyle.Font, sub: sub); if (playSound) { SoundPlayer.PlayUISound(soundType); } - bool overlapFound = true; - int tries = 0; - while (overlapFound) - { - overlapFound = false; - foreach (var otherMessage in messages) - { - float xDiff = otherMessage.Pos.X - newMessage.Pos.X; - if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; } - float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y; - if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; } - Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f)); - if (moveDir.LengthSquared() > 0.0001f) - { - moveDir = Vector2.Normalize(moveDir); - } - else - { - moveDir = Rand.Vector(1.0f); - } - moveDir.Y = -Math.Abs(moveDir.Y); - newMessage.Pos -= Vector2.UnitY * 10; - } - tries++; - if (tries > 20) { break; } - } - messages.Add(newMessage); + lock (mutex) + { + bool overlapFound = true; + int tries = 0; + while (overlapFound) + { + overlapFound = false; + foreach (var otherMessage in messages) + { + float xDiff = otherMessage.Pos.X - newMessage.Pos.X; + if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; } + float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y; + if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; } + Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f)); + if (moveDir.LengthSquared() > 0.0001f) + { + moveDir = Vector2.Normalize(moveDir); + } + else + { + moveDir = Rand.Vector(1.0f); + } + moveDir.Y = -Math.Abs(moveDir.Y); + newMessage.Pos -= Vector2.UnitY * 10; + } + tries++; + if (tries > 20) { break; } + } + messages.Add(newMessage); + } } public static void ClearMessages() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs index d27e2c086..10334efc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs @@ -8,9 +8,7 @@ namespace Barotrauma { public class GUICanvas : RectTransform { - private static readonly object mutex = new object(); - - protected GUICanvas() : base(size, parent: null) { } + protected GUICanvas() : base(Size, parent: null) { } private static GUICanvas _instance; public static GUICanvas Instance @@ -33,7 +31,7 @@ namespace Barotrauma //GUICanvas stores the children as weak references, to allow elements that we no longer need to get garbage collected private readonly List> childrenWeakRef = new List>(); - private static Vector2 size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); + private static Vector2 Size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); protected override Rectangle NonScaledUIRect => UIRect; @@ -41,25 +39,27 @@ namespace Barotrauma private static void OnChildrenChanged(RectTransform _) { - lock (mutex) + CrossThread.RequestExecutionOnMainThread(RefreshChildren); + } + + private static void RefreshChildren() + { + //add weak reference if we don't have one yet + foreach (var child in _instance.Children) { - //add weak reference if we don't have one yet - foreach (var child in _instance.Children) + if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) { - if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) - { - _instance.childrenWeakRef.Add(new WeakReference(child)); - } + _instance.childrenWeakRef.Add(new WeakReference(child)); } - //get rid of strong references - _instance.children.Clear(); - //remove dead children - for (int i = _instance.childrenWeakRef.Count - 2; i >= 0; i--) + } + //get rid of strong references + _instance.children.Clear(); + //remove dead children + for (int i = _instance.childrenWeakRef.Count - 1; i >= 0; i--) + { + if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) { - if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) - { - _instance.childrenWeakRef.RemoveAt(i); - } + _instance.childrenWeakRef.RemoveAt(i); } } } @@ -67,7 +67,7 @@ namespace Barotrauma // Turn public, if there is a need to call this manually. private static void RecalculateSize() { - Vector2 recalculatedSize = size; + Vector2 recalculatedSize = Size; // Scale children that are supposed to encompass the whole screen so that they are properly scaled on ultrawide as well for (int i = 0; i < Instance.childrenWeakRef.Count; i++) @@ -109,7 +109,7 @@ namespace Barotrauma } } - Instance.Resize(size, resizeChildren: true); + Instance.Resize(Size, resizeChildren: true); Instance.GetAllChildren().Select(c => c.GUIComponent as GUITextBlock).ForEach(t => t?.SetTextPos()); _instance.children.Clear(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index dd848bd65..8a795ccd2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -244,18 +244,16 @@ namespace Barotrauma return parentHierarchy.Last(); } - public void AddItem(LocalizedString text, object userData = null, LocalizedString toolTip = null) + public GUIComponent AddItem(LocalizedString text, object userData = null, LocalizedString toolTip = null, Color? color = null, Color? textColor = null) { toolTip ??= ""; if (selectMultiple) { - var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) - { IsFixedSize = false }, style: "ListBoxElement") + var frame = new GUIFrame(new RectTransform(new Point(button.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) { UserData = userData, @@ -275,7 +273,7 @@ namespace Barotrauma foreach (GUIComponent child in ListBox.Content.Children) { var tickBox = child.GetChild(); - if (tickBox.Selected) + if (tickBox is { Selected: true }) { selectedDataMultiple.Add(child.UserData); selectedIndexMultiple.Add(i); @@ -289,11 +287,11 @@ namespace Barotrauma return true; } }; + return frame; } else { - new GUITextBlock(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) - { IsFixedSize = false }, text, style: "ListBoxElement") + 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) { UserData = userData, ToolTip = toolTip @@ -323,7 +321,7 @@ namespace Barotrauma } else { - if (!(component is GUITextBlock textBlock)) + if (component is not GUITextBlock textBlock) { textBlock = component.GetChild(); if (textBlock is null && !AllowNonText) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index b03bc27a6..aa1a6a882 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -1059,6 +1059,7 @@ namespace Barotrauma GUIComponent child = Content.GetChild(childIndex); if (child is null) { return; } + if (!child.Enabled) { return; } bool wasSelected = true; if (OnSelected != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index fc1ade91e..4d3f69ea8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -268,7 +268,7 @@ namespace Barotrauma Buttons.Clear(); } - Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); + Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true, textColor: GUIStyle.TextColorBright); GUIStyle.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 4f938a89f..9edfae736 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -313,7 +313,9 @@ namespace Barotrauma break; } - RectTransform.MinSize = TextBox.RectTransform.MinSize; + RectTransform.MinSize = new Point( + Math.Max(rectT.MinSize.X, TextBox.RectTransform.MinSize.X), + Math.Max(rectT.MinSize.Y, TextBox.RectTransform.MinSize.Y)); LayoutGroup.Recalculate(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index c9a7631cb..ee15c0c06 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -1,14 +1,12 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; -using System.Text; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -20,6 +18,26 @@ namespace Barotrauma { return element.NameAsIdentifier(); } + + protected int ParseSize(XElement element, string attributeName) + { + string valueStr = element.GetAttributeString(attributeName, string.Empty); + bool relativeToWidth = valueStr.EndsWith("vw"); + bool relativeToHeight = valueStr.EndsWith("vh"); + if (relativeToWidth || relativeToHeight) + { + string floatStr = valueStr.Substring(0, valueStr.Length - 2); + if (!float.TryParse(floatStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float relativeHeight)) + { + DebugConsole.ThrowError($"Error while parsing a {nameof(GUIComponentStyle)}: {valueStr} is not a valid size."); + } + return (int)(relativeHeight / 100.0f * (relativeToWidth ? GameMain.GraphicsWidth : GameMain.GraphicsHeight)); + } + else + { + return element.GetAttributeInt(attributeName, 0); + } + } } public abstract class GUISelector where T : GUIPrefab @@ -166,7 +184,8 @@ namespace Barotrauma Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) { - return (uint)Math.Round(subElement.GetAttributeInt("size", 14) * GameSettings.CurrentConfig.Graphics.TextScale); + int rawSize = ParseSize(subElement, "size"); + return (uint)Math.Round(rawSize * GameSettings.CurrentConfig.Graphics.TextScale); } } return (uint)Math.Round(defaultSize * GameSettings.CurrentConfig.Graphics.TextScale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 4972e95d3..1ae121338 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -438,7 +438,7 @@ namespace Barotrauma } else { - if ((PlayerInput.LeftButtonClicked() || PlayerInput.RightButtonClicked()) && selected) + if ((PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked()) && selected) { if (!mouseHeldInside) { Deselect(); } mouseHeldInside = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index dec3485bb..8442731b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -143,14 +143,13 @@ namespace Barotrauma } int healthBarHeight = (int)(50f * GUI.Scale); HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight); - HealthBarAfflictionArea = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); + HealthBarAfflictionArea = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); int messageAreaWidth = GameMain.GraphicsWidth / 3; MessageAreaTop = new Rectangle((GameMain.GraphicsWidth - messageAreaWidth) / 2, ButtonAreaTop.Bottom + ButtonAreaTop.Height, messageAreaWidth, ButtonAreaTop.Height); - bool isFourByThree = GUI.IsFourByThree(); - int chatBoxWidth = !isFourByThree ? (int)(475 * GUI.Scale) : (int)(375 * GUI.Scale); + int chatBoxWidth = (int)(475 * GUI.Scale * GUI.AspectRatioAdjustment); int chatBoxHeight = (int)Math.Max(GameMain.GraphicsHeight * 0.25f, 150); ChatBoxArea = new Rectangle(Padding, GameMain.GraphicsHeight - Padding - chatBoxHeight, chatBoxWidth, chatBoxHeight); @@ -160,8 +159,9 @@ namespace Barotrauma int crewAreaY = ButtonAreaTop.Bottom + Padding; int crewAreaHeight = ObjectiveAnchor.Top - Padding - crewAreaY; - CrewArea = new Rectangle(Padding, crewAreaY, (int)Math.Max(400 * GUI.Scale, 220), crewAreaHeight); + float crewAreaWidthMultiplier = GUI.IsUltrawide ? GUI.HorizontalAspectRatio : 1.0f; + CrewArea = new Rectangle(Padding, crewAreaY, (int)(Math.Max(400 * GUI.Scale, 220) * crewAreaWidthMultiplier), crewAreaHeight); InventoryAreaLower = new Rectangle(ChatBoxArea.Right + Padding * 7, inventoryTopY, GameMain.GraphicsWidth - Padding * 9 - ChatBoxArea.Width, GameMain.GraphicsHeight - inventoryTopY); int healthWindowWidth = (int)(GameMain.GraphicsWidth * 0.5f); @@ -187,19 +187,26 @@ namespace Barotrauma public static void Draw(SpriteBatch spriteBatch) { - DrawRectangle(ButtonAreaTop, Color.White * 0.5f); - DrawRectangle(TutorialObjectiveListArea, GUIStyle.Blue * 0.5f); - DrawRectangle(MessageAreaTop, GUIStyle.Orange * 0.5f); - DrawRectangle(CrewArea, Color.Blue * 0.5f); - DrawRectangle(ChatBoxArea, Color.Cyan * 0.5f); - DrawRectangle(HealthBarArea, Color.Red * 0.5f); - DrawRectangle(HealthBarAfflictionArea, Color.Red * 0.5f); - DrawRectangle(InventoryAreaLower, Color.Yellow * 0.5f); - DrawRectangle(HealthWindowAreaLeft, Color.Red * 0.5f); - DrawRectangle(BottomRightInfoArea, Color.Green * 0.5f); - DrawRectangle(ItemHUDArea, Color.Magenta * 0.3f); + DrawRectangle(nameof(ButtonAreaTop), ButtonAreaTop, Color.White * 0.5f); + DrawRectangle(nameof(TutorialObjectiveListArea), TutorialObjectiveListArea, GUIStyle.Blue * 0.5f); + DrawRectangle(nameof(MessageAreaTop), MessageAreaTop, GUIStyle.Orange * 0.5f); + DrawRectangle(nameof(CrewArea), CrewArea, Color.Blue * 0.5f); + DrawRectangle(nameof(ChatBoxArea), ChatBoxArea, Color.Cyan * 0.5f); + DrawRectangle(nameof(HealthBarArea), HealthBarArea, Color.Red * 0.5f); + DrawRectangle(nameof(HealthBarAfflictionArea), HealthBarAfflictionArea, Color.Red * 0.5f); + DrawRectangle(nameof(InventoryAreaLower), InventoryAreaLower, Color.Yellow * 0.5f); + DrawRectangle(nameof(HealthWindowAreaLeft), HealthWindowAreaLeft, Color.Red * 0.5f); + DrawRectangle(nameof(BottomRightInfoArea), BottomRightInfoArea, Color.Green * 0.5f); + DrawRectangle(nameof(ItemHUDArea), ItemHUDArea, Color.Magenta * 0.3f); - void DrawRectangle(Rectangle r, Color c) => GUI.DrawRectangle(spriteBatch, r, c); + void DrawRectangle(string label, Rectangle r, Color c) + { + if (!label.IsNullOrEmpty()) + { + GUI.DrawString(spriteBatch, r.Location.ToVector2() + Vector2.One * 3, label, c, font: GUIStyle.SmallFont); + } + GUI.DrawRectangle(spriteBatch, r, c); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index b8026a67c..0c5c6cc17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -11,9 +11,9 @@ namespace Barotrauma { class LoadingScreen { - private readonly Texture2D defaultBackgroundTexture, overlay; + private readonly Sprite defaultBackgroundTexture, overlay; private readonly SpriteSheet decorativeGraph, decorativeMap; - private Texture2D currentBackgroundTexture; + private Sprite currentBackgroundTexture; private readonly Sprite noiseSprite; private string randText = ""; @@ -24,6 +24,8 @@ namespace Barotrauma private Video currSplashScreen; private DateTime videoStartTime; + private bool mirrorBackground; + public struct PendingSplashScreen { public string Filename; @@ -112,12 +114,12 @@ namespace Barotrauma public LoadingScreen(GraphicsDevice graphics) { - defaultBackgroundTexture = TextureLoader.FromFile("Content/Map/LocationPortraits/AlienRuins.png"); + defaultBackgroundTexture = new Sprite("Content/Map/LocationPortraits/MainMenu1.png", Vector2.Zero); decorativeMap = new SpriteSheet("Content/Map/MapHUD.png", 6, 5, Vector2.Zero, sourceRect: new Rectangle(0, 0, 2048, 640)); decorativeGraph = new SpriteSheet("Content/Map/MapHUD.png", 4, 10, Vector2.Zero, sourceRect: new Rectangle(1025, 1259, 1024, 732)); - overlay = TextureLoader.FromFile("Content/UI/LoadingScreenOverlay.png"); + overlay = new Sprite("Content/UI/MainMenuVignette.png", Vector2.Zero); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero); DrawLoadingText = true; SetSelectedTip(TextManager.Get("LoadingScreenTip")); @@ -138,35 +140,24 @@ namespace Barotrauma DisableSplashScreen(); } } - - var titleStyle = GUIStyle.GetComponentStyle("TitleText"); - Sprite titleSprite = null; - if (!WaitForLanguageSelection && titleStyle != null && titleStyle.Sprites.ContainsKey(GUIComponent.ComponentState.None)) - { - titleSprite = titleStyle.Sprites[GUIComponent.ComponentState.None].First()?.Sprite; - } drawn = true; currentBackgroundTexture ??= defaultBackgroundTexture; + float overlayScale = Math.Min(GameMain.GraphicsWidth / overlay.size.X, GameMain.GraphicsHeight / overlay.size.Y); + + Rectangle drawArea = new Rectangle( + (int)(overlay.size.X * overlayScale / 2), 0, + (int)(GameMain.GraphicsWidth - overlay.size.X * overlayScale / 2), GameMain.GraphicsHeight); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState); - float scale = (GameMain.GraphicsWidth / (float)currentBackgroundTexture.Width) * 1.2f; - float paddingX = currentBackgroundTexture.Width * scale - GameMain.GraphicsWidth; - float paddingY = currentBackgroundTexture.Height * scale - GameMain.GraphicsHeight; - - double noiseT = (Timing.TotalTime * 0.02f); - Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f); - pos = new Vector2(pos.X * paddingX, pos.Y * paddingY); - - spriteBatch.Draw(currentBackgroundTexture, - new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, - null, Color.White, 0.0f, new Vector2(currentBackgroundTexture.Width / 2, currentBackgroundTexture.Height / 2), - scale, SpriteEffects.None, 0.0f); - - spriteBatch.Draw(overlay, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), null, Color.White, 0.0f, Vector2.Zero, SpriteEffects.None, 0.0f); + GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea, + spriteEffects: mirrorBackground ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale); + double noiseT = Timing.TotalTime * 0.02f; float noiseStrength = (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0); float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 4.0f; noiseSprite.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight), @@ -174,10 +165,7 @@ namespace Barotrauma color: Color.White * noiseStrength * 0.1f, textureScale: Vector2.One * noiseScale); - titleSprite?.Draw(spriteBatch, new Vector2(GameMain.GraphicsWidth * 0.05f, GameMain.GraphicsHeight * 0.125f), - Color.White, origin: new Vector2(0.0f, titleSprite.SourceRect.Height / 2.0f), - scale: GameMain.GraphicsHeight / 2000.0f); - + Vector2 textPos = new Vector2((int)(GameMain.GraphicsWidth * 0.05f), (int)(GameMain.GraphicsHeight * 0.75f)); if (WaitForLanguageSelection) { DrawLanguageSelectionPrompt(spriteBatch, graphics); @@ -215,16 +203,18 @@ namespace Barotrauma #endif } } + if (GUIStyle.LargeFont.HasValue) { GUIStyle.LargeFont.DrawString(spriteBatch, loadText.ToUpper(), - new Vector2(GameMain.GraphicsWidth / 2.0f - GUIStyle.LargeFont.MeasureString(loadText.ToUpper()).X / 2.0f, GameMain.GraphicsHeight * 0.75f), + textPos, Color.White); + textPos.Y += GUIStyle.LargeFont.MeasureString(loadText.ToUpper()).Y * 1.2f; } if (GUIStyle.Font.HasValue && selectedTip != null) { - string wrappedTip = ToolBox.WrapText(selectedTip.SanitizedValue, GameMain.GraphicsWidth * 0.5f, GUIStyle.Font.Value); + string wrappedTip = ToolBox.WrapText(selectedTip.SanitizedValue, GameMain.GraphicsWidth * 0.3f, GUIStyle.Font.Value); string[] lines = wrappedTip.Split('\n'); float lineHeight = GUIStyle.Font.MeasureString(selectedTip).Y; @@ -234,7 +224,8 @@ namespace Barotrauma for (int i = 0; i < lines.Length; i++) { GUIStyle.Font.DrawStringWithColors(spriteBatch, lines[i], - new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUIStyle.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White, + new Vector2(textPos.X, (int)(textPos.Y + i * lineHeight)), + Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f, selectedTip.RichTextData.Value, rtdOffset); rtdOffset += lines[i].Length; } @@ -244,7 +235,8 @@ namespace Barotrauma for (int i = 0; i < lines.Length; i++) { GUIStyle.Font.DrawString(spriteBatch, lines[i], - new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUIStyle.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White); + new Vector2(textPos.X, (int)(textPos.Y + i * lineHeight)), + new Color(228, 217, 167, 255)); } } } @@ -257,13 +249,16 @@ namespace Barotrauma Vector2 decorativeScale = new Vector2(GameMain.GraphicsHeight / 1080.0f); float noiseVal = (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.25f, Timing.TotalTime * 0.5f, 0); - decorativeGraph.Draw(spriteBatch, (int)(decorativeGraph.FrameCount * noiseVal), - new Vector2(GameMain.GraphicsWidth * 0.001f, GameMain.GraphicsHeight * 0.24f), - Color.White, Vector2.Zero, 0.0f, decorativeScale, SpriteEffects.FlipVertically); + if (!WaitForLanguageSelection) + { + decorativeGraph.Draw(spriteBatch, (int)(decorativeGraph.FrameCount * noiseVal), + new Vector2(GameMain.GraphicsWidth * 0.001f, textPos.Y), + Color.White, new Vector2(0, decorativeMap.FrameSize.Y), 0.0f, decorativeScale, SpriteEffects.FlipVertically); + } decorativeMap.Draw(spriteBatch, (int)(decorativeMap.FrameCount * noiseVal), - new Vector2(GameMain.GraphicsWidth * 0.99f, GameMain.GraphicsHeight * 0.66f), - Color.White, decorativeMap.FrameSize.ToVector2(), 0.0f, decorativeScale); + new Vector2(GameMain.GraphicsWidth * 0.99f, GameMain.GraphicsHeight * 0.01f), + Color.White, new Vector2(decorativeMap.FrameSize.X, 0), 0.0f, decorativeScale, SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically); if (noiseVal < 0.2f) { @@ -285,8 +280,9 @@ namespace Barotrauma if (GUIStyle.LargeFont.HasValue) { + Vector2 textSize = GUIStyle.LargeFont.MeasureString(randText); GUIStyle.LargeFont.DrawString(spriteBatch, randText, - new Vector2(GameMain.GraphicsWidth - decorativeMap.FrameSize.X * decorativeScale.X * 0.8f, GameMain.GraphicsHeight * 0.57f), + new Vector2(GameMain.GraphicsWidth * 0.95f - textSize.X, GameMain.GraphicsHeight * 0.06f), Color.White * (1.0f - noiseVal)); } @@ -312,8 +308,8 @@ namespace Barotrauma languageSelectionCursor = new Sprite("Content/UI/cursor.png", Vector2.Zero); } - Vector2 textPos = new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight * 0.3f); - Vector2 textSpacing = new Vector2(0.0f, (GameMain.GraphicsHeight * 0.5f) / AvailableLanguages.Length); + Vector2 textPos = new Vector2((int)(GameMain.GraphicsWidth * 0.05f), (int)(GameMain.GraphicsHeight * 0.3f)); + Vector2 textSpacing = new Vector2(0.0f, GameMain.GraphicsHeight * 0.5f / AvailableLanguages.Length); foreach (LanguageIdentifier language in AvailableLanguages) { string localizedLanguageName = TextManager.GetTranslatedLanguageName(language); @@ -321,10 +317,10 @@ namespace Barotrauma Vector2 textSize = font.MeasureString(localizedLanguageName); bool hover = - Math.Abs(PlayerInput.MousePosition.X - textPos.X) < textSize.X / 2 && - Math.Abs(PlayerInput.MousePosition.Y - textPos.Y) < textSpacing.Y / 2; + PlayerInput.MousePosition.X > textPos.X && PlayerInput.MousePosition.X < textPos.X + textSize.X && + PlayerInput.MousePosition.Y > textPos.Y && PlayerInput.MousePosition.Y < textPos.Y + textSize.Y; - font.DrawString(spriteBatch, localizedLanguageName, textPos - textSize / 2, + font.DrawString(spriteBatch, localizedLanguageName, textPos, hover ? Color.White : Color.White * 0.6f); if (hover && PlayerInput.PrimaryMouseButtonClicked()) { @@ -431,7 +427,12 @@ namespace Barotrauma drawn = false; LoadState = null; SetSelectedTip(TextManager.Get("LoadingScreenTip")); - currentBackgroundTexture = LocationType.Prefabs.GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue))?.Texture; + currentBackgroundTexture = LocationType.Prefabs.Where(p => p.UsePortraitInRandomLoadingScreens).GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue)); + if (GameMain.GameSession?.GameMode?.Missions is { } missions && missions.Any(m => m.Prefab.HasPortraits)) + { + currentBackgroundTexture = missions.Where(m => m.Prefab.HasPortraits).First().Prefab.GetPortrait(Rand.Int(int.MaxValue)); + } + mirrorBackground = Rand.Range(0.0f, 1.0f) < 0.5f; while (!drawn) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 6f5272743..c43df5f39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -143,33 +143,32 @@ namespace Barotrauma { public readonly MedicalClinic.NetAffliction Target; public readonly ImmutableArray ElementsToDisable; + public readonly GUIComponent TargetElement; - public PopupAffliction(ImmutableArray elementsToDisable, MedicalClinic.NetAffliction target) + public PopupAffliction(ImmutableArray elementsToDisable, GUIComponent component, MedicalClinic.NetAffliction target) { Target = target; ElementsToDisable = elementsToDisable; + TargetElement = component; } } private readonly struct PopupAfflictionList { public readonly MedicalClinic.NetCrewMember Target; + public readonly GUIListBox ListElement; public readonly GUIButton TreatAllButton; - public readonly List Afflictions; + public readonly HashSet Afflictions; - public PopupAfflictionList(MedicalClinic.NetCrewMember crewMember, GUIButton treatAllButton) + public PopupAfflictionList(MedicalClinic.NetCrewMember crewMember, GUIListBox listElement, GUIButton treatAllButton) { + ListElement = listElement; Target = crewMember; TreatAllButton = treatAllButton; - Afflictions = new List(); + Afflictions = new HashSet(); } } - // private enum SortMode - // { - // Severity - // } - private readonly MedicalClinic medicalClinic; private readonly GUIComponent container; private Point prevResolution; @@ -221,23 +220,22 @@ namespace Barotrauma private void UpdatePopupAfflictions() { - if (selectedCrewAfflictionList is { } afflictionList) - { - foreach (PopupAffliction popupAffliction in afflictionList.Afflictions) - { - ToggleElements(ElementState.Enabled, popupAffliction.ElementsToDisable); - if (medicalClinic.IsAfflictionPending(afflictionList.Target, popupAffliction.Target)) - { - ToggleElements(ElementState.Disabled, popupAffliction.ElementsToDisable); - } - } + if (selectedCrewAfflictionList is not { } afflictionList) { return; } - afflictionList.TreatAllButton.Enabled = true; - if (afflictionList.Afflictions.All(a => medicalClinic.IsAfflictionPending(afflictionList.Target, a.Target))) + foreach (PopupAffliction popupAffliction in afflictionList.Afflictions) + { + ToggleElements(ElementState.Enabled, popupAffliction.ElementsToDisable); + if (medicalClinic.IsAfflictionPending(afflictionList.Target, popupAffliction.Target)) { - afflictionList.TreatAllButton.Enabled = false; + ToggleElements(ElementState.Disabled, popupAffliction.ElementsToDisable); } } + + afflictionList.TreatAllButton.Enabled = true; + if (afflictionList.Afflictions.All(a => medicalClinic.IsAfflictionPending(afflictionList.Target, a.Target))) + { + afflictionList.TreatAllButton.Enabled = false; + } } private void UpdatePending() @@ -309,7 +307,7 @@ namespace Barotrauma } } - private void UpdateCrewPanel() + public void UpdateCrewPanel() { if (crewHealList is not { } healList) { return; } @@ -502,7 +500,7 @@ namespace Barotrauma return true; } }; - + crewHealList = new CrewHealList(crewList, parent, treatAllButton); void OnReceived(MedicalClinic.CallbackOnlyRequest obj) @@ -526,7 +524,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { - TextGetter = () => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)(info.Character?.HealthPercentage ?? 100f)}"), + TextGetter = () => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)MathF.Round(info.Character?.HealthPercentage ?? 100f)}"), TextColor = GUIStyle.Green }; @@ -789,7 +787,7 @@ namespace Barotrauma GUIListBox afflictionList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), mainLayout.RectTransform)) { Visible = false }; - PopupAfflictionList popupAfflictionList = new PopupAfflictionList(crewMember, treatAllButton); + PopupAfflictionList popupAfflictionList = new PopupAfflictionList(crewMember, afflictionList, treatAllButton); selectedCrewElement = mainFrame; selectedCrewAfflictionList = popupAfflictionList; @@ -810,9 +808,9 @@ namespace Barotrauma List allComponents = new List(); foreach (MedicalClinic.NetAffliction affliction in request.Afflictions) { - ImmutableArray createdComponents = CreatePopupAffliction(afflictionList.Content, crewMember, affliction); - allComponents.AddRange(createdComponents); - popupAfflictionList.Afflictions.Add(new PopupAffliction(createdComponents, affliction)); + CreatedPopupAfflictionElement createdComponents = CreatePopupAffliction(afflictionList.Content, crewMember, affliction); + allComponents.AddRange(createdComponents.AllCreatedElements); + popupAfflictionList.Afflictions.Add(new PopupAffliction(createdComponents.AllCreatedElements, createdComponents.MainElement, affliction)); } allComponents.Add(treatAllButton); @@ -832,9 +830,11 @@ namespace Barotrauma } } - private ImmutableArray CreatePopupAffliction(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction) + private readonly record struct CreatedPopupAfflictionElement(GUIComponent MainElement, ImmutableArray AllCreatedElements); + + private CreatedPopupAfflictionElement CreatePopupAffliction(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction) { - if (!(affliction.Prefab is { } prefab)) { return ImmutableArray.Empty; } + ToolBox.ThrowIfNull(affliction.Prefab); GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), parent.RectTransform), style: "ListBoxElement"); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), backgroundFrame.RectTransform, Anchor.BottomCenter), style: "HorizontalLine"); @@ -846,9 +846,9 @@ namespace Barotrauma GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.33f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; - Color iconColor = CharacterHealth.GetAfflictionIconColor(prefab, affliction.Strength); + Color iconColor = CharacterHealth.GetAfflictionIconColor(affliction.Prefab, affliction.Strength); - GUIImage icon = new GUIImage(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), prefab.Icon, scaleToFit: true) + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), affliction.Prefab.Icon, scaleToFit: true) { Color = iconColor, DisabledColor = iconColor * 0.5f @@ -856,7 +856,7 @@ namespace Barotrauma GUILayoutGroup topTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, topLayout.RectTransform), isHorizontal: true); - GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUIStyle.SubHeadingFont); + GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), affliction.Prefab.Name, font: GUIStyle.SubHeadingFont); Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); @@ -878,7 +878,7 @@ namespace Barotrauma AutoScaleHorizontal = true }; - EnsureTextDoesntOverflow(prefab.Name.Value, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); + EnsureTextDoesntOverflow(affliction.Prefab.Name.Value, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), mainLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -923,7 +923,7 @@ namespace Barotrauma return true; }; - return elementsToDisable; + return new CreatedPopupAfflictionElement(backgroundFrame, elementsToDisable); } private void AddPending(ImmutableArray elementsToDisable, MedicalClinic.NetCrewMember crewMember, ImmutableArray afflictions) @@ -1033,11 +1033,53 @@ namespace Barotrauma } } + public void UpdateAfflictions(MedicalClinic.NetCrewMember crewMember) + { + if (selectedCrewAfflictionList is not { } afflictionList || !afflictionList.Target.CharacterEquals(crewMember)) { return; } + + List allComponents = new List(); + foreach (PopupAffliction existingAffliction in afflictionList.Afflictions.ToHashSet()) + { + if (crewMember.Afflictions.None(received => received.AfflictionEquals(existingAffliction.Target))) + { + // remove from UI + existingAffliction.TargetElement.RectTransform.Parent = null; + afflictionList.Afflictions.Remove(existingAffliction); + } + else + { + allComponents.AddRange(existingAffliction.ElementsToDisable); + } + } + + foreach (MedicalClinic.NetAffliction received in crewMember.Afflictions) + { + // we're not that concerned about updating the strength of the afflictions + if (afflictionList.Afflictions.Any(existing => existing.Target.AfflictionEquals(received))) { continue; } + + CreatedPopupAfflictionElement createdComponents = CreatePopupAffliction(afflictionList.ListElement.Content, crewMember, received); + allComponents.AddRange(createdComponents.AllCreatedElements); + afflictionList.Afflictions.Add(new PopupAffliction(createdComponents.AllCreatedElements, createdComponents.MainElement, received)); + } + + allComponents.Add(afflictionList.TreatAllButton); + afflictionList.TreatAllButton.OnClicked = (_, _) => + { + var afflictions = crewMember.Afflictions.Where(a => !medicalClinic.IsAfflictionPending(crewMember, a)).ToImmutableArray(); + if (!afflictions.Any()) { return true; } + + AddPending(allComponents.ToImmutableArray(), crewMember, afflictions); + return true; + }; + + UpdatePopupAfflictions(); + } + public void ClosePopup() { if (selectedCrewElement is { } popup) { - popup.Parent?.RemoveChild(selectedCrewElement); + popup.RectTransform.Parent = null; } selectedCrewElement = null; @@ -1096,5 +1138,14 @@ namespace Barotrauma refreshTimer = 0; } } + + public void OnDeselected() + { + if (GameMain.NetworkMember is not null) + { + MedicalClinic.SendUnsubscribeRequest(); + } + ClosePopup(); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 7eb393015..1cb5e37ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -207,11 +207,12 @@ namespace Barotrauma cargoManager.OnItemsInSellFromSubCrateChanged.RegisterOverwriteExisting(refreshStoreId, _ => needsSellingFromSubRefresh = true); } - public void SelectStore(Identifier identifier) + public void SelectStore(Character merchant) { + Identifier storeIdentifier = merchant?.MerchantIdentifier ?? Identifier.Empty; if (CurrentLocation?.Stores != null) { - if (!identifier.IsEmpty && CurrentLocation.GetStore(identifier) is { } store) + if (!storeIdentifier.IsEmpty && CurrentLocation.GetStore(storeIdentifier) is { } store) { ActiveStore = store; if (storeNameBlock != null) @@ -223,12 +224,13 @@ namespace Barotrauma } storeNameBlock.SetRichText(storeName); } + ActiveStore.SetMerchantFaction(merchant.Faction); } else { ActiveStore = null; string errorId, msg; - if (identifier.IsEmpty) + if (storeIdentifier.IsEmpty) { errorId = "Store.SelectStore:IdentifierEmpty"; msg = $"Error selecting store at {CurrentLocation}: identifier is empty."; @@ -236,7 +238,7 @@ namespace Barotrauma else { errorId = "Store.SelectStore:StoreDoesntExist"; - msg = $"Error selecting store with identifier \"{identifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; } DebugConsole.LogError(msg); GameAnalyticsManager.AddErrorEventOnce(errorId, GameAnalyticsManager.ErrorSeverity.Error, msg); @@ -249,17 +251,17 @@ namespace Barotrauma if (campaignUI.Campaign.Map == null) { errorId = "Store.SelectStore:MapNull"; - msg = $"Error selecting store with identifier \"{identifier}\": Map is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": Map is null."; } else if (CurrentLocation == null) { errorId = "Store.SelectStore:CurrentLocationNull"; - msg = $"Error selecting store with identifier \"{identifier}\": CurrentLocation is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": CurrentLocation is null."; } else if (CurrentLocation.Stores == null) { errorId = "Store.SelectStore:StoresNull"; - msg = $"Error selecting store with identifier \"{identifier}\": CurrentLocation.Stores is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": CurrentLocation.Stores is null."; } if (!msg.IsNullOrEmpty()) { @@ -406,11 +408,11 @@ namespace Barotrauma TextScale = 1.1f, TextGetter = () => { - if (CurrentLocation != null) + if (ActiveStore is not null) { Color textColor = GUIStyle.ColorReputationNeutral; string sign = ""; - int reputationModifier = (int)MathF.Round((CurrentLocation.GetStoreReputationModifier(activeTab == StoreTab.Buy) - 1) * 100); + int reputationModifier = (int)MathF.Round((ActiveStore.GetReputationModifier(activeTab == StoreTab.Buy) - 1) * 100); if (reputationModifier > 0) { textColor = IsBuying ? GUIStyle.ColorReputationLow : GUIStyle.ColorReputationHigh; @@ -727,7 +729,7 @@ namespace Barotrauma ChangeStoreTab(StoreTab.Buy); if (newLocation?.Reputation != null) { - CurrentLocation.Reputation.OnReputationValueChanged.RegisterOverwriteExisting("RefreshStore".ToIdentifier(), _ => { SetNeedsRefresh(); }); + newLocation.Reputation.OnReputationValueChanged.RegisterOverwriteExisting("RefreshStore".ToIdentifier(), _ => { SetNeedsRefresh(); }); } } @@ -855,6 +857,28 @@ namespace Barotrauma FilterStoreItems(category, searchBox.Text); } + private static KeyValuePair? GetReputationRequirement(PriceInfo priceInfo) + { + return GameMain.GameSession?.Campaign is not null + ? priceInfo.MinReputation.FirstOrNull() + : null; + } + + private static KeyValuePair? GetTooLowReputation(PriceInfo priceInfo) + { + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + foreach (var minRep in priceInfo.MinReputation) + { + if (campaign.GetReputation(minRep.Key) < minRep.Value) + { + return minRep; + } + } + } + return null; + } + int prevDailySpecialCount, prevRequestedGoodsCount, prevSubRequestedGoodsCount; private void RefreshStoreBuyList() @@ -898,6 +922,7 @@ namespace Barotrauma { if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo) && itemPrefab.CanCharacterBuy()) { + bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab); var itemFrame = isDailySpecial ? storeDailySpecialsGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : @@ -922,7 +947,8 @@ namespace Barotrauma SetOwnedText(itemFrame); SetPriceGetters(itemFrame, true); } - SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0); + + SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0 && !GetTooLowReputation(priceInfo).HasValue); existingItemFrames.Add(itemFrame); } } @@ -1317,6 +1343,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } int sortResult = itemX.ItemPrefab.Name != itemY.ItemPrefab.Name ? itemX.ItemPrefab.Name.CompareTo(itemY.ItemPrefab.Name) : itemX.ItemPrefab.Identifier.CompareTo(itemY.ItemPrefab.Identifier); @@ -1345,6 +1373,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } int sortResult = ActiveStore.GetAdjustedItemSellPrice(itemX.ItemPrefab).CompareTo( ActiveStore.GetAdjustedItemSellPrice(itemY.ItemPrefab)); if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } @@ -1369,6 +1399,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } int sortResult = ActiveStore.GetAdjustedItemBuyPrice(itemX.ItemPrefab).CompareTo( ActiveStore.GetAdjustedItemBuyPrice(itemY.ItemPrefab)); if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } @@ -1391,10 +1423,12 @@ namespace Barotrauma specialsGroup.Recalculate(); } - static int CompareByCategory(RectTransform x, RectTransform y) + int CompareByCategory(RectTransform x, RectTransform y) { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } return itemX.ItemPrefab.Category.CompareTo(itemY.ItemPrefab.Category); } else @@ -1424,6 +1458,19 @@ namespace Barotrauma } } + int CompareByReputationRestriction(PurchasedItem item1, PurchasedItem item2) + { + PriceInfo priceInfo1 = item1.ItemPrefab.GetPriceInfo(ActiveStore); + PriceInfo priceInfo2 = item2.ItemPrefab.GetPriceInfo(ActiveStore); + if (priceInfo1 != null && priceInfo2 != null) + { + var requiredReputation1 = GetTooLowReputation(priceInfo1)?.Value ?? 0.0f; + var requiredReputation2 = GetTooLowReputation(priceInfo2)?.Value ?? 0.0f; + return requiredReputation1.CompareTo(requiredReputation2); + } + return 0; + } + static int CompareByElement(RectTransform x, RectTransform y) { if (ShouldBeOnTop(x) || ShouldBeOnBottom(y)) @@ -1753,7 +1800,7 @@ namespace Barotrauma { if (pi.ItemPrefab?.InventoryIcon != null) { - icon.Color = pi.ItemPrefab.InventoryIconColor * (enabled ? 1.0f: 0.5f); + icon.Color = pi.ItemPrefab.InventoryIconColor * (enabled ? 1.0f : 0.5f); } else if (pi.ItemPrefab?.Sprite != null) { @@ -1858,7 +1905,7 @@ namespace Barotrauma LocalizedString toolTip = string.Empty; if (purchasedItem.ItemPrefab != null) { - toolTip = purchasedItem.ItemPrefab.GetTooltip(); + toolTip = purchasedItem.ItemPrefab.GetTooltip(Character.Controlled); if (itemQuantity != null) { if (itemQuantity.AllNonEmpty) @@ -1871,6 +1918,23 @@ namespace Barotrauma toolTip += $"\n{TextManager.GetWithVariable("campaignstore.ownedtotal", "[amount]", itemQuantity.Total.ToString())}"; } } + + PriceInfo priceInfo = purchasedItem.ItemPrefab.GetPriceInfo(ActiveStore); + var campaign = GameMain.GameSession?.Campaign; + if (priceInfo != null && campaign != null) + { + var requiredReputation = GetReputationRequirement(priceInfo); + if (requiredReputation != null) + { + var repStr = TextManager.GetWithVariables( + "campaignstore.reputationrequired", + ("[amount]", ((int)requiredReputation.Value.Value).ToString()), + ("[faction]", TextManager.Get("faction." + requiredReputation.Value.Key).Value)); + Color color = campaign.GetReputation(requiredReputation.Value.Key) < requiredReputation.Value.Value ? + GUIStyle.Orange : GUIStyle.Green; + toolTip += $"\n‖color:{color.ToStringHex()}‖{repStr}‖color:end‖"; + } + } } itemComponent.ToolTip = RichString.Rich(toolTip); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index ed1740549..d1ea71205 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -15,8 +15,6 @@ namespace Barotrauma private int pageCount; private readonly bool transferService, purchaseService; private bool initialized; - private int deliveryFee; - private string deliveryLocationName; public GUIFrame GuiFrame; private GUIFrame pageIndicatorHolder; @@ -34,14 +32,13 @@ namespace Barotrauma private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; private SubmarineInfo selectedSubmarine = null; - private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, selectedSubText, switchText, missingPreviewText, currencyName; + private LocalizedString purchaseAndSwitchText, purchaseOnlyText, selectedSubText, switchText, missingPreviewText, currencyName; private readonly RectTransform parent; private readonly Action closeAction; private Sprite pageIndicator; private readonly LocalizedString[] messageBoxOptions; - public const int DeliveryFeePerDistanceTravelled = 1000; public static bool ContentRefreshRequired = false; private static readonly Color indicatorColor = new Color(112, 149, 129); @@ -108,14 +105,9 @@ namespace Barotrauma { initialized = true; selectedSubText = TextManager.Get("selectedsub"); - deliveryText = TextManager.Get("requestdeliverybutton"); switchText = TextManager.Get("switchtosubmarinebutton"); purchaseAndSwitchText = TextManager.Get("purchaseandswitch"); purchaseOnlyText = TextManager.Get("purchase"); - if (transferService) - { - deliveryFee = CalculateDeliveryFee(); - } currencyName = TextManager.Get("credit").Value.ToLowerInvariant(); @@ -124,13 +116,6 @@ namespace Barotrauma CreateGUI(); } - private int CalculateDeliveryFee() - { - int distanceToOutpost = GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - deliveryLocationName = endLocation.Name; - return DeliveryFeePerDistanceTravelled * distanceToOutpost; - } - private void CreateGUI() { createdForResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); @@ -194,7 +179,7 @@ namespace Barotrauma confirmButtonAlt = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseOnlyText, style: "GUIButtonFreeScale"); transferInfoFrameWidth -= confirmButtonAlt.RectTransform.RelativeSize.X; } - confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseService ? purchaseAndSwitchText : deliveryFee > 0 ? deliveryText : switchText, style: "GUIButtonFreeScale"); + confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseService ? purchaseAndSwitchText : switchText, style: "GUIButtonFreeScale"); SetConfirmButtonState(false); transferInfoFrameWidth -= confirmButton.RectTransform.RelativeSize.X; GUIFrame transferInfoFrame = new GUIFrame(new RectTransform(new Vector2(transferInfoFrameWidth, 1.0f), bottomContainer.RectTransform), style: null) @@ -406,22 +391,14 @@ namespace Barotrauma if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) { - LocalizedString amountString = TextManager.FormatCurrency(subToDisplay.Price); + LocalizedString amountString = TextManager.FormatCurrency(subToDisplay.GetPrice()); submarineDisplays[i].submarineFee.Text = TextManager.GetWithVariable("price", "[amount]", amountString); } else { if (subToDisplay.Name != CurrentOrPendingSubmarine().Name) { - if (deliveryFee > 0) - { - LocalizedString amountString = TextManager.FormatCurrency(deliveryFee); - submarineDisplays[i].submarineFee.Text = TextManager.GetWithVariable("deliveryfee", "[amount]", amountString); - } - else - { - submarineDisplays[i].submarineFee.Text = string.Empty; - } + submarineDisplays[i].submarineFee.Text = string.Empty; } else { @@ -581,7 +558,7 @@ namespace Barotrauma if (owned) { - confirmButton.Text = deliveryFee > 0 ? deliveryText : switchText; + confirmButton.Text = switchText; confirmButton.OnClicked = (button, userData) => { ShowTransferPrompt(); @@ -702,37 +679,12 @@ namespace Barotrauma private void ShowTransferPrompt() { - if (!GameMain.GameSession.Campaign.CanAfford(deliveryFee) && deliveryFee > 0) - { - new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", - ("[currencyname]", currencyName), - ("[submarinename]", selectedSubmarine.DisplayName), - ("[location1]", deliveryLocationName), - ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name))); - return; - } + var text = TextManager.GetWithVariables("switchsubmarinetext", + ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), + ("[submarinename2]", selectedSubmarine.DisplayName)); + text += GetItemTransferText(); + GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), text, messageBoxOptions); - GUIMessageBox msgBox; - - if (deliveryFee > 0) - { - msgBox = new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("deliveryrequesttext", - ("[submarinename1]", selectedSubmarine.DisplayName), - ("[location1]", deliveryLocationName), - ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name), - ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName), - ("[amount]", deliveryFee.ToString()), - ("[currencyname]", currencyName)), messageBoxOptions); - msgBox.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; - } - else - { - var text = TextManager.GetWithVariables("switchsubmarinetext", - ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), - ("[submarinename2]", selectedSubmarine.DisplayName)); - text += GetItemTransferText(); - msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), text, messageBoxOptions); - } msgBox.Buttons[0].OnClicked = (applyButton, obj) => { @@ -777,7 +729,7 @@ namespace Barotrauma { if (GameMain.Client == null) { - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, deliveryFee); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch); RefreshSubmarineDisplay(true); } else @@ -797,7 +749,9 @@ namespace Barotrauma private void ShowBuyPrompt(bool purchaseOnly) { - if (!GameMain.GameSession.Campaign.CanAfford(selectedSubmarine.Price)) + int price = selectedSubmarine.GetPrice(); + + if (!GameMain.GameSession.Campaign.CanAfford(price)) { new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", ("[currencyname]", currencyName), @@ -810,7 +764,7 @@ namespace Barotrauma { var text = TextManager.GetWithVariables("purchaseandswitchsubmarinetext", ("[submarinename1]", selectedSubmarine.DisplayName), - ("[amount]", selectedSubmarine.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", currencyName), ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)); text += GetItemTransferText(); @@ -854,7 +808,7 @@ namespace Barotrauma if (GameMain.Client == null) { GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, 0); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch); RefreshSubmarineDisplay(true); } else @@ -868,7 +822,7 @@ namespace Barotrauma { msgBox = new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("purchasesubmarinetext", ("[submarinename]", selectedSubmarine.DisplayName), - ("[amount]", selectedSubmarine.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", currencyName)) + '\n' + TextManager.Get("submarineswitchinstruction"), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 75219550f..6b8ccfbb8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -836,7 +836,7 @@ namespace Barotrauma Identifier eventIdentifier = new Identifier($"{nameof(CreateWalletCrewFrame)}.{character.ID}"); campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => { - if (!(e.Owner is Some { Value: var owner }) || owner != character) { return; } + if (!e.Owner.TryUnwrap(out var owner) || owner != character) { return; } SetWalletText(walletBlock, e.Wallet, icon, largeIcon); }); registeredEvents.Add(eventIdentifier); @@ -1502,27 +1502,9 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(10) }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Name, font: GUIStyle.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont); - - var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), - TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), Level.Loaded.LevelData.Biome.DisplayName, textAlignment: Alignment.CenterRight); - var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), - TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)Level.Loaded.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionFrameContent.RectTransform) { AbsoluteOffset = new Point(0, locationInfoContainer.Rect.Height + padding) }, style: "HorizontalLine") - { - CanBeFocused = false - }; - - int locationInfoYOffset = locationInfoContainer.Rect.Height + padding * 2; - Sprite portrait = location.Type.GetPortrait(location.PortraitId); bool hasPortrait = portrait != null && portrait.SourceRect.Width > 0 && portrait.SourceRect.Height > 0; int contentWidth = missionFrameContent.Rect.Width; - if (hasPortrait) { float portraitAspectRatio = portrait.SourceRect.Width / portrait.SourceRect.Height; @@ -1534,6 +1516,30 @@ namespace Barotrauma portraitImage.RectTransform.NonScaledSize = new Point(Math.Min((int)(portraitImage.Rect.Size.Y * portraitAspectRatio), portraitImage.Rect.Width), portraitImage.Rect.Size.Y); } + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Name, font: GUIStyle.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont); + + if (location.Faction?.Prefab != null) + { + var factionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), + TextManager.Get("Faction"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), factionLabel.RectTransform), location.Faction.Prefab.Name, textAlignment: Alignment.CenterRight); + } + var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), + TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), Level.Loaded.LevelData.Biome.DisplayName, textAlignment: Alignment.CenterRight); + var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), + TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), TextManager.GetWithVariable("percentageformat", "[value]", ((int)Level.Loaded.LevelData.Difficulty).ToString()), textAlignment: Alignment.CenterRight); + + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionFrameContent.RectTransform) { AbsoluteOffset = new Point(0, locationInfoContainer.Rect.Height + padding) }, style: "HorizontalLine") + { + CanBeFocused = false + }; + + int locationInfoYOffset = locationInfoContainer.Rect.Height + padding * 2; + + GUIListBox missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrameContent.Rect.Height - locationInfoYOffset), missionFrameContent.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); missionList.ContentBackground.Color = Color.Transparent; missionList.Spacing = GUI.IntScale(15); @@ -1545,6 +1551,7 @@ namespace Barotrauma foreach (Mission mission in GameMain.GameSession.Missions) { + if (!mission.Prefab.ShowInMenus) { continue; } GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(Vector2.One, missionList.Content.RectTransform), style: null); GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(iconSize + spacing, 0) }, false, childAnchor: Anchor.TopLeft) { @@ -1556,7 +1563,7 @@ namespace Barotrauma descriptionText += "\n\n" + missionMessage; } RichString rewardText = mission.GetMissionRewardText(Submarine.MainSub); - RichString reputationText = mission.GetReputationRewardText(mission.Locations[0]); + RichString reputationText = mission.GetReputationRewardText(); Func wrapMissionText(GUIFont font) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index 9b5318e30..6f440255e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -727,7 +727,7 @@ namespace Barotrauma talentStages.Add(GetTalentState(character, button.Identifier, selectedTalents)); } - TalentStages collectiveStage = talentStages.Any(static stage => stage is Locked) + TalentStages collectiveStage = talentStages.All(static stage => stage is Locked) ? Locked : Available; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 3a2e531c1..cff4b0a2f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using Barotrauma.Extensions; @@ -77,6 +78,8 @@ namespace Barotrauma private PlayerBalanceElement? playerBalanceElement; + private static ImmutableHashSet characterList = ImmutableHashSet.Empty; + /// /// While set to true any call to will cause the buy button to be disabled and to not update the prices. /// This is to prevent us from buying another upgrade before the server has given us the new prices and causing potential syncing issues. @@ -102,6 +105,7 @@ namespace Barotrauma public UpgradeStore(CampaignUI campaignUI, GUIComponent parent) { WaitForServerUpdate = false; + characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both); this.campaignUI = campaignUI; GUIFrame upgradeFrame = new GUIFrame(rectT(1, 1, parent, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) { @@ -130,6 +134,7 @@ namespace Barotrauma private void RefreshAll() { + characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both); switch (selectedUpgradeTab) { case UpgradeTab.Repairs: @@ -273,7 +278,7 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty) { UserData = "moreindicator" }; ItemInfoFrame.Children.ForEach(c => { c.CanBeFocused = false; c.Children.ForEach(c2 => c2.CanBeFocused = false); }); - GUIFrame paddedLayout = new GUIFrame(rectT(0.95f, GUI.IsFourByThree() ? 0.98f : 0.95f, parent, Anchor.Center), style: null); + GUIFrame paddedLayout = new GUIFrame(rectT(0.95f, 0.95f, parent, Anchor.Center), style: null); mainStoreLayout = new GUILayoutGroup(rectT(1, 0.9f, paddedLayout, Anchor.BottomLeft), isHorizontal: true) { RelativeSpacing = 0.01f }; topHeaderLayout = new GUILayoutGroup(rectT(1, 0.1f, paddedLayout, Anchor.TopLeft), isHorizontal: true); @@ -295,8 +300,8 @@ namespace Barotrauma new GUITextBlock(rectT(1.0f, 1, locationLayout), TextManager.Get("UpgradeUI.AllSubmarinesInfo"), font: GUIStyle.SmallFont, wrap: true); categoryButtonLayout = new GUILayoutGroup(rectT(0.4f, 0.3f, leftLayout), isHorizontal: true) { Stretch = true }; - GUIButton upgradeButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; - GUIButton repairButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; + GUIButton upgradeButton = new GUIButton(rectT(0.5f, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; + GUIButton repairButton = new GUIButton(rectT(0.5f, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; /* RIGHT HEADER LAYOUT * |---------------------------------------------------------------------------------------------------| @@ -347,12 +352,15 @@ namespace Barotrauma SelectTab(UpgradeTab.Upgrade); - var itemSwapPreview = new GUICustomComponent(new RectTransform(new Vector2(0.27f, 0.4f), mainStoreLayout.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(GUI.IsFourByThree() ? 0.5f : 0.47f, 0.0f) }, DrawItemSwapPreview) + var itemSwapPreview = new GUICustomComponent(new RectTransform(new Vector2(0.25f, 0.4f), mainStoreLayout.RectTransform, Anchor.TopLeft) + { RelativeOffset = new Vector2(0.52f * GUI.AspectRatioAdjustment, 0.0f) }, DrawItemSwapPreview) { IgnoreLayoutGroups = true, CanBeFocused = true }; + GUITextBlock.AutoScaleAndNormalize(upgradeButton.TextBlock, repairButton.TextBlock); + #if DEBUG // creates a button that re-creates the UI CreateRefreshButton(); @@ -725,7 +733,7 @@ namespace Barotrauma if (storeLayout == null || mainStoreLayout == null) { return; } currentStoreLayout = CreateUpgradeCategoryList(rectT(1.0f, 1.5f, storeLayout)); - selectedUpgradeCategoryLayout = new GUIFrame(rectT(GUI.IsFourByThree() ? 0.3f : 0.25f, 1, mainStoreLayout), style: null) { CanBeFocused = false }; + selectedUpgradeCategoryLayout = new GUIFrame(rectT(0.3f * GUI.AspectRatioAdjustment, 1, mainStoreLayout), style: null) { CanBeFocused = false }; RefreshUpgradeList(); @@ -956,7 +964,7 @@ namespace Barotrauma bool isUninstallPending = item.Prefab.SwappableItem != null && item.PendingItemSwap?.Identifier == item.Prefab.SwappableItem.ReplacementOnUninstall; if (isUninstallPending) { canUninstall = false; } - frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), currentOrPending.UpgradePreviewSprite, + frames.Add(CreateUpgradeEntry(rectT(1f, 0.35f, parent.Content), currentOrPending.UpgradePreviewSprite, item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : TextManager.GetWithVariable("upgrades.installeditem", "[itemname]", nameWithQuantity), currentOrPending.Description, 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton").Frame); @@ -996,7 +1004,7 @@ namespace Barotrauma int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count(); - frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, + frames.Add(CreateUpgradeEntry(rectT(1f, 0.35f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, price, replacement, addBuyButton: true, addProgressBar: false, @@ -1102,7 +1110,7 @@ namespace Barotrauma public static UpgradeFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) { - int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList); return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category)); } @@ -1129,7 +1137,8 @@ namespace Barotrauma GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; GUILayoutGroup textLayout = new GUILayoutGroup(rectT(1f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); - var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + var name = new GUITextBlock(rectT(1, 0.35f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + //name.RectTransform.MinSize = new Point(0, (int)name.TextSize.Y); GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; GUILayoutGroup? progressLayout = null; @@ -1171,7 +1180,7 @@ namespace Barotrauma materialCostList.Visible = false; materialCostList.UserData = UpgradeStoreUserData.MaterialCostList; - var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Right) + var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice, textAlignment: Alignment.CenterRight) { UserData = UpgradeStoreUserData.PriceLabel, //prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades @@ -1258,7 +1267,7 @@ namespace Barotrauma { LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", ("[upgradename]", prefab.Name), - ("[amount]", prefab.Price.GetBuyPrice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString())); + ("[amount]", prefab.Price.GetBuyPrice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation, characterList).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -1673,7 +1682,7 @@ namespace Barotrauma GUITextBlock priceLabel = (GUITextBlock)buttonParent.FindChild(UpgradeStoreUserData.PriceLabel, recursive: true); priceLabel.Visible = true; - int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList); if (!WaitForServerUpdate) { @@ -1713,7 +1722,7 @@ namespace Barotrauma static void CreateMaterialCosts(GUIListBox list, UpgradePrefab prefab, int targetLevel) { list.Content.ClearChildren(); - List allItems = Character.Controlled?.Inventory?.FindAllItems(recursive: true) ?? new List(); + var allItems = CargoManager.FindAllItemsOnPlayerAndSub(Character.Controlled); var resources = prefab.GetApplicableResources(targetLevel); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index 6afb4c50f..3a217bf1e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -163,6 +163,7 @@ namespace Barotrauma private void SetSubmarineVotingText(Client starter, SubmarineInfo info, bool transferItems, VoteType type) { + int price = info.GetPrice(); string name = starter.Name; JobPrefab prefab = starter?.Character?.Info?.Job?.Prefab; Color nameColor = prefab != null ? prefab.UIColor : Color.White; @@ -177,35 +178,21 @@ namespace Barotrauma text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString), - ("[amount]", info.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.PurchaseSub: text = TextManager.GetWithVariables("submarinepurchasevote", ("[playername]", characterRichString), ("[submarinename]", submarineRichString), - ("[amount]", info.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.SwitchSub: - int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - if (deliveryFee > 0) - { - tag = transferItems ? "submarineswitchwithitemsfeevote" : "submarineswitchfeevote"; - text = TextManager.GetWithVariables(tag, - ("[playername]", characterRichString), - ("[submarinename]", submarineRichString), - ("[locationname]", endLocation.Name), - ("[amount]", deliveryFee.ToString()), - ("[currencyname]", TextManager.Get("credit").ToLower())); - } - else - { - tag = transferItems ? "submarineswitchwithitemsnofeevote" : "submarineswitchnofeevote"; - text = TextManager.GetWithVariables(tag, - ("[playername]", characterRichString), - ("[submarinename]", submarineRichString)); - } + tag = transferItems ? "submarineswitchwithitemsnofeevote" : "submarineswitchnofeevote"; + text = TextManager.GetWithVariables(tag, + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString)); break; } votingOnText = RichString.Rich(text); @@ -218,6 +205,7 @@ namespace Barotrauma private LocalizedString GetSubmarineVoteResultMessage(SubmarineInfo info, VoteType type, int yesVoteCount, int noVoteCount, bool votePassed) { + int price = info.GetPrice(); LocalizedString result = string.Empty; switch (type) @@ -225,7 +213,7 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: result = TextManager.GetWithVariables(votePassed ? "submarinepurchaseandswitchvotepassed" : "submarinepurchaseandswitchvotefailed", ("[submarinename]", info.DisplayName), - ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", info.Price)), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", price)), ("[currencyname]", TextManager.Get("credit").ToLower()), ("[yesvotecount]", yesVoteCount.ToString()), ("[novotecount]" , noVoteCount.ToString())); @@ -233,31 +221,16 @@ namespace Barotrauma case VoteType.PurchaseSub: result = TextManager.GetWithVariables(votePassed ? "submarinepurchasevotepassed" : "submarinepurchasevotefailed", ("[submarinename]", info.DisplayName), - ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", info.Price)), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", price)), ("[currencyname]", TextManager.Get("credit").ToLower()), ("[yesvotecount]", yesVoteCount.ToString()), ("[novotecount]", noVoteCount.ToString())); break; case VoteType.SwitchSub: - int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - - if (deliveryFee > 0) - { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchfeevotepassed" : "submarineswitchfeevotefailed", - ("[submarinename]", info.DisplayName), - ("[locationname]", endLocation.Name), - ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", deliveryFee)), - ("[currencyname]", TextManager.Get("credit").ToLower()), - ("[yesvotecount]", yesVoteCount.ToString()), - ("[novotecount]", noVoteCount.ToString())); - } - else - { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", - ("[submarinename]", info.DisplayName), - ("[yesvotecount]", yesVoteCount.ToString()), - ("[novotecount]", noVoteCount.ToString())); - } + result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", + ("[submarinename]", info.DisplayName), + ("[yesvotecount]", yesVoteCount.ToString()), + ("[novotecount]", noVoteCount.ToString())); break; default: break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index a730310d6..0df30ed06 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -232,9 +232,8 @@ namespace Barotrauma } GameSettings.Init(); + CreatureMetrics.Init(); - Md5Hash.Cache.Load(); - ConsoleArguments = args; try @@ -747,7 +746,7 @@ namespace Barotrauma } else if (HasLoaded) { - if (ConnectCommand is Some { Value: var connectCommand }) + if (ConnectCommand.TryUnwrap(out var connectCommand)) { if (Client != null) { @@ -1069,21 +1068,23 @@ namespace Barotrauma public static void QuitToMainMenu(bool save) { + CreatureMetrics.Save(); if (save) { GUI.SetSavingIndicatorState(true); - if (GameSession.Submarine != null && !GameSession.Submarine.Removed) { GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine); } - - // Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called) - if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost) + if (GameSession.Campaign is CampaignMode campaign) { - spCampaign.UpdateStoreStock(); + if (campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost) + { + spCampaign.UpdateStoreStock(); + } + GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true); + campaign.End(); } - SaveUtil.SaveGame(GameSession.SavePath); } @@ -1174,6 +1175,7 @@ namespace Barotrauma protected override void OnExiting(object sender, EventArgs args) { exiting = true; + CreatureMetrics.Save(); DebugConsole.NewMessage("Exiting..."); Client?.Quit(); SteamManager.ShutDown(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 7a09abd5e..eddd741f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Steam; namespace Barotrauma { @@ -194,7 +193,7 @@ namespace Barotrauma }; } - var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); + var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); @@ -377,6 +376,7 @@ namespace Barotrauma - (0.1f * iconRelativeWidth) // Spacing - (7 * layoutGroup.RelativeSpacing); + nameRelativeWidth = Math.Max(nameRelativeWidth, 0.25f); var font = layoutGroup.Rect.Width < 150 ? GUIStyle.SmallFont : GUIStyle.Font; var nameBlock = new GUITextBlock( @@ -1403,8 +1403,7 @@ namespace Barotrauma bool hitDeselect = PlayerInput.KeyHit(InputType.Deselect) && (!PlayerInput.SecondaryMouseButtonClicked() || (!isMouseOnOptionNode && !isMouseOnShortcutNode)); - bool isBoundToPrimaryMouse = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Command].MouseButton is MouseButton mouseButton && - (mouseButton == MouseButton.PrimaryMouse || mouseButton == (PlayerInput.MouseButtonsSwapped() ? MouseButton.RightMouse : MouseButton.LeftMouse)); + bool isBoundToPrimaryMouse = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Command].MouseButton == MouseButton.PrimaryMouse; bool canToggleInterface = !isBoundToPrimaryMouse || (!isMouseOnOptionNode && !isMouseOnShortcutNode && extraOptionNodes.None(n => GUI.IsMouseOn(n)) && !GUI.IsMouseOn(returnNode)); @@ -2796,8 +2795,8 @@ namespace Barotrauma var orderName = GetOrderNameBasedOnContextuality(order); var icon = CreateNodeIcon(Vector2.One, node.RectTransform, order.SymbolSprite, order.Color, tooltip: !showAssignmentTooltip ? orderName : orderName + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); + "\n" + PlayerInput.PrimaryMouseLabel + ": " + TextManager.Get("commandui.quickassigntooltip") + + "\n" + PlayerInput.SecondaryMouseLabel + ": " + TextManager.Get("commandui.manualassigntooltip")); if (disableNode) { @@ -2999,8 +2998,8 @@ namespace Barotrauma var showAssignmentTooltip = characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; icon = CreateNodeIcon(Vector2.One, node.RectTransform, sprite, order.Color, tooltip: characterContext != null ? optionName : optionName + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); + "\n" + PlayerInput.PrimaryMouseLabel + ": " + TextManager.Get("commandui.quickassigntooltip") + + "\n" + PlayerInput.SecondaryMouseLabel + ": " + TextManager.Get("commandui.manualassigntooltip")); } if (!CanCharacterBeHeard()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs index 8e0430760..3e60238ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using System.Collections.Immutable; using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { @@ -11,21 +9,22 @@ namespace Barotrauma { private const int MaxDrawnElements = 12; - public void DebugDraw(SpriteBatch spriteBatch, Vector2 pos, int debugDrawMetadataOffset, string[] ignoredMetadataInfo) + public void DebugDraw(SpriteBatch spriteBatch, Vector2 pos, CampaignMode campaign, GUI.DebugDrawMetaData debugDrawMetaData) { var campaignData = data; - foreach (string ignored in ignoredMetadataInfo) + if (!debugDrawMetaData.FactionMetadata) { removeData("reputation.faction"); } + if (!debugDrawMetaData.UpgradeLevels) { removeData("upgrade."); } + if (!debugDrawMetaData.UpgradePrices) { removeData("upgradeprice."); } + + void removeData(string keyStartsWith) { - if (!string.IsNullOrWhiteSpace(ignored)) - { - campaignData = campaignData.Where(pair => !pair.Key.StartsWith(ignored)).ToDictionary(i => i.Key, i => i.Value); - } + campaignData = campaignData.Where(pair => !pair.Key.StartsWith(keyStartsWith)).ToDictionary(i => i.Key, i => i.Value); } int offset = 0;; if (campaignData.Count > 0) { - offset = debugDrawMetadataOffset % campaignData.Count; + offset = debugDrawMetaData.Offset % campaignData.Count; if (offset < 0) { offset += campaignData.Count; } } @@ -72,7 +71,7 @@ namespace Barotrauma } float y = infoRect.Bottom + 16; - if (Campaign.Factions != null) + if (campaign.Factions != null) { const string factionHeader = "Reputations"; Vector2 factionHeaderSize = GUIStyle.SubHeadingFont.MeasureString(factionHeader); @@ -81,7 +80,7 @@ namespace Barotrauma GUI.DrawString(spriteBatch, factionPos, factionHeader, Color.White, font: GUIStyle.SubHeadingFont); y += factionHeaderSize.Y + 8; - foreach (Faction faction in Campaign.Factions) + foreach (Faction faction in campaign.Factions) { LocalizedString name = faction.Prefab.Name; Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); @@ -94,20 +93,6 @@ namespace Barotrauma y += 15; } } - - Location location = Campaign.Map?.CurrentLocation; - if (location?.Reputation != null) - { - string name = Campaign.Map?.CurrentLocation.Name; - Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 264, y), name, Color.White, font: GUIStyle.SmallFont); - y += nameSize.Y + 5; - - float normalizedReputation = MathUtils.InverseLerp(location.Reputation.MinReputation, location.Reputation.MaxReputation, location.Reputation.Value); - Color color = ToolBox.GradientLerp(normalizedReputation, Color.Red, Color.Yellow, Color.LightGreen); - GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, (int)(normalizedReputation * 255), 10), color, isFilled: true); - GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, 256, 10), Color.White); - } } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs index 4a91c9026..d004cd9bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs @@ -13,7 +13,7 @@ namespace Barotrauma partial void SettingsChanged(Option balanceChanged, Option rewardChanged) { - if (Owner is Some { Value: var character }) + if (Owner.TryUnwrap(out var character)) { if (!character.IsPlayer) { return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 3aae45f01..d893dc526 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -1,5 +1,4 @@ using Barotrauma.Extensions; -using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -16,8 +15,6 @@ namespace Barotrauma protected bool crewDead; protected Color overlayColor; - protected LocalizedString overlayText, overlayTextBottom; - protected Color overlayTextColor; protected Sprite overlaySprite; private TransitionType prevCampaignUIAutoOpenType; @@ -30,7 +27,13 @@ namespace Barotrauma protected GUIFrame campaignUIContainer; public CampaignUI CampaignUI; - public static CancellationTokenSource StartRoundCancellationToken { get; private set; } + public SlideshowPlayer SlideshowPlayer + { + get; + protected set; + } + + private CancellationTokenSource startRoundCancellationToken; public bool ForceMapUI { @@ -59,10 +62,19 @@ namespace Barotrauma { chatBox.ToggleOpen = wasChatBoxOpen; } - if (!value && CampaignUI?.SelectedTab == InteractionType.PurchaseSub) + if (!value) { - SubmarinePreview.Close(); + switch (CampaignUI?.SelectedTab) + { + case InteractionType.PurchaseSub: + SubmarinePreview.Close(); + break; + case InteractionType.MedicalClinic: + CampaignUI.MedicalClinic?.OnDeselected(); + break; + } } + showCampaignUI = value; } } @@ -77,6 +89,7 @@ namespace Barotrauma { foreach (Mission mission in Missions.ToList()) { + if (!mission.Prefab.ShowStartMessage) { continue; } new GUIMessageBox( RichString.Rich(mission.Prefab.IsSideObjective ? TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), mission.Name) : mission.Name), RichString.Rich(mission.Description), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) @@ -108,6 +121,23 @@ namespace Barotrauma { return AllowedToManageCampaign(ClientPermissions.ManageMoney); } + protected GUIButton CreateEndRoundButton() + { + int buttonWidth = (int)(450 * GUI.xScale * (GUI.IsUltrawide ? 3.0f : 1.0f)); + int buttonHeight = (int)(40 * GUI.yScale); + var rectT = HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y, buttonWidth, buttonHeight), GUI.Canvas); + rectT.Pivot = Pivot.Center; + return new GUIButton(rectT, TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") + { + Pulse = true, + TextBlock = + { + Shadow = true, + AutoScaleHorizontal = true + } + }; + } + public override void Draw(SpriteBatch spriteBatch) { @@ -123,32 +153,10 @@ namespace Barotrauma { GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), overlayColor, isFilled: true); } - if (!overlayText.IsNullOrEmpty() && overlayTextColor.A > 0) - { - var backgroundSprite = GUIStyle.GetComponentStyle("CommandBackground").GetDefaultSprite(); - Vector2 centerPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2; - LocalizedString wrappedText = ToolBox.WrapText(overlayText, GameMain.GraphicsWidth / 3, GUIStyle.Font); - Vector2 textSize = GUIStyle.Font.MeasureString(wrappedText); - Vector2 textPos = centerPos - textSize / 2; - backgroundSprite.Draw(spriteBatch, - centerPos, - Color.White * (overlayTextColor.A / 255.0f), - origin: backgroundSprite.size / 2, - rotate: 0.0f, - scale: new Vector2(GameMain.GraphicsWidth / 2 / backgroundSprite.size.X, textSize.Y / backgroundSprite.size.Y * 1.5f)); - - GUI.DrawString(spriteBatch, textPos + Vector2.One, wrappedText, Color.Black * (overlayTextColor.A / 255.0f)); - GUI.DrawString(spriteBatch, textPos, wrappedText, overlayTextColor); - - if (!overlayTextBottom.IsNullOrEmpty()) - { - Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUIStyle.Font.MeasureString(overlayTextBottom) / 2; - GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, overlayTextBottom.Value, Color.Black * (overlayTextColor.A / 255.0f)); - GUI.DrawString(spriteBatch, bottomTextPos, overlayTextBottom.Value, overlayTextColor); - } - } } + SlideshowPlayer?.DrawManually(spriteBatch); + if (GUI.DisableHUD || GUI.DisableUpperHUD || ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition")) { endRoundButton.Visible = false; @@ -157,7 +165,7 @@ namespace Barotrauma } if (Submarine.MainSub == null || Level.Loaded == null) { return; } - endRoundButton.Visible = false; + bool allowEndingRound = false; var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub); LocalizedString buttonText = ""; switch (availableTransition) @@ -168,12 +176,12 @@ namespace Barotrauma { string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation"; buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; } break; case TransitionType.LeaveLocation: buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; break; case TransitionType.ReturnToPreviousLocation: case TransitionType.ReturnToPreviousEmptyLocation: @@ -181,31 +189,27 @@ namespace Barotrauma { string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation"; buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; } - break; case TransitionType.None: default: if (Level.Loaded.Type == LevelData.LevelType.Outpost && + !Level.Loaded.IsEndBiome && (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) { buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); - endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; - } - else - { - endRoundButton.Visible = false; + allowEndingRound = !ForceMapUI && !ShowCampaignUI; } break; } if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted()) { - endRoundButton.Visible = false; + allowEndingRound = false; } + if (ReadyCheckButton != null) { ReadyCheckButton.Visible = allowEndingRound; } - if (ReadyCheckButton != null) { ReadyCheckButton.Visible = endRoundButton.Visible; } - + endRoundButton.Visible = allowEndingRound && Character.Controlled is { IsIncapacitated: false }; if (endRoundButton.Visible) { if (!AllowedToManageCampaign(ClientPermissions.ManageMap)) @@ -259,11 +263,11 @@ namespace Barotrauma GUI.ClearCursorWait(); - StartRoundCancellationToken = new CancellationTokenSource(); + startRoundCancellationToken = new CancellationTokenSource(); var loadTask = Task.Run(async () => { await Task.Yield(); - Rand.ThreadId = Thread.CurrentThread.ManagedThreadId; + Rand.ThreadId = Environment.CurrentManagedThreadId; try { GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); @@ -273,7 +277,8 @@ namespace Barotrauma roundSummaryScreen.LoadException = e; } Rand.ThreadId = 0; - }, StartRoundCancellationToken.Token); + startRoundCancellationToken = null; + }, startRoundCancellationToken.Token); TaskPool.Add("AsyncCampaignStartRound", loadTask, (t) => { overlayColor = Color.Transparent; @@ -283,6 +288,21 @@ namespace Barotrauma return loadTask; } + public void CancelStartRound() + { + startRoundCancellationToken?.Cancel(); + } + + public void ThrowIfStartRoundCancellationRequested() + { + if (startRoundCancellationToken != null && + startRoundCancellationToken.Token.IsCancellationRequested) + { + startRoundCancellationToken.Token.ThrowIfCancellationRequested(); + startRoundCancellationToken = null; + } + } + protected SubmarineInfo GetPredefinedStartOutpost() { if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters && !parameters.OutpostFilePath.IsNullOrEmpty()) @@ -316,10 +336,15 @@ namespace Barotrauma goto default; default: ShowCampaignUI = true; - CampaignUI.SelectTab(npc.CampaignInteractionType, storeIdentifier: npc.MerchantIdentifier); + CampaignUI.SelectTab(npc.CampaignInteractionType, npc); CampaignUI.UpgradeStore?.RequestRefresh(); break; } + + if (npc.AIController is HumanAIController humanAi && humanAi.IsInHostileFaction()) + { + npc.Speak(TextManager.Get("dialoglowrepcampaigninteraction").Value, identifier: "dialoglowrepcampaigninteraction".ToIdentifier(), minDurationBetweenSimilar: 60.0f); + } } public override void AddToGUIUpdateList() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 470e9be68..c7fb14619 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -125,38 +125,25 @@ namespace Barotrauma private void CreateButtons() { - int buttonHeight = (int) (GUI.Scale * 40), - buttonWidth = GUI.IntScale(450), - buttonCenter = buttonHeight / 2, - screenMiddle = GameMain.GraphicsWidth / 2; - - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle - buttonWidth / 2, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, buttonWidth, buttonHeight), GUI.Canvas), - TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") + endRoundButton = CreateEndRoundButton(); + endRoundButton.OnClicked = (btn, userdata) => { - Pulse = true, - TextBlock = - { - Shadow = true, - AutoScaleHorizontal = true - }, - OnClicked = (btn, userdata) => - { - TryEndRoundWithFuelCheck( - onConfirm: () => GameMain.Client.RequestStartRound(), - onReturnToMapScreen: () => - { - ShowCampaignUI = true; - if (CampaignUI == null) { InitCampaignUI(); } - CampaignUI.SelectTab(InteractionType.Map); - }); - return true; - } + TryEndRoundWithFuelCheck( + onConfirm: () => GameMain.Client.RequestStartRound(), + onReturnToMapScreen: () => + { + ShowCampaignUI = true; + if (CampaignUI == null) { InitCampaignUI(); } + CampaignUI.SelectTab(InteractionType.Map); + }); + return true; }; - int readyButtonHeight = buttonHeight; - int readyButtonWidth = (int) (GUI.Scale * 50); - - ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (buttonWidth / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas), + int readyButtonWidth = (int)(GUI.Scale * 50 * (GUI.IsUltrawide ? 3.0f : 1.0f)); + int readyButtonHeight = (int)(GUI.Scale * 40); + int readyButtonCenter = readyButtonHeight / 2, + screenMiddle = GameMain.GraphicsWidth / 2; + ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (endRoundButton.Rect.Width / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - readyButtonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas), style: "RepairBuyButton") { ToolTip = TextManager.Get("ReadyCheck.Tooltip"), @@ -216,51 +203,35 @@ namespace Barotrauma } Character prevControlled = Character.Controlled; - if (prevControlled?.AIController != null) - { - prevControlled.AIController.Enabled = false; - } GUI.DisableHUD = true; if (IsFirstRound) { - Character.Controlled = null; + if (SlideshowPrefab.Prefabs.TryGet("campaignstart".ToIdentifier(), out var slideshow)) + { + SlideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow); + } + Character.Controlled = null; prevControlled?.ClearInputs(); overlayColor = Color.LightGray; overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); - overlayTextColor = Color.Transparent; - overlayText = TextManager.GetWithVariables("campaignstart", - ("xxxx", Map.CurrentLocation.Name), ("yyyy", TextManager.Get($"submarineclass.{Submarine.MainSub.Info.SubmarineClass}"))); - float fadeInDuration = 1.0f; - float textDuration = 10.0f; - float timer = 0.0f; - while (timer < textDuration) - { - if (GameMain.GameSession == null || Screen.Selected != GameMain.GameScreen) - { - GUI.DisableHUD = false; - yield return CoroutineStatus.Success; - } - // Try to grab the controlled here to prevent inputs, assigned late on multiplayer - if (Character.Controlled != null) - { - prevControlled = Character.Controlled; - Character.Controlled = null; - prevControlled.ClearInputs(); - } - GameMain.GameScreen.Cam.Freeze = true; - overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); - timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); - yield return CoroutineStatus.Running; - } + var outpost = GameMain.GameSession.Level.StartOutpost; var borders = outpost.GetDockedBorders(); borders.Location += outpost.WorldPosition.ToPoint(); GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2); float startZoom = 0.8f / ((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X); - GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + GameMain.GameScreen.Cam.Zoom = GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + while (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown) + { + GUI.PreventPauseMenuToggle = true; + yield return CoroutineStatus.Running; + } + GUI.PreventPauseMenuToggle = false; + prevControlled ??= Character.Controlled; + GameMain.LightManager.LosAlpha = 0.0f; var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, null, null, fadeOut: false, @@ -272,16 +243,6 @@ namespace Barotrauma AllowInterrupt = true, RemoveControlFromCharacter = false }; - fadeInDuration = 1.0f; - timer = 0.0f; - overlayTextColor = Color.Transparent; - overlayText = ""; - while (timer < fadeInDuration) - { - overlayColor = Color.Lerp(Color.LightGray, Color.Transparent, timer / fadeInDuration); - timer += CoroutineManager.DeltaTime; - yield return CoroutineStatus.Running; - } overlayColor = Color.Transparent; while (transition.Running) { @@ -385,7 +346,7 @@ namespace Barotrauma overlayColor = Color.Transparent; if (DateTime.Now > timeOut) { GameMain.NetLobbyScreen.Select(); } - if (!(Screen.Selected is RoundSummaryScreen)) + if (Screen.Selected is not RoundSummaryScreen) { if (continueButton != null) { @@ -409,6 +370,8 @@ namespace Barotrauma base.Update(deltaTime); + SlideshowPlayer?.UpdateManually(deltaTime); + if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { @@ -442,7 +405,8 @@ namespace Barotrauma CampaignUI.SelectTab(InteractionType.Map); } } - else + //end biome is handled by the server (automatic transition without a map screen when the end of the level is reached) + else if (!Level.Loaded.IsEndBiome) { //wasn't initially docked (sub doesn't have a docking port?) // -> choose a destination when the sub is far enough from the start outpost @@ -467,11 +431,17 @@ namespace Barotrauma } } + public override void UpdateWhilePaused(float deltaTime) + { + SlideshowPlayer?.UpdateManually(deltaTime); + } + public override void End(TransitionType transitionType = TransitionType.None) { base.End(transitionType); ForceMapUI = ShowCampaignUI = false; - + SlideshowPlayer?.Finish(); + // remove all event dialogue boxes GUIMessageBox.MessageBoxes.ForEachMod(mb => { @@ -501,7 +471,8 @@ namespace Barotrauma { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } - CoroutineManager.StartCoroutine(DoEndCampaignCameraTransition(), "DoEndCampaignCameraTransition"); + GameMain.CampaignEndScreen.Select(); + GUI.DisableHUD = false; GameMain.CampaignEndScreen.OnFinished = () => { GameMain.NetLobbyScreen.Select(); @@ -510,32 +481,6 @@ namespace Barotrauma }; } - private IEnumerable DoEndCampaignCameraTransition() - { - Character controlled = Character.Controlled; - if (controlled != null) - { - controlled.AIController.Enabled = false; - } - - GUI.DisableHUD = true; - ISpatialEntity endObject = Level.Loaded.LevelObjectManager.GetAllObjects().FirstOrDefault(obj => obj.Prefab.SpawnPos == LevelObjectPrefab.SpawnPosType.LevelEnd); - var transition = new CameraTransition(endObject ?? Submarine.MainSub, GameMain.GameScreen.Cam, - null, Alignment.Center, - fadeOut: true, - panDuration: 10, - startZoom: null, endZoom: 0.2f); - - while (transition.Running) - { - yield return CoroutineStatus.Running; - } - GameMain.CampaignEndScreen.Select(); - GUI.DisableHUD = false; - - yield return CoroutineStatus.Success; - } - public void ClientWrite(IWriteMessage msg) { System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue); @@ -844,8 +789,6 @@ namespace Barotrauma { DebugConsole.Log("Received campaign update (Reputation)"); UInt16 id = msg.ReadUInt16(); - float? reputation = null; - if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } Dictionary factionReps = new Dictionary(); byte factionsCount = msg.ReadByte(); for (int i = 0; i < factionsCount; i++) @@ -854,11 +797,6 @@ namespace Barotrauma } if (ShouldApply(NetFlags.Reputation, id, requireUpToDateSave: true)) { - if (reputation.HasValue) - { - campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); - campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); - } foreach (var (identifier, rep) in factionReps) { Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); @@ -871,6 +809,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\"."); } } + campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); } } if (requiredFlags.HasFlag(NetFlags.CharacterInfo)) @@ -995,7 +934,9 @@ namespace Barotrauma if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); } } - if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null) + if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null && + /*can't apply until we have the latest save file*/ + !NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID)) { CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires); if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters); } @@ -1010,25 +951,20 @@ namespace Barotrauma foreach (NetWalletTransaction transaction in update.Transactions) { WalletInfo info = transaction.Info; - switch (transaction.CharacterID) + if (transaction.CharacterID.TryUnwrap(out var charID)) { - case Some { Value: var charID }: - { - Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); - if (targetCharacter is null) { break; } - Wallet wallet = targetCharacter.Wallet; + Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); + if (targetCharacter is null) { break; } + Wallet wallet = targetCharacter.Wallet; - wallet.Balance = info.Balance; - wallet.RewardDistribution = info.RewardDistribution; - TryInvokeEvent(wallet, transaction.ChangedData, info); - break; - } - case None _: - { - Bank.Balance = info.Balance; - TryInvokeEvent(Bank, transaction.ChangedData, info); - break; - } + wallet.Balance = info.Balance; + wallet.RewardDistribution = info.RewardDistribution; + TryInvokeEvent(wallet, transaction.ChangedData, info); + } + else + { + Bank.Balance = info.Balance; + TryInvokeEvent(Bank, transaction.ChangedData, info); } } @@ -1043,7 +979,7 @@ namespace Barotrauma public override bool TryPurchase(Client client, int price) { - if (!AllowedToManageCampaign(ClientPermissions.ManageCampaign)) + if (!AllowedToManageCampaign(ClientPermissions.ManageMoney)) { return PersonalWallet.TryDeduct(price); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 722297b66..d2c611e10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -12,7 +12,13 @@ namespace Barotrauma public override bool Paused { - get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition") || ShowCampaignUI && CampaignUI.SelectedTab == InteractionType.Map; } + get + { + return + ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition") || + ShowCampaignUI && CampaignUI.SelectedTab == InteractionType.Map || + (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown); + } } public override void UpdateWhilePaused(float deltaTime) @@ -31,6 +37,8 @@ namespace Barotrauma } } + SlideshowPlayer?.UpdateManually(deltaTime); + CrewManager.ChatBox?.Update(deltaTime); CrewManager.UpdateReports(); } @@ -77,9 +85,9 @@ namespace Barotrauma /// private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign, settings) { - CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); Settings = settings; + InitFactions(); map = new Map(this, mapSeed); foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) { @@ -89,7 +97,6 @@ namespace Barotrauma CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant)); } } - InitCampaignData(); InitUI(); } @@ -99,6 +106,17 @@ namespace Barotrauma private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign, CampaignSettings.Empty) { IsFirstRound = false; + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "metadata": + CampaignMetadata.Load(subElement); + break; + } + } + + InitFactions(); foreach (var subElement in element.Elements()) { @@ -114,9 +132,6 @@ namespace Barotrauma case "map": map = Map.Load(this, subElement); break; - case "metadata": - CampaignMetadata = new CampaignMetadata(this, subElement); - break; case "cargo": CargoManager.LoadPurchasedItems(subElement); break; @@ -133,14 +148,14 @@ namespace Barotrauma case "stats": LoadStats(subElement); break; + case "eventmanager": + GameMain.GameSession.EventManager.Load(subElement); + break; } } - CampaignMetadata ??= new CampaignMetadata(this); UpgradeManager ??= new UpgradeManager(this); - InitCampaignData(); - InitUI(); //backwards compatibility for saves made prior to the addition of personal wallets @@ -198,28 +213,14 @@ namespace Barotrauma { StartRound = () => { TryEndRound(); } }; - } - private void CreateEndRoundButton() - { - int buttonHeight = (int)(GUI.Scale * 40); - int buttonWidth = GUI.IntScale(450); - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUI.Canvas), - TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") + endRoundButton = CreateEndRoundButton(); + endRoundButton.OnClicked = (btn, userdata) => { - Pulse = true, - TextBlock = - { - Shadow = true, - AutoScaleHorizontal = true - }, - OnClicked = (btn, userdata) => - { - TryEndRoundWithFuelCheck( - onConfirm: () => TryEndRound(), - onReturnToMapScreen: () => { ShowCampaignUI = true; CampaignUI.SelectTab(InteractionType.Map); }); - return true; - } + TryEndRoundWithFuelCheck( + onConfirm: () => TryEndRound(), + onReturnToMapScreen: () => { ShowCampaignUI = true; CampaignUI.SelectTab(InteractionType.Map); }); + return true; }; } @@ -265,7 +266,6 @@ namespace Barotrauma private IEnumerable DoLoadInitialLevel(LevelData level, bool mirror) { - GameMain.GameSession.StartRound(level, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); GameMain.GameScreen.Select(); @@ -296,34 +296,9 @@ namespace Barotrauma if (IsFirstRound || showCampaignResetText) { - overlayColor = Color.LightGray; - overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); - overlayTextColor = Color.Transparent; - overlayText = TextManager.GetWithVariables(showCampaignResetText ? "campaignend4" : "campaignstart", - ("xxxx", Map.CurrentLocation.Name), - ("yyyy", TextManager.Get("submarineclass." + Submarine.MainSub.Info.SubmarineClass))); - LocalizedString pressAnyKeyText = TextManager.Get("pressanykey"); - float fadeInDuration = 2.0f; - float textDuration = 10.0f; - float timer = 0.0f; - while (true) + if (SlideshowPrefab.Prefabs.TryGet("campaignstart".ToIdentifier(), out var slideshow)) { - if (timer > fadeInDuration) - { - overlayTextBottom = pressAnyKeyText; - if (PlayerInput.GetKeyboardState.GetPressedKeys().Length > 0 || PlayerInput.PrimaryMouseButtonClicked()) - { - break; - } - } - if (GameMain.GameSession == null) - { - GUI.DisableHUD = false; - yield return CoroutineStatus.Success; - } - overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); - timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); - yield return CoroutineStatus.Running; + SlideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow); } var outpost = GameMain.GameSession.Level.StartOutpost; var borders = outpost.GetDockedBorders(); @@ -331,7 +306,13 @@ namespace Barotrauma GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2); float startZoom = 0.8f / ((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X); - GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + GameMain.GameScreen.Cam.Zoom = GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + while (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown) + { + GUI.PreventPauseMenuToggle = true; + yield return CoroutineStatus.Running; + } + GUI.PreventPauseMenuToggle = false; var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, null, null, fadeOut: false, @@ -343,17 +324,6 @@ namespace Barotrauma AllowInterrupt = true, RemoveControlFromCharacter = false }; - fadeInDuration = 1.0f; - timer = 0.0f; - overlayTextColor = Color.Transparent; - overlayText = ""; - while (timer < fadeInDuration) - { - overlayColor = Color.Lerp(Color.LightGray, Color.Transparent, timer / fadeInDuration); - timer += CoroutineManager.DeltaTime; - yield return CoroutineStatus.Running; - } - overlayColor = Color.Transparent; while (transition.Running) { yield return CoroutineStatus.Running; @@ -440,61 +410,68 @@ namespace Barotrauma TotalPassedLevels++; break; case TransitionType.ProgressToNextEmptyLocation: + Map.Visit(Map.CurrentLocation); TotalPassedLevels++; break; + case TransitionType.End: + EndCampaign(); + IsFirstRound = true; + break; } - Map.ProgressWorld(transitionType, GameMain.GameSession.RoundDuration); - - var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, - transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, - fadeOut: false, - panDuration: EndTransitionDuration); + Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration); GUI.ClearMessages(); - Location portraitLocation = Map.SelectedLocation ?? Map.CurrentLocation; - overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); - float fadeOutDuration = endTransition.PanDuration; - float t = 0.0f; - while (t < fadeOutDuration || endTransition.Running) - { - t += CoroutineManager.DeltaTime; - overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); - yield return CoroutineStatus.Running; - } - overlayColor = Color.White; - yield return CoroutineStatus.Running; - //-------------------------------------- - - if (success) + if (transitionType != TransitionType.End) { - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - } - else - { - PendingSubmarineSwitch = null; - EnableRoundSummaryGameOverState(); - } + var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, + transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, + fadeOut: false, + panDuration: EndTransitionDuration); - CrewManager?.ClearCurrentOrders(); - - //-------------------------------------- - - SelectSummaryScreen(roundSummary, newLevel, mirror, () => - { - GameMain.GameScreen.Select(); - if (continueButton != null) + Location portraitLocation = Map.SelectedLocation ?? Map.CurrentLocation; + overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); + float fadeOutDuration = endTransition.PanDuration; + float t = 0.0f; + while (t < fadeOutDuration || endTransition.Running) { - continueButton.Visible = true; + t += CoroutineManager.DeltaTime; + overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); + yield return CoroutineStatus.Running; + } + overlayColor = Color.White; + yield return CoroutineStatus.Running; + + //-------------------------------------- + + if (success) + { + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + } + else + { + PendingSubmarineSwitch = null; + EnableRoundSummaryGameOverState(); } - GUI.DisableHUD = false; - GUI.ClearCursorWait(); - overlayColor = Color.Transparent; - }); + CrewManager?.ClearCurrentOrders(); + + SelectSummaryScreen(roundSummary, newLevel, mirror, () => + { + GameMain.GameScreen.Select(); + if (continueButton != null) + { + continueButton.Visible = true; + } + + GUI.DisableHUD = false; + GUI.ClearCursorWait(); + overlayColor = Color.Transparent; + }); + } GUI.SetSavingIndicatorState(false); yield return CoroutineStatus.Success; @@ -502,7 +479,10 @@ namespace Barotrauma protected override void EndCampaignProjSpecific() { - CoroutineManager.StartCoroutine(DoEndCampaignCameraTransition(), "DoEndCampaignCameraTransition"); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + GameMain.CampaignEndScreen.Select(); + GUI.DisableHUD = false; GameMain.CampaignEndScreen.OnFinished = () => { showCampaignResetText = true; @@ -511,39 +491,14 @@ namespace Barotrauma }; } - private IEnumerable DoEndCampaignCameraTransition() - { - if (Character.Controlled != null) - { - Character.Controlled.AIController.Enabled = false; - Character.Controlled = null; - } - GUI.DisableHUD = true; - ISpatialEntity endObject = Level.Loaded.LevelObjectManager.GetAllObjects().FirstOrDefault(obj => obj.Prefab.SpawnPos == LevelObjectPrefab.SpawnPosType.LevelEnd); - var transition = new CameraTransition(endObject ?? Submarine.MainSub, GameMain.GameScreen.Cam, - null, Alignment.Center, - fadeOut: true, - panDuration: 10, - startZoom: null, endZoom: 0.2f); - - while (transition.Running) - { - yield return CoroutineStatus.Running; - } - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - GameMain.CampaignEndScreen.Select(); - GUI.DisableHUD = false; - - yield return CoroutineStatus.Success; - } - public override void Update(float deltaTime) { if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition") || gameOver) { return; } base.Update(deltaTime); - + + SlideshowPlayer?.UpdateManually(deltaTime); + Map?.Radiation?.UpdateRadiation(deltaTime); if (PlayerInput.SecondaryMouseButtonClicked() || @@ -594,11 +549,19 @@ namespace Barotrauma CampaignUI.SelectTab(InteractionType.Map); } } + else if (Level.Loaded.IsEndBiome) + { + var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); + if (transitionType == TransitionType.ProgressToNextLocation) + { + LoadNewLevel(); + } + } else { //wasn't initially docked (sub doesn't have a docking port?) // -> choose a destination when the sub is far enough from the start outpost - if (!Submarine.MainSub.AtStartExit) + if (!Submarine.MainSub.AtStartExit && !Level.Loaded.StartOutpost.ExitPoints.Any()) { ForceMapUI = true; CampaignUI.SelectTab(InteractionType.Map); @@ -608,11 +571,11 @@ namespace Barotrauma else { var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); - if (transitionType == TransitionType.End) + if (Level.Loaded.IsEndBiome && transitionType == TransitionType.ProgressToNextLocation) { - EndCampaign(); + LoadNewLevel(); } - if (transitionType == TransitionType.ProgressToNextLocation && + else if (transitionType == TransitionType.ProgressToNextLocation && Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) { LoadNewLevel(); @@ -725,6 +688,11 @@ namespace Barotrauma modeElement.Add(Settings.Save()); modeElement.Add(SaveStats()); + if (GameMain.GameSession?.EventManager != null) + { + modeElement.Add(GameMain.GameSession?.EventManager.Save()); + } + //save and remove all items that are in someone's inventory so they don't get included in the sub file as well foreach (Character c in Character.CharacterList) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index fcb5438dc..49f2a6fcc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -585,7 +585,7 @@ namespace Barotrauma if (!gap.IsRoomToRoom) { if (!IsWearingDivingSuit()) { continue; } - if (Character.Controlled.IsProtectedFromPressure()) { continue; } + if (Character.Controlled.IsProtectedFromPressure) { continue; } if (DisplayHint("divingsuitwarning".ToIdentifier(), extendTextTag: false)) { return; } continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index 232e84838..3b4d31cf6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -11,6 +11,8 @@ namespace Barotrauma { internal sealed partial class MedicalClinic { + private MedicalClinicUI? ui => campaign?.CampaignUI?.MedicalClinic; + public enum RequestResult { Undecided, @@ -303,6 +305,12 @@ namespace Barotrauma } } + private void AfflictionUpdateReceived(IReadMessage inc) + { + NetCrewMember crewMember = INetSerializableStruct.Read(inc); + ui?.UpdateAfflictions(crewMember); + } + private void PendingRequestReceived(IReadMessage inc) { var pendingCrew = INetSerializableStruct.Read>(inc); @@ -312,6 +320,10 @@ namespace Barotrauma } } + public static void SendUnsubscribeRequest() => ClientSend(null, + header: NetworkHeader.UNSUBSCRIBE_ME, + deliveryMethod: DeliveryMethod.Reliable); + private static IWriteMessage StartSending() { IWriteMessage writeMessage = new WriteOnlyMessage(); @@ -337,6 +349,9 @@ namespace Barotrauma case NetworkHeader.REQUEST_AFFLICTIONS: AfflictionRequestReceived(inc); break; + case NetworkHeader.AFFLICTION_UPDATE: + AfflictionUpdateReceived(inc); + break; case NetworkHeader.REQUEST_PENDING: PendingRequestReceived(inc); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index 9da10c685..8ba436270 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -40,7 +40,7 @@ namespace Barotrauma private void CreateMessageBox(string author) { - Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.3f : 0.2f, 0.15f); + Vector2 relativeSize = new Vector2(0.2f / GUI.AspectRatioAdjustment, 0.15f); Point minSize = new Point(300, 200); msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index da2c6b35b..53bcf4f1f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -21,8 +21,7 @@ namespace Barotrauma private readonly GameMode gameMode; - private readonly float initialLocationReputation; - private readonly Dictionary initialFactionReputations = new Dictionary(); + private readonly Dictionary initialFactionReputations = new Dictionary(); public GUILayoutGroup ButtonArea { get; private set; } @@ -36,12 +35,11 @@ namespace Barotrauma this.selectedMissions = selectedMissions.ToList(); this.startLocation = startLocation; this.endLocation = endLocation; - initialLocationReputation = startLocation?.Reputation?.Value ?? 0.0f; if (gameMode is CampaignMode campaignMode) { foreach (Faction faction in campaignMode.Factions) { - initialFactionReputations.Add(faction, faction.Reputation.Value); + initialFactionReputations.Add(faction.Prefab.Identifier, faction.Reputation.Value); } } } @@ -214,11 +212,13 @@ namespace Barotrauma Stretch = true }; - List missionsToDisplay = new List(selectedMissions); - if (!selectedMissions.Any() && startLocation != null) + List missionsToDisplay = new List(selectedMissions.Where(m => m.Prefab.ShowInMenus)); + if (startLocation != null) { foreach (Mission mission in startLocation.SelectedMissions) { + if (missionsToDisplay.Contains(mission)) { continue; } + if (!mission.Prefab.ShowInMenus) { continue; } if (mission.Locations[0] == mission.Locations[1] || mission.Locations.Contains(campaignMode?.Map.SelectedLocation)) { @@ -312,18 +312,27 @@ namespace Barotrauma } var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(missionMessage), wrap: true); - int reward = displayedMission.GetReward(Submarine.MainSub); - if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && reward > 0) + if (selectedMissions.Contains(displayedMission) && displayedMission.Completed) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); - if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) + RichString reputationText = displayedMission.GetReputationRewardText(); + if (!reputationText.IsNullOrEmpty()) { - var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(reward)); - if (share > 0) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText, wrap: true); + } + + int totalReward = displayedMission.GetFinalReward(Submarine.MainSub); + if (totalReward > 0) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); + if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) { - string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); - RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(totalReward)); + if (share > 0) + { + string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); + RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + } } } } @@ -401,33 +410,17 @@ namespace Barotrauma }; reputationList.ContentBackground.Color = Color.Transparent; - if (startLocation.Type.HasOutpost && startLocation.Reputation != null) - { - var iconStyle = GUIStyle.GetComponentStyle("LocationReputationIcon"); - var locationFrame = CreateReputationElement( - reputationList.Content, - startLocation.Name, - startLocation.Reputation.Value, startLocation.Reputation.NormalizedValue, initialLocationReputation, - startLocation.Type.Name, "", - iconStyle?.GetDefaultSprite(), startLocation.Type.GetPortrait(0), iconStyle?.Color ?? Color.White); - CreatePathUnlockElement(locationFrame, null, startLocation); - } - foreach (Faction faction in campaignMode.Factions.OrderBy(f => f.Prefab.MenuOrder).ThenBy(f => f.Prefab.Name)) { float initialReputation = faction.Reputation.Value; - if (initialFactionReputations.ContainsKey(faction)) - { - initialReputation = initialFactionReputations[faction]; - } - else + if (!initialFactionReputations.TryGetValue(faction.Prefab.Identifier, out initialReputation)) { DebugConsole.AddWarning($"Could not determine reputation change for faction \"{faction.Prefab.Name}\" (faction was not present at the start of the round)."); } var factionFrame = CreateReputationElement( reputationList.Content, faction.Prefab.Name, - faction.Reputation.Value, faction.Reputation.NormalizedValue, initialReputation, + faction.Reputation, initialReputation, faction.Prefab.ShortDescription, faction.Prefab.Description, faction.Prefab.Icon, faction.Prefab.BackgroundPortrait, faction.Prefab.IconColor); CreatePathUnlockElement(factionFrame, faction, null); @@ -455,52 +448,60 @@ namespace Barotrauma void CreatePathUnlockElement(GUIComponent reputationFrame, Faction faction, Location location) { - if (GameMain.GameSession?.Campaign?.Map != null) + if (GameMain.GameSession?.Campaign?.Map == null) { return; } + + IEnumerable connectionsBetweenBiomes = + GameMain.GameSession.Campaign.Map.Connections.Where(c => c.Locations[0].Biome != c.Locations[1].Biome); + + foreach (LocationConnection connection in connectionsBetweenBiomes) { - foreach (LocationConnection connection in GameMain.GameSession.Campaign.Map.Connections) + if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; } + + //don't show the "reputation required to unlock" text if another connection between the biomes has already been unlocked + if (connectionsBetweenBiomes.Where(c => !c.Locked).Any(c => + (c.Locations[0].Biome == connection.Locations[0].Biome && c.Locations[1].Biome == connection.Locations[1].Biome) || + (c.Locations[1].Biome == connection.Locations[0].Biome && c.Locations[0].Biome == connection.Locations[1].Biome))) { - if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; } + continue; + } - var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; - var unlockEvent = - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == Identifier.Empty); + var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; + var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction); - if (unlockEvent == null) { continue; } - if (string.IsNullOrEmpty(unlockEvent.UnlockPathFaction) || unlockEvent.UnlockPathFaction.Equals("location", StringComparison.OrdinalIgnoreCase)) + if (unlockEvent == null) { continue; } + if (unlockEvent.Faction.IsEmpty) + { + if (location == null || gateLocation != location) { continue; } + } + else + { + if (faction == null || faction.Prefab.Identifier != unlockEvent.Faction) { continue; } + } + + if (unlockEvent != null) + { + Reputation unlockReputation = gateLocation.Reputation; + Faction unlockFaction = null; + if (!unlockEvent.Faction.IsEmpty) { - if (location == null || gateLocation != location) { continue; } + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction); + unlockReputation = unlockFaction?.Reputation; } - else + float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); + RichString unlockText = RichString.Rich(TextManager.GetWithVariables( + "lockedpathreputationrequirement", + ("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)), + ("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖"))); + var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, + unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); + unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); + unlockInfoPanel.UserData = "unlockinfo"; + if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) { - if (faction == null || faction.Prefab.Identifier != unlockEvent.UnlockPathFaction) { continue; } - } - - if (unlockEvent != null) - { - Reputation unlockReputation = gateLocation.Reputation; - Faction unlockFaction = null; - if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) - { - unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.UnlockPathFaction); - unlockReputation = unlockFaction?.Reputation; - } - float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); - RichString unlockText = RichString.Rich(TextManager.GetWithVariables( - "lockedpathreputationrequirement", - ("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)), - ("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖"))); - var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, - unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); - unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); - unlockInfoPanel.UserData = "unlockinfo"; - if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) - { - unlockInfoPanel.Font = GUIStyle.SmallFont; - } + unlockInfoPanel.Font = GUIStyle.SmallFont; } } - } + } } } @@ -543,6 +544,11 @@ namespace Barotrauma } } + if (startLocation?.Biome != null && startLocation.Biome.IsEndBiome) + { + locationName ??= startLocation.Name; + } + if (textTag == null) { return ""; } if (locationName == null) @@ -680,7 +686,7 @@ namespace Barotrauma } private GUIFrame CreateReputationElement(GUIComponent parent, - LocalizedString name, float reputation, float normalizedReputation, float initialReputation, + LocalizedString name, Reputation reputation, float initialReputation, LocalizedString shortDescription, LocalizedString fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) { var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), style: null); @@ -698,21 +704,22 @@ namespace Barotrauma }; } - var factionInfoHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), factionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft, isHorizontal: true) + var factionInfoHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), factionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterRight, isHorizontal: true) { AbsoluteSpacing = GUI.IntScale(5), Stretch = true }; + var factionIcon = new GUIImage(new RectTransform(Vector2.One * 0.7f, factionInfoHorizontal.RectTransform, scaleBasis: ScaleBasis.Smallest), icon, scaleToFit: true) + { + Color = iconColor + }; var factionTextContent = new GUILayoutGroup(new RectTransform(Vector2.One, factionInfoHorizontal.RectTransform)) { AbsoluteSpacing = GUI.IntScale(10), Stretch = true }; - var factionIcon = new GUIImage(new RectTransform(Vector2.One * 0.7f, factionInfoHorizontal.RectTransform, scaleBasis: ScaleBasis.Smallest), icon, scaleToFit: true) - { - Color = iconColor - }; + factionInfoHorizontal.Recalculate(); var header = new GUITextBlock(new RectTransform(new Point(factionTextContent.Rect.Width, GUI.IntScale(40)), factionTextContent.RectTransform), @@ -733,24 +740,30 @@ namespace Barotrauma factionTextContent.Recalculate(); new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), - onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, normalizedReputation)); + onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, reputation.NormalizedValue)); + + var reputationText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), + string.Empty, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); + SetReputationText(reputationText); + reputation?.OnReputationValueChanged.RegisterOverwriteExisting("RefreshRoundSummary".ToIdentifier(), _ => + { + SetReputationText(reputationText); + }); - LocalizedString reputationText = Reputation.GetFormattedReputationText(normalizedReputation, reputation, addColorTags: true); - int reputationChange = (int)Math.Round(reputation - initialReputation); - if (Math.Abs(reputationChange) > 0) + void SetReputationText(GUITextBlock textBlock) { - string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; - string colorStr = XMLExtensions.ToStringHex(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); - var richText = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - richText, - textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); - } - else - { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - RichString.Rich(reputationText), - textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); + LocalizedString reputationText = Reputation.GetFormattedReputationText(reputation.NormalizedValue, reputation.Value, addColorTags: true); + int reputationChange = (int)Math.Round(reputation.Value - initialReputation); + if (Math.Abs(reputationChange) > 0) + { + string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; + string colorStr = XMLExtensions.ToStringHex(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); + textBlock.Text = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); + } + else + { + textBlock.Text = RichString.Rich(reputationText); + } } //spacing diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 18139a698..e388ac515 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -63,7 +63,6 @@ namespace Barotrauma public Vector2[] SlotPositions; public static Point SlotSize; - public static int Spacing; private Layout layout; public Layout CurrentLayout @@ -103,7 +102,7 @@ namespace Barotrauma { visualSlots ??= new VisualSlot[capacity]; - float multiplier = !GUI.IsFourByThree() ? UIScale : UIScale * 0.925f; + float multiplier = UIScale * GUI.AspectRatioAdjustment; for (int i = 0; i < capacity; i++) { @@ -219,18 +218,11 @@ namespace Barotrauma private void SetSlotPositions(Layout layout) { - bool isFourByThree = GUI.IsFourByThree(); - if (isFourByThree) - { - Spacing = (int)(5 * UIScale); - } - else - { - Spacing = (int)(8 * UIScale); - } + int spacing = GUI.IntScale(5); - SlotSize = !isFourByThree ? (SlotSpriteSmall.size * UIScale).ToPoint() : (SlotSpriteSmall.size * UIScale * .925f).ToPoint(); - int bottomOffset = SlotSize.Y + Spacing * 2 + ContainedIndicatorHeight; + SlotSize = (SlotSpriteSmall.size * UIScale * GUI.AspectRatioAdjustment).ToPoint(); + int bottomOffset = SlotSize.Y + spacing * 2 + ContainedIndicatorHeight; + int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - spacing * 2 - (int)(UnequippedIndicator.size.Y * UIScale); if (visualSlots == null) { CreateSlots(); } if (visualSlots.None()) { return; } @@ -242,11 +234,11 @@ namespace Barotrauma int personalSlotCount = SlotTypes.Count(s => PersonalSlots.HasFlag(s)); int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s) && s != InvSlotType.HealthInterface); - int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + Spacing) / 2; - int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing; + int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + spacing) / 2; + int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - spacing; //make sure the rightmost normal slot doesn't overlap with the personal slots - x -= Math.Max((x + normalSlotCount * (SlotSize.X + Spacing)) - (upperX - personalSlotCount * (SlotSize.X + Spacing)), 0); + x -= Math.Max((x + normalSlotCount * (SlotSize.X + spacing)) - (upperX - personalSlotCount * (SlotSize.X + spacing)), 0); int hideButtonSlotIndex = -1; for (int i = 0; i < SlotPositions.Length; i++) @@ -254,7 +246,7 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(upperX, GameMain.GraphicsHeight - bottomOffset); - upperX -= SlotSize.X + Spacing; + upperX -= SlotSize.X + spacing; personalSlotArea = (hideButtonSlotIndex == -1) ? new Rectangle(SlotPositions[i].ToPoint(), SlotSize) : Rectangle.Union(personalSlotArea, new Rectangle(SlotPositions[i].ToPoint(), SlotSize)); @@ -263,7 +255,7 @@ namespace Barotrauma else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += SlotSize.X + Spacing; + x += SlotSize.X + spacing; } } } @@ -271,7 +263,7 @@ namespace Barotrauma case Layout.Right: { int x = HUDLayoutSettings.InventoryAreaLower.Right; - int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - Spacing; + int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - spacing; for (int i = 0; i < visualSlots.Length; i++) { if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } @@ -282,19 +274,18 @@ namespace Barotrauma } else { - x -= SlotSize.X + Spacing; + x -= SlotSize.X + spacing; } } int lowerX = x; int handSlotX = x; - int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { SlotPositions[i] = new Vector2(handSlotX, personalSlotY); - handSlotX += visualSlots[i].Rect.Width + Spacing; + handSlotX += visualSlots[i].Rect.Width + spacing; continue; } @@ -302,12 +293,12 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); - personalSlotX -= visualSlots[i].Rect.Width + Spacing; + personalSlotX -= visualSlots[i].Rect.Width + spacing; } else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } @@ -316,7 +307,7 @@ namespace Barotrauma { if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } - x -= visualSlots[i].Rect.Width + Spacing; + x -= visualSlots[i].Rect.Width + spacing; SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); } } @@ -325,7 +316,6 @@ namespace Barotrauma { int x = HUDLayoutSettings.InventoryAreaLower.X; int personalSlotX = x; - int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { @@ -334,33 +324,33 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); - personalSlotX += visualSlots[i].Rect.Width + Spacing; + personalSlotX += visualSlots[i].Rect.Width + spacing; } else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } - int handSlotX = x - visualSlots[0].Rect.Width - Spacing; + int handSlotX = x - visualSlots[0].Rect.Width - spacing; for (int i = 0; i < SlotPositions.Length; i++) { if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { bool rightSlot = SlotTypes[i] == InvSlotType.RightHand; - SlotPositions[i] = new Vector2(rightSlot ? handSlotX : handSlotX - visualSlots[0].Rect.Width - Spacing, personalSlotY); + SlotPositions[i] = new Vector2(rightSlot ? handSlotX : handSlotX - visualSlots[0].Rect.Width - spacing, personalSlotY); continue; } if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } break; case Layout.Center: { int columns = 5; - int startX = (GameMain.GraphicsWidth / 2) - (SlotSize.X * columns + Spacing * (columns - 1)) / 2; + int startX = (GameMain.GraphicsWidth / 2) - (SlotSize.X * columns + spacing * (columns - 1)) / 2; int startY = GameMain.GraphicsHeight / 2 - (SlotSize.Y * 2); int x = startX, y = startY; for (int i = 0; i < SlotPositions.Length; i++) @@ -369,10 +359,10 @@ namespace Barotrauma if (SlotTypes[i] == InvSlotType.Card || SlotTypes[i] == InvSlotType.Headset || SlotTypes[i] == InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } - y += visualSlots[0].Rect.Height + Spacing + ContainedIndicatorHeight + visualSlots[0].EquipButtonRect.Height; + y += visualSlots[0].Rect.Height + spacing + ContainedIndicatorHeight + visualSlots[0].EquipButtonRect.Height; x = startX; int n = 0; for (int i = 0; i < SlotPositions.Length; i++) @@ -381,12 +371,12 @@ namespace Barotrauma if (SlotTypes[i] != InvSlotType.Card && SlotTypes[i] != InvSlotType.Headset && SlotTypes[i] != InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; n++; if (n >= columns) { x = startX; - y += visualSlots[i].Rect.Height + Spacing + ContainedIndicatorHeight + visualSlots[i].EquipButtonRect.Height; + y += visualSlots[i].Rect.Height + spacing + ContainedIndicatorHeight + visualSlots[i].EquipButtonRect.Height; n = 0; } } @@ -402,7 +392,7 @@ namespace Barotrauma { if (SlotTypes[i] != InvSlotType.HealthInterface) { continue; } SlotPositions[i] = pos; - pos.Y += visualSlots[i].Rect.Height + Spacing; + pos.Y += visualSlots[i].Rect.Height + spacing; } } @@ -641,7 +631,7 @@ namespace Barotrauma { slot.EquipButtonState = slot.EquipButtonRect.Contains(PlayerInput.MousePosition) ? GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None; - if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) + if (PlayerInput.PrimaryMouseButtonHeld() && PlayerInput.SecondaryMouseButtonHeld()) { slot.EquipButtonState = GUIComponent.ComponentState.None; } @@ -1018,7 +1008,47 @@ namespace Barotrauma SoundPlayer.PlayUISound(success ? GUISoundType.PickItem : GUISoundType.PickItemFail); } } - + + public bool CanBeAutoMovedToCorrectSlots(Item item) + { + if (item == null) { return false; } + foreach (var allowedSlot in item.AllowedSlots) + { + InvSlotType slotsFree = InvSlotType.None; + for (int i = 0; i < slots.Length; i++) + { + if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Empty()) { slotsFree |= SlotTypes[i]; } + } + if (allowedSlot == slotsFree) { return true; } + } + return false; + } + + /// + /// Flash the slots the item is allowed to go in (not taking into account whether there's already something in those slots) + /// + public void FlashAllowedSlots(Item item, Color color) + { + if (item == null || visualSlots == null) { return; } + bool flashed = false; + foreach (var allowedSlot in item.AllowedSlots) + { + for (int i = 0; i < slots.Length; i++) + { + if (allowedSlot.HasFlag(SlotTypes[i])) + { + visualSlots[i].ShowBorderHighlight(color, 0.1f, 0.9f); + flashed = true; + } + } + } + if (flashed) + { + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + } + } + + public void DrawOwn(SpriteBatch spriteBatch) { if (!AccessibleWhenAlive && !character.IsDead && !AccessibleByOwner) { return; } @@ -1106,40 +1136,24 @@ namespace Barotrauma color *= 0.5f; } - if (character.HasEquippedItem(slots[i].First())) + Vector2 indicatorScale = new Vector2( + visualSlots[i].EquipButtonRect.Size.X / EquippedIndicator.size.X, + visualSlots[i].EquipButtonRect.Size.Y / EquippedIndicator.size.Y); + + bool isEquipped = character.HasEquippedItem(slots[i].First()); + var sprite = state switch { - switch (state) - { - case GUIComponent.ComponentState.None: - EquippedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Hover: - EquippedHoverIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Pressed: - case GUIComponent.ComponentState.Selected: - case GUIComponent.ComponentState.HoverSelected: - EquippedClickedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - } - } - else - { - switch (state) - { - case GUIComponent.ComponentState.None: - UnequippedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Hover: - UnequippedHoverIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Pressed: - case GUIComponent.ComponentState.Selected: - case GUIComponent.ComponentState.HoverSelected: - UnequippedClickedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - } - } + GUIComponent.ComponentState.None + => isEquipped ? EquippedIndicator : UnequippedIndicator, + GUIComponent.ComponentState.Hover + => isEquipped ? EquippedHoverIndicator : UnequippedHoverIndicator, + GUIComponent.ComponentState.Pressed + or GUIComponent.ComponentState.Selected + or GUIComponent.ComponentState.HoverSelected + => isEquipped ? EquippedClickedIndicator : UnequippedClickedIndicator, + _ => throw new NotImplementedException() + }; + sprite.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, indicatorScale); } if (Locked) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index f5faabb79..1093798a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -92,9 +92,6 @@ namespace Barotrauma.Items.Components rect.Height = (int)(rect.Height * (1.0f - openState)); } - //only merge the door's convex hull with overlapping wall segments if it's fully open or fully closed - //it's the heaviest part of changing the convex hull, and doesn't need to be done while the door is still in motion - bool mergeOverlappingSegments = openState <= 0.0f || openState >= 1.0f; if (Window.Height > 0 && Window.Width > 0) { if (IsHorizontal) @@ -117,7 +114,7 @@ namespace Barotrauma.Items.Components else { convexHull2.Enabled = true; - convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); + SetVertices(convexHull2, rect2); } } } @@ -141,7 +138,7 @@ namespace Barotrauma.Items.Components else { convexHull2.Enabled = true; - convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); + SetVertices(convexHull2, rect2); } } } @@ -156,13 +153,28 @@ namespace Barotrauma.Items.Components else { convexHull.Enabled = true; - convexHull.SetVertices(GetConvexHullCorners(rect), mergeOverlappingSegments); + SetVertices(convexHull, rect); } + convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom; + if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; } } + private void SetVertices(ConvexHull convexHull, Rectangle rect) + { + var verts = GetConvexHullCorners(rect); + Vector2 center = (verts[0] + verts[2]) / 2; + convexHull.SetVertices( + verts, + IsHorizontal ? + new Vector2[] { new Vector2(verts[0].X, center.Y), new Vector2(verts[2].X, center.Y) } : + new Vector2[] { new Vector2(center.X, verts[0].Y), new Vector2(center.X, verts[2].Y) }); + } + partial void UpdateProjSpecific(float deltaTime) { + convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom; + if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; } if (shakeTimer > 0.0f) { shakeTimer -= deltaTime; @@ -182,7 +194,7 @@ namespace Barotrauma.Items.Components if (brokenSprite == null) { //broken doors turn black if no broken sprite has been configured - color *= (item.Condition / item.MaxCondition); + color = color.Multiply(item.Condition / item.MaxCondition); color.A = 255; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index f50239f35..428996d21 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -88,6 +88,11 @@ namespace Barotrauma.Items.Components public void ClientEventRead(IReadMessage msg, float sendingTime) { + UInt16 userID = msg.ReadUInt16(); + if (userID != Entity.NullEntityID) + { + user = Entity.FindEntityByID(userID) as Character; + } CurrPowerConsumption = powerConsumption; charging = true; timer = Duration; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index fd8f360eb..2f23d59d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -1,13 +1,10 @@ using Barotrauma.Particles; +using Barotrauma.Sounds; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using Barotrauma.IO; -using System.Text; -using System.Xml.Linq; -using Barotrauma.Sounds; using System.Linq; namespace Barotrauma.Items.Components @@ -169,11 +166,11 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific() { Vector2 particlePos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); - float rotation = -item.body.Rotation; + float rotation = item.body.Rotation; if (item.body.Dir < 0.0f) { rotation += MathHelper.Pi; } foreach (ParticleEmitter emitter in particleEmitters) { - emitter.Emit(1.0f, particlePos, hullGuess: item.CurrentHull, angle: rotation, particleRotation: rotation); + emitter.Emit(1.0f, particlePos, hullGuess: item.CurrentHull, angle: rotation, particleRotation: -rotation); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 345411caa..ebd078a7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -505,13 +505,14 @@ namespace Barotrauma.Items.Components } ActionType type; + string typeStr = subElement.GetAttributeString("type", ""); try { - type = (ActionType)Enum.Parse(typeof(ActionType), subElement.GetAttributeString("type", ""), true); + type = (ActionType)Enum.Parse(typeof(ActionType), typeStr, true); } catch (Exception e) { - DebugConsole.ThrowError("Invalid sound type in " + subElement + "!", e); + DebugConsole.ThrowError($"Invalid sound type \"{typeStr}\" in item \"{item.Prefab.Identifier}\"!", e); break; } @@ -524,11 +525,13 @@ namespace Barotrauma.Items.Components VolumeProperty = subElement.GetAttributeIdentifier("volumeproperty", "") }; - if (soundSelectionModes == null) soundSelectionModes = new Dictionary(); + if (soundSelectionModes == null) + { + soundSelectionModes = new Dictionary(); + } if (!soundSelectionModes.ContainsKey(type) || soundSelectionModes[type] == SoundSelectionMode.Random) { - Enum.TryParse(subElement.GetAttributeString("selectionmode", "Random"), out SoundSelectionMode selectionMode); - soundSelectionModes[type] = selectionMode; + soundSelectionModes[type] = subElement.GetAttributeEnum("selectionmode", SoundSelectionMode.Random); } if (!sounds.TryGetValue(itemSound.Type, out List soundList)) @@ -584,6 +587,8 @@ namespace Barotrauma.Items.Components { if (GuiFrame != null && GuiFrameSource.GetAttributeBool("draggable", true)) { + bool hideDragIcons = GuiFrameSource.GetAttributeBool("hidedragicons", false); + var handle = new GUIDragHandle(new RectTransform(Vector2.One, GuiFrame.RectTransform, Anchor.Center), GuiFrame.RectTransform, style: null) { @@ -623,7 +628,7 @@ namespace Barotrauma.Items.Components }; int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); - new GUIButton(new RectTransform(new Point(buttonHeight), handle.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, + var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), handle.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, style: "GUIButtonSettings") { OnClicked = (btn, userdata) => @@ -648,6 +653,12 @@ namespace Barotrauma.Items.Components return true; } }; + + if (hideDragIcons) + { + dragIcon.Visible = false; + settingsIcon.Visible = false; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 5b16c7d9c..17293bf7f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -277,7 +277,8 @@ namespace Barotrauma.Items.Components int ignoredItemCount = 0; var subContainableItems = AllSubContainableItems; - float capacity = GetMaxStackSize(targetSlot); + float targetSlotCapacity = GetMaxStackSize(targetSlot); + float capacity = targetSlotCapacity * MainContainerCapacity; if (subContainableItems != null) { bool useMainContainerCapacity = true; @@ -299,15 +300,11 @@ namespace Barotrauma.Items.Components } if (!useMainContainerCapacity) { break; } } - if (useMainContainerCapacity) - { - capacity *= MainContainerCapacity; - } - else + if (!useMainContainerCapacity) { // Ignore all items in the main container. ignoredItemCount = Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it))); - capacity *= Capacity - MainContainerCapacity; + capacity = targetSlotCapacity * (Capacity - MainContainerCapacity); } } int itemCount = Inventory.AllItems.Count() - ignoredItemCount; @@ -391,63 +388,60 @@ namespace Barotrauma.Items.Components bool isWiringMode = SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); int i = 0; - foreach (Item containedItem in Inventory.AllItems) + foreach (DrawableContainedItem contained in drawableContainedItems) { Vector2 itemPos = currentItemPos; - var relatedItem = FindContainableItem(containedItem); - if (relatedItem != null) + + if (contained.Item?.Sprite == null) { continue; } + + if (contained.Hide) { continue; } + if (contained.ItemPos.HasValue) { - if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; } - if (relatedItem.ItemPos.HasValue) + Vector2 pos = contained.ItemPos.Value; + if (item.body != null) { - Vector2 pos = relatedItem.ItemPos.Value; - if (item.body != null) + Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) { - Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); - pos.X *= item.body.Dir; - itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition; + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; } - else + if (item.FlippedY) { - itemPos = pos; - // This code is aped based on above. Not tested. - if (item.FlippedX) - { - itemPos.X = -itemPos.X; - itemPos.X += item.Rect.Width; - } - if (item.FlippedY) - { - itemPos.Y = -itemPos.Y; - itemPos.Y -= item.Rect.Height; - } - itemPos += new Vector2(item.Rect.X, item.Rect.Y); - if (item.Submarine != null) - { - itemPos += item.Submarine.DrawPosition; - } - if (Math.Abs(item.RotationRad) > 0.01f) - { - Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); - itemPos = Vector2.Transform(itemPos - item.DrawPosition, transform) + item.DrawPosition; - } + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (item.Submarine != null) + { + itemPos += item.Submarine.DrawPosition; + } + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.DrawPosition, transform) + item.DrawPosition; } } } - - if (containedItem?.Sprite == null) { continue; } - + if (AutoInteractWithContained) { - containedItem.IsHighlighted = item.IsHighlighted; + contained.Item.IsHighlighted = item.IsHighlighted; item.IsHighlighted = false; } - Vector2 origin = containedItem.Sprite.Origin; - if (item.FlippedX) { origin.X = containedItem.Sprite.SourceRect.Width - origin.X; } - if (item.FlippedY) { origin.Y = containedItem.Sprite.SourceRect.Height - origin.Y; } + 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; } - float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? containedItem.Sprite.Depth : ContainedSpriteDepth; + float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? contained.Item.Sprite.Depth : ContainedSpriteDepth; if (i < containedSpriteDepths.Length) { containedSpriteDepth = containedSpriteDepths[i]; @@ -456,9 +450,9 @@ namespace Barotrauma.Items.Components SpriteEffects spriteEffects = SpriteEffects.None; float spriteRotation = ItemRotation; - if (relatedItem != null && relatedItem.Rotation != 0) + if (contained.Rotation != 0) { - spriteRotation = relatedItem.Rotation; + spriteRotation = contained.Rotation; } if ((item.body != null && item.body.Dir == -1) || item.FlippedX) { @@ -469,17 +463,17 @@ namespace Barotrauma.Items.Components spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; } - containedItem.Sprite.Draw( + contained.Item.Sprite.Draw( spriteBatch, new Vector2(itemPos.X, -itemPos.Y), - isWiringMode ? containedItem.GetSpriteColor(withHighlight: true) * 0.15f : containedItem.GetSpriteColor(withHighlight: true), + isWiringMode ? contained.Item.GetSpriteColor(withHighlight: true) * 0.15f : contained.Item.GetSpriteColor(withHighlight: true), origin, - -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation), - containedItem.Scale, + -(contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), + contained.Item.Scale, spriteEffects, depth: containedSpriteDepth); - foreach (ItemContainer ic in containedItem.GetComponents()) + foreach (ItemContainer ic in contained.Item.GetComponents()) { if (ic.hideItems) { continue; } ic.DrawContainedItems(spriteBatch, containedSpriteDepth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 2c764937c..763d89b4f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -14,6 +14,16 @@ namespace Barotrauma.Items.Components private CoroutineHandle resetPredictionCoroutine; private float resetPredictionTimer; + /// + /// The current multiplier for the light color (usually equal to , but in the case of e.g. blinking lights the multiplier + /// doesn't go to 0 when the light turns off, because otherwise it'd take a while for it turn back on based on the lightBrightness which is interpolated + /// towards the current voltage). + /// + private float lightColorMultiplier; + + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The scale of the light sprite.")] + public float LightSpriteScale { get; set; } + public Vector2 DrawSize { get { return new Vector2(Light.Range * 2, Light.Range * 2); } @@ -27,21 +37,14 @@ namespace Barotrauma.Items.Components Light.Position = ParentBody != null ? ParentBody.Position : item.Position; } - partial void SetLightSourceState(bool enabled, float? brightness) + partial void SetLightSourceState(bool enabled, float brightness) { if (Light == null) { return; } Light.Enabled = enabled; - if (brightness.HasValue) - { - lightBrightness = brightness.Value; - } - else - { - lightBrightness = enabled ? 1.0f : 0.0f; - } + lightColorMultiplier = brightness; if (enabled) { - Light.Color = LightColor.Multiply(lightBrightness); + Light.Color = LightColor.Multiply(lightColorMultiplier); } } @@ -92,7 +95,13 @@ namespace Barotrauma.Items.Components { color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value); } - Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), color * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); + Light.LightSprite.Draw(spriteBatch, + new Vector2(drawPos.X, -drawPos.Y), + color * lightBrightness, + origin, + -Light.Rotation, + item.Scale * LightSpriteScale, + Light.LightSpriteEffect, itemDepth - 0.0001f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 3debe66f7..1dd6f4341 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -185,6 +185,7 @@ namespace Barotrauma.Items.Components RefreshActivateButtonText(); if (GameMain.Client != null) { + pendingFabricatedItem = null; item.CreateClientEvent(this); } return true; @@ -336,8 +337,11 @@ namespace Barotrauma.Items.Components int calculatePlacement(FabricationRecipe recipe) { + if (recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem)) + { + return -2; + } int placement = FabricationDegreeOfSuccess(character, recipe.RequiredSkills) >= 0.5f ? 0 : -1; - placement += recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem) ? -2 : 0; return placement; } @@ -524,7 +528,7 @@ namespace Barotrauma.Items.Components if (slotRect.Contains(PlayerInput.MousePosition)) { - var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name); + var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name).Distinct(); LocalizedString toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients); if (suitableIngredients.Count() > 3) { toolTipText += "..."; } if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) @@ -546,9 +550,11 @@ namespace Barotrauma.Items.Components { toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText); } + + toolTipText = $"‖color:{Color.White.ToStringHex()}‖{toolTipText}‖color:end‖"; if (!requiredItemPrefab.Description.IsNullOrEmpty()) { - toolTipText += '\n' + requiredItemPrefab.Description; + toolTipText = '\n' + requiredItemPrefab.Description; } tooltip = new ToolTip { TargetElement = slotRect, Tooltip = toolTipText }; } @@ -590,7 +596,7 @@ namespace Barotrauma.Items.Components if (tooltip != null) { - GUIComponent.DrawToolTip(spriteBatch, tooltip.Tooltip, tooltip.TargetElement); + GUIComponent.DrawToolTip(spriteBatch, RichString.Rich(tooltip.Tooltip), tooltip.TargetElement); tooltip = null; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index f901ec9ea..969a67312 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -298,7 +298,7 @@ namespace Barotrauma.Items.Components } } - OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); + OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, style: null) { @@ -452,7 +452,7 @@ namespace Barotrauma.Items.Components foreach (var (entity, component) in electricalMapComponents) { GUIComponent parent = component.RectComponent; - if (!(entity is Item it )) { continue; } + if (entity is not Item it ) { continue; } Sprite? sprite = it.Prefab.UpgradePreviewSprite; if (sprite is null) { continue; } @@ -476,7 +476,7 @@ namespace Barotrauma.Items.Components { if (!hullPointsOfInterest.Contains(entity)) { continue; } - if (!(entity is Item it)) { continue; } + if (entity is not Item it) { continue; } const int borderMaxSize = 2; if (it.GetComponent() is { }) @@ -643,7 +643,7 @@ namespace Barotrauma.Items.Components elementSize = GuiFrame.Rect.Size; } - float distort = 1.0f - item.Condition / item.MaxCondition; + float distort = item.Repairables.Any(r => r.IsBelowRepairThreshold) ? 1.0f - item.Condition / item.MaxCondition : 0.0f; foreach (HullData hullData in hullDatas.Values) { hullData.DistortionTimer -= deltaTime; @@ -702,6 +702,12 @@ namespace Barotrauma.Items.Components private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container) { + if (miniMapFrame == null) + { + //frame not created yet, could happen if the item hasn't been inside any sub this round? + return; + } + if (Voltage < MinVoltage) { Vector2 textSize = GUIStyle.Font.MeasureString(noPowerTip); @@ -1130,7 +1136,7 @@ namespace Barotrauma.Items.Components { foreach (var (entity, miniMapGuiComponent) in electricalMapComponents) { - if (!(entity is Item it)) { continue; } + if (entity is not Item it) { continue; } if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent? component)) { continue; } if (entity.Removed) @@ -1220,7 +1226,7 @@ namespace Barotrauma.Items.Components { foreach (var (entity, component) in hullStatusComponents) { - if (!(entity is Hull hull)) { continue; } + if (entity is not Hull hull) { continue; } if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } if (hullData.Distort) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 628f5a54b..9c03c2a3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -17,6 +17,7 @@ namespace Barotrauma.Items.Components Default, Disruption, Destructible, + Door, LongRange } @@ -110,6 +111,10 @@ namespace Barotrauma.Items.Components BlipType.Destructible, new Color[] { Color.TransparentBlack, new Color(74, 113, 75) * 0.8f, new Color(151, 236, 172) * 0.8f, new Color(153, 217, 234) * 0.8f } }, + { + BlipType.Door, + new Color[] { Color.TransparentBlack, new Color(73, 78, 86), new Color(66, 94, 100), new Color(47, 115, 58), new Color(255, 255, 255) } + }, { BlipType.LongRange, new Color[] { Color.TransparentBlack, Color.TransparentBlack, new Color(254, 68, 19) * 0.8f, Color.TransparentBlack } @@ -975,7 +980,7 @@ namespace Barotrauma.Items.Components if (GameMain.GameSession == null || Level.Loaded == null) { return; } - if (Level.Loaded.StartLocation != null) + if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true }) { DrawMarker(spriteBatch, Level.Loaded.StartLocation.Name, @@ -985,7 +990,7 @@ namespace Barotrauma.Items.Components displayScale, center, DisplayRadius); } - if (Level.Loaded.EndLocation != null && Level.Loaded.Type == LevelData.LevelType.LocationConnection) + if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection }) { DrawMarker(spriteBatch, Level.Loaded.EndLocation.Name, @@ -1010,19 +1015,19 @@ namespace Barotrauma.Items.Components int missionIndex = 0; foreach (Mission mission in GameMain.GameSession.Missions) { - if (!mission.SonarLabel.IsNullOrWhiteSpace()) + int i = 0; + foreach ((LocalizedString label, Vector2 position) in mission.SonarLabels) { - int i = 0; - foreach (Vector2 sonarPosition in mission.SonarPositions) + if (!string.IsNullOrEmpty(label.Value)) { DrawMarker(spriteBatch, - mission.SonarLabel.Value, + label.Value, mission.SonarIconIdentifier, "mission" + missionIndex + ":" + i, - sonarPosition, transducerCenter, + position, transducerCenter, displayScale, center, DisplayRadius * 0.95f); - i++; } + i++; } missionIndex++; } @@ -1176,13 +1181,18 @@ namespace Barotrauma.Items.Components if (dockingPort.Item.Submarine == null) { continue; } if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } // docking ports should be shown even if defined as not, if the submarine is the same as the sonar's - if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine && !dockingPort.Item.Submarine.Info.IsOutpost) { continue; } + if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine && + !dockingPort.Item.Submarine.Info.IsOutpost && !dockingPort.Item.Submarine.Info.IsBeacon) + { + continue; + } //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 && - dockingPort.Item.Submarine.Info.Type != SubmarineType.Outpost) + !dockingPort.Item.Submarine.Info.IsOutpost && + !dockingPort.Item.Submarine.Info.IsBeacon) { // specifically checking for friendlyNPC seems more logical here if (dockingPort.Item.Submarine.TeamID != item.Submarine.TeamID && dockingPort.Item.Submarine.TeamID != CharacterTeamType.FriendlyNPC) { continue; } @@ -1348,6 +1358,38 @@ namespace Barotrauma.Items.Components } } + public void RegisterExplosion(Explosion explosion, Vector2 worldPosition) + { + if (Character.Controlled?.SelectedItem != item) { return; } + if (explosion.Attack.StructureDamage <= 0 && explosion.Attack.ItemDamage <= 0 && explosion.EmpStrength <= 0) { return; } + Vector2 transducerCenter = GetTransducerPos(); + if (Vector2.DistanceSquared(worldPosition, transducerCenter) > range * range) { return; } + int blipCount = MathHelper.Clamp((int)(explosion.Attack.Range / 100.0f), 0, 50); + for (int i = 0; i < blipCount; i++) + { + sonarBlips.Add(new SonarBlip( + worldPosition + Rand.Vector(Rand.Range(0.0f, explosion.Attack.Range)), + 1.0f, + Rand.Range(0.5f, 1.0f), + BlipType.Disruption)); + } + if (explosion.EmpStrength > 0.0f) + { + int empBlipCount = MathHelper.Clamp((int)(blipCount * explosion.EmpStrength), 10, 50); + for (int i = 0; i < empBlipCount; i++) + { + Vector2 dir = Rand.Vector(1.0f); + var longRangeBlip = new SonarBlip(worldPosition, Rand.Range(1.9f, 2.1f), Rand.Range(1.0f, 1.5f), BlipType.LongRange) + { + Velocity = dir * MathUtils.Round(Rand.Range(4000.0f, 6000.0f), 1000.0f), + Rotation = (float)Math.Atan2(-dir.Y, dir.X) + }; + longRangeBlip.Size.Y *= 4.0f; + sonarBlips.Add(longRangeBlip); + } + } + } + private void Ping(Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float displayScale, float range, bool passive, float pingStrength = 1.0f, AITarget needsToBeInSector = null) { @@ -1392,6 +1434,16 @@ namespace Barotrauma.Items.Components if (connectedSubs.Contains(submarine)) { continue; } } + //display the actual walls if the ping source is inside the sub (but not inside a hull, that's handled above) + //only relevant in the end levels or maybe custom subs with some kind of non-hulled parts + Rectangle worldBorders = submarine.GetDockedBorders(); + worldBorders.Location += submarine.WorldPosition.ToPoint(); + if (Submarine.RectContains(worldBorders, pingSource)) + { + CreateBlipsForSubmarineWalls(submarine, pingSource, transducerPos, pingRadius, prevPingRadius, range, passive); + continue; + } + for (int i = 0; i < submarine.HullVertices.Count; i++) { Vector2 start = ConvertUnits.ToDisplayUnits(submarine.HullVertices[i]); @@ -1608,6 +1660,40 @@ namespace Barotrauma.Items.Components } } + private void CreateBlipsForSubmarineWalls(Submarine sub, Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float range, bool passive) + { + foreach (Structure structure in Structure.WallList) + { + if (structure.Submarine != sub) { continue; } + CreateBlips(structure.IsHorizontal, structure.WorldPosition, structure.WorldRect); + } + foreach (var door in Door.DoorList) + { + if (door.Item.Submarine != sub || door.IsOpen) { continue; } + CreateBlips(door.IsHorizontal, door.Item.WorldPosition, door.Item.WorldRect, BlipType.Door); + } + + void CreateBlips(bool isHorizontal, Vector2 worldPos, Rectangle worldRect, BlipType blipType = BlipType.Default) + { + Vector2 point1, point2; + if (isHorizontal) + { + point1 = new Vector2(worldRect.X, worldPos.Y); + point2 = new Vector2(worldRect.Right, worldPos.Y); + } + else + { + point1 = new Vector2(worldPos.X, worldRect.Y); + point2 = new Vector2(worldPos.X, worldRect.Y - worldRect.Height); + } + CreateBlipsForLine( + point1, + point2, + pingSource, transducerPos, + pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, blipType); + } + } + private bool CheckBlipVisibility(SonarBlip blip, Vector2 transducerPos) { Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 3759e9fd1..6a5d4a44b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -178,8 +178,9 @@ namespace Barotrauma.Items.Components var autoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.62f), paddedControlContainer.RectTransform, Anchor.BottomCenter), "OutlineFrame"); var paddedAutoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.92f, 0.88f), autoPilotControls.RectTransform, Anchor.Center), style: null); + int textLimit = (int)(paddedAutoPilotControls.Rect.Width * 0.75f); maintainPosTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.TopCenter), - TextManager.Get("SteeringMaintainPos"), font: GUIStyle.SmallFont, style: "GUIRadioButton") + ToolBox.LimitString(TextManager.Get("SteeringMaintainPos"), GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") { UserData = UIHighlightAction.ElementId.MaintainPosTickBox, Enabled = autoPilot, @@ -214,7 +215,6 @@ namespace Barotrauma.Items.Components return true; } }; - int textLimit = (int)(paddedAutoPilotControls.Rect.Width * 0.75f); levelStartTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.Center), GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.Name, GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") @@ -340,6 +340,10 @@ namespace Barotrauma.Items.Components centerText = $"({TextManager.Get("Meter")})"; rightTextGetter = () => { + if (Level.Loaded is { IsEndBiome: true }) + { + return Timing.TotalTime % 5.0f < 0.5f ? Rand.Range(-9000, 9000).ToString() : "ERROR"; + } float realWorldDepth = controlledSub == null ? -1000.0f : controlledSub.RealWorldDepth; return ((int)realWorldDepth).ToString(); }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 28a25c88e..6a2b6571b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -20,6 +20,7 @@ namespace Barotrauma.Items.Components User = Entity.FindEntityByID(userId) as Character; Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); float rotation = msg.ReadSingle(); + SpreadCounter = msg.ReadByte(); if (User != null) { Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index afaeb2dca..c25a7c6cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -262,9 +262,11 @@ namespace Barotrauma.Items.Components } } + float conditionPercentage = item.Condition / (item.MaxCondition / item.MaxRepairConditionMultiplier) * 100f; + for (int i = 0; i < particleEmitters.Count; i++) { - if ((item.ConditionPercentage >= particleEmitterConditionRanges[i].X && item.ConditionPercentage <= particleEmitterConditionRanges[i].Y) || FakeBrokenTimer > 0.0f) + if ((conditionPercentage >= particleEmitterConditionRanges[i].X && conditionPercentage <= particleEmitterConditionRanges[i].Y) || FakeBrokenTimer > 0.0f) { particleEmitters[i].Emit(deltaTime, item.WorldPosition, item.CurrentHull); } @@ -436,12 +438,16 @@ namespace Barotrauma.Items.Components ushort currentFixerID = msg.ReadUInt16(); currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; - item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); - if (CurrentFixer == null) + + if (CurrentFixer is null) { qteTimer = QteDuration; qteCooldown = 0.0f; } + else + { + item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); + } } public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 9526f7f63..5bee8bba0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -108,6 +108,7 @@ namespace Barotrauma.Items.Components { 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 -= turret.GetRecoilOffset(); } else if (weapon != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 022972368..a83d9a305 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -259,7 +259,7 @@ namespace Barotrauma.Items.Components { bool alreadyConnected = DraggingConnected.IsConnectedTo(panel.Item); DraggingConnected.RemoveConnection(panel.Item); - if (DraggingConnected.Connect(this, !alreadyConnected, true)) + if (DraggingConnected.TryConnect(this, !alreadyConnected, true)) { var otherConnection = DraggingConnected.OtherConnection(this); ConnectWire(DraggingConnected); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 915e8f695..9f755a3a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -110,8 +110,8 @@ namespace Barotrauma.Items.Components if (HighlightedWire != null) { HighlightedWire.Item.IsHighlighted = true; - if (HighlightedWire.Connections[0] != null && HighlightedWire.Connections[0].Item != null) HighlightedWire.Connections[0].Item.IsHighlighted = true; - if (HighlightedWire.Connections[1] != null && HighlightedWire.Connections[1].Item != null) HighlightedWire.Connections[1].Item.IsHighlighted = true; + if (HighlightedWire.Connections[0] != null && HighlightedWire.Connections[0].Item != null) { HighlightedWire.Connections[0].Item.IsHighlighted = true; } + if (HighlightedWire.Connections[1] != null && HighlightedWire.Connections[1].Item != null) { HighlightedWire.Connections[1].Item.IsHighlighted = true; } } } @@ -225,7 +225,7 @@ namespace Barotrauma.Items.Components foreach (var wire in newWires.Where(w => !connection.Wires.Contains(w)).ToArray()) { connection.ConnectWire(wire); - wire.Connect(connection, false); + wire.TryConnect(connection, false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 29b3adae2..1668f9739 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components private readonly List uiElements = new List(); private GUILayoutGroup uiElementContainer; + private bool readingNetworkEvent; + private Point ElementMaxSize => new Point(uiElementContainer.Rect.Width, (int)(65 * GUI.yScale)); public override bool RecreateGUIOnResolutionChange => true; @@ -100,7 +102,7 @@ namespace Barotrauma.Items.Components { ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this); } @@ -126,7 +128,7 @@ namespace Barotrauma.Items.Components { ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this); } @@ -161,7 +163,7 @@ namespace Barotrauma.Items.Components { TickBoxToggled(tBox.UserData as CustomInterfaceElement, tBox.Selected); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this); } @@ -181,12 +183,12 @@ namespace Barotrauma.Items.Components }; btn.OnClicked += (_, userdata) => { - CustomInterfaceElement btnElement = userdata as CustomInterfaceElement;; + CustomInterfaceElement btnElement = userdata as CustomInterfaceElement; if (GameMain.Client == null) { ButtonClicked(btnElement); } - else + else if (!readingNetworkEvent) { item.CreateClientEvent(this, new EventData(btnElement)); } @@ -297,9 +299,10 @@ namespace Barotrauma.Items.Components LocalizedString CreateLabelText(int elementIndex) { - return string.IsNullOrWhiteSpace(customInterfaceElementList[elementIndex].Label) ? + var label = customInterfaceElementList[elementIndex].Label; + return string.IsNullOrWhiteSpace(label) ? TextManager.GetWithVariable("connection.signaloutx", "[num]", (elementIndex + 1).ToString()) : - customInterfaceElementList[elementIndex].Label; + TextManager.Get(label).Fallback(label); } uiElementContainer.Recalculate(); @@ -386,45 +389,53 @@ namespace Barotrauma.Items.Components public void ClientEventRead(IReadMessage msg, float sendingTime) { - for (int i = 0; i < customInterfaceElementList.Count; i++) + readingNetworkEvent = true; + try { - var element = customInterfaceElementList[i]; - if (element.HasPropertyName) + for (int i = 0; i < customInterfaceElementList.Count; i++) { - string newValue = msg.ReadString(); - if (!element.IsNumberInput) + var element = customInterfaceElementList[i]; + if (element.HasPropertyName) { - TextChanged(element, newValue); + string newValue = msg.ReadString(); + if (!element.IsNumberInput) + { + TextChanged(element, newValue); + } + else + { + switch (element.NumberType) + { + case NumberType.Int when int.TryParse(newValue, out int value): + ValueChanged(element, value); + break; + case NumberType.Float when TryParseFloatInvariantCulture(newValue, out float value): + ValueChanged(element, value); + break; + } + } } else { - switch (element.NumberType) + bool elementState = msg.ReadBoolean(); + if (element.ContinuousSignal) { - case NumberType.Int when int.TryParse(newValue, out int value): - ValueChanged(element, value); - break; - case NumberType.Float when TryParseFloatInvariantCulture(newValue, out float value): - ValueChanged(element, value); - break; + ((GUITickBox)uiElements[i]).Selected = elementState; + TickBoxToggled(element, elementState); + } + else if (elementState) + { + ButtonClicked(element); } } } - else - { - bool elementState = msg.ReadBoolean(); - if (element.ContinuousSignal) - { - ((GUITickBox)uiElements[i]).Selected = elementState; - TickBoxToggled(element, elementState); - } - else if (elementState) - { - ButtonClicked(element); - } - } - } - UpdateSignalsProjSpecific(); + UpdateSignalsProjSpecific(); + } + finally + { + readingNetworkEvent = false; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index ec3054df9..c879cb159 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Linq; using System.Xml.Linq; @@ -24,7 +25,9 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element) { - var layoutGroup = new GUILayoutGroup(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }) + float marginMultiplier = element.GetAttributeFloat("marginmultiplier", 1.0f); + + var layoutGroup = new GUILayoutGroup(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin.Multiply(marginMultiplier), GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset.Multiply(marginMultiplier) }) { ChildAnchor = Anchor.TopCenter, RelativeSpacing = 0.02f, @@ -33,31 +36,34 @@ namespace Barotrauma.Items.Components historyBox = new GUIListBox(new RectTransform(new Vector2(1, .9f), layoutGroup.RectTransform), style: null) { - AutoHideScrollBar = false + AutoHideScrollBar = this.AutoHideScrollbar }; - CreateFillerBlock(); - - new GUIFrame(new RectTransform(new Vector2(0.9f, 0.01f), layoutGroup.RectTransform), style: "HorizontalLine"); - - inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: TextColor) + if (!Readonly) { - MaxTextLength = MaxMessageLength, - OverflowClip = true, - OnEnterPressed = (GUITextBox textBox, string text) => + CreateFillerBlock(); + + new GUIFrame(new RectTransform(new Vector2(0.9f, 0.01f), layoutGroup.RectTransform), style: "HorizontalLine"); + + inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: TextColor) { - if (GameMain.NetworkMember == null) + MaxTextLength = MaxMessageLength, + OverflowClip = true, + OnEnterPressed = (GUITextBox textBox, string text) => { - SendOutput(text); + if (GameMain.NetworkMember == null) + { + SendOutput(text); + } + else + { + item.CreateClientEvent(this, new ClientEventData(text)); + } + textBox.Text = string.Empty; + return true; } - else - { - item.CreateClientEvent(this, new ClientEventData(text)); - } - textBox.Text = string.Empty; - return true; - } - }; + }; + } layoutGroup.Recalculate(); } @@ -101,7 +107,7 @@ namespace Barotrauma.Items.Components GUITextBlock newBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), - "> " + input, + LineStartSymbol + TextManager.Get(input).Fallback(input), textColor: color, wrap: true, font: UseMonospaceFont ? GUIStyle.MonospacedFont : GUIStyle.Font) { CanBeFocused = false @@ -123,7 +129,10 @@ namespace Barotrauma.Items.Components historyBox.RecalculateChildren(); historyBox.UpdateScrollBarSize(); - historyBox.ScrollBar.BarScrollValue = 1; + if (AutoScrollToBottom) + { + historyBox.ScrollBar.BarScrollValue = 1; + } } public override bool Select(Character character) @@ -138,7 +147,7 @@ namespace Barotrauma.Items.Components public override void AddToGUIUpdateList(int order = 0) { base.AddToGUIUpdateList(order: order); - if (shouldSelectInputBox) + if (shouldSelectInputBox && !Readonly) { inputBox.Select(); shouldSelectInputBox = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index afe938020..426d2c127 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -212,7 +212,7 @@ namespace Barotrauma.Items.Components Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite; foreach (Limb limb in c.AnimController.Limbs) { - if (limb.Mass < 1.0f) { continue; } + 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); @@ -302,7 +302,18 @@ namespace Barotrauma.Items.Components Dictionary combinedAfflictionStrengths = new Dictionary(); foreach (Affliction affliction in allAfflictions) { - if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold || affliction.Strength <= 0.0f) { continue; } + if (affliction.Strength <= 0f) { continue; } + if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold) + { + if (target.IsHuman || target.IsOnPlayerTeam || (affliction.Prefab.AfflictionType != AfflictionPrefab.PoisonType && affliction.Prefab.AfflictionType != AfflictionPrefab.ParalysisType)) + { + // Always show the poisons on monsters, because poisoning bigger monsters require multiple doses. + // The solution is hacky, but didn't want to introduce an extra property for this. + // We also want to have a relatively high thershold for showing the poisons on the scanner on humans, so that it's not instantly clear that a target is poisoned and especially not which poison was used. + // Paralysis is treated like a poison but isn't technically a poison, so that we can have multiple afflictions that still are treated the same. + continue; + } + } if (combinedAfflictionStrengths.ContainsKey(affliction.Prefab)) { combinedAfflictionStrengths[affliction.Prefab] += affliction.Strength; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index e66a15de4..402805f70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -333,15 +333,9 @@ namespace Barotrauma.Items.Components crosshairPointerPos = PlayerInput.MousePosition; } - - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) - { - if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) - { - UpdateTransformedBarrelPos(); - } - Vector2 drawPos = GetDrawPos(); + public Vector2 GetRecoilOffset() + { float recoilOffset = 0.0f; if (Math.Abs(RecoilDistance) > 0.0f && recoilTimer > 0.0f) { @@ -362,6 +356,17 @@ namespace Barotrauma.Items.Components recoilOffset = RecoilDistance; } } + return new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset; + } + + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + { + if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) + { + UpdateTransformedBarrelPos(); + } + Vector2 drawPos = GetDrawPos(); + railSprite?.Draw(spriteBatch, drawPos, @@ -370,7 +375,7 @@ namespace Barotrauma.Items.Components SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); barrelSprite?.Draw(spriteBatch, - drawPos - new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset * item.Scale, + drawPos - GetRecoilOffset() * item.Scale, item.SpriteColor, rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 5b4d60065..96607abbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -53,11 +53,11 @@ namespace Barotrauma get { // Returns a point off-screen, Rectangle.Empty places buttons in the top left of the screen - if (IsMoving) return offScreenRect; + if (IsMoving) { return offScreenRect; } int buttonDir = Math.Sign(SubInventoryDir); - float sizeY = Inventory.UnequippedIndicator.size.Y * Inventory.UIScale * Inventory.IndicatorScaleAdjustment; + float sizeY = Inventory.UnequippedIndicator.size.Y * Inventory.UIScale; Vector2 equipIndicatorPos = new Vector2(Rect.Left, Rect.Center.Y + (Rect.Height / 2 + 15 * Inventory.UIScale) * buttonDir - sizeY / 2f); equipIndicatorPos += DrawOffset; @@ -176,14 +176,6 @@ namespace Barotrauma public static Sprite DraggableIndicator; public static Sprite UnequippedIndicator, UnequippedHoverIndicator, UnequippedClickedIndicator, EquippedIndicator, EquippedHoverIndicator, EquippedClickedIndicator; - public static float IndicatorScaleAdjustment - { - get - { - return !GUI.IsFourByThree() ? 0.8f : 0.7f; - } - } - public static Inventory DraggingInventory; public Inventory ReplacedBy; @@ -249,11 +241,11 @@ namespace Barotrauma { itemsInSlot = ParentInventory.GetItemsAt(SlotIndex); } - Tooltip = GetTooltip(Item, itemsInSlot); + Tooltip = GetTooltip(Item, itemsInSlot, Character.Controlled); tooltipDisplayedCondition = (int)Item.ConditionPercentage; } - private RichString GetTooltip(Item item, IEnumerable itemsInSlot) + private static RichString GetTooltip(Item item, IEnumerable itemsInSlot, Character character) { if (item == null) { return null; } @@ -348,10 +340,12 @@ namespace Barotrauma } if (itemsInSlot.Count() > 1) { - string colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; - colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; + toolTip += $"\n‖color:gui.blue‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; + toolTip += $"\n‖color:gui.blue‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; + } + if (item.Prefab.SkillRequirementHints != null && item.Prefab.SkillRequirementHints.Any()) + { + toolTip += item.Prefab.GetSkillRequirementHints(character); } return RichString.Rich(toolTip); } @@ -576,7 +570,7 @@ namespace Barotrauma } } - if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) + if (PlayerInput.PrimaryMouseButtonHeld() && PlayerInput.SecondaryMouseButtonHeld()) { mouseOn = false; } @@ -727,14 +721,7 @@ namespace Barotrauma Rectangle subRect = slot.Rect; Vector2 spacing; - if (GUI.IsFourByThree()) - { - spacing = new Vector2(5 * UIScale, (5 + UnequippedIndicator.size.Y) * UIScale); - } - else - { - spacing = new Vector2(10 * UIScale, (10 + UnequippedIndicator.size.Y) * UIScale); - } + spacing = new Vector2(10 * UIScale, (10 + UnequippedIndicator.size.Y) * UIScale * GUI.AspectRatioAdjustment); int columns = MathHelper.Clamp((int)Math.Floor(Math.Sqrt(itemCapacity)), 1, container.SlotsPerRow); while (itemCapacity / columns * (subRect.Height + spacing.Y) > GameMain.GraphicsHeight * 0.5f) @@ -1535,16 +1522,6 @@ namespace Barotrauma { Sprite slotSprite = slot.SlotSprite ?? SlotSpriteSmall; - /*if (inventory != null && (CharacterInventory.PersonalSlots.HasFlag(type) || (inventory.isSubInventory && (inventory.Owner as Item) != null - && (inventory.Owner as Item).AllowedSlots.Any(a => CharacterInventory.PersonalSlots.HasFlag(a))))) - { - slotColor = slot.IsHighlighted ? GUIStyle.EquipmentSlotColor : GUIStyle.EquipmentSlotColor * 0.8f; - } - else - { - slotColor = slot.IsHighlighted ? GUIStyle.InventorySlotColor : GUIStyle.InventorySlotColor * 0.8f; - }*/ - if (inventory != null && inventory.Locked) { slotColor = Color.Gray * 0.5f; } spriteBatch.Draw(slotSprite.Texture, rect, slotSprite.SourceRect, slotColor); @@ -1731,7 +1708,17 @@ namespace Barotrauma slot.InventoryKeyIndex < GameSettings.CurrentConfig.InventoryKeyMap.Bindings.Length) { spriteBatch.Draw(slotHotkeySprite.Texture, rect.ScaleSize(1.15f), slotHotkeySprite.SourceRect, slotColor); - GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name, Color.Black, font: GUIStyle.HotkeyFont); + + GUIStyle.HotkeyFont.DrawString( + spriteBatch, + GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name, + rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), + Color.Black, + rotation: 0.0f, + origin: Vector2.Zero, + scale: Vector2.One * GUI.AspectRatioAdjustment, + SpriteEffects.None, + layerDepth: 0.0f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 684cb0d5c..0f85d846d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -23,7 +23,21 @@ namespace Barotrauma private readonly List activeEditors = new List(); - public GUIComponentStyle IconStyle { get; private set; } = null; + + private GUIComponentStyle iconStyle; + public GUIComponentStyle IconStyle + { + get { return iconStyle; } + private set + { + if (IconStyle != value) + { + iconStyle = value; + CheckIsHighlighted(); + } + } + } + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) { if (interactionType == CampaignMode.InteractionType.None) @@ -143,6 +157,18 @@ namespace Barotrauma return color; } + protected override void CheckIsHighlighted() + { + if (IsHighlighted || ExternalHighlight || IconStyle != null) + { + highlightedEntities.Add(this); + } + else + { + highlightedEntities.Remove(this); + } + } + public Color GetInventoryIconColor() { Color color = InventoryIconColor; @@ -281,7 +307,8 @@ namespace Barotrauma cachedVisibleExtents = extents = new Rectangle(min.ToPoint(), max.ToPoint()); } - Vector2 worldPosition = WorldPosition; + Vector2 worldPosition = WorldPosition + GetCollapseEffectOffset(); + 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; } @@ -310,7 +337,9 @@ namespace Barotrauma BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; - Vector2 drawOffset = Vector2.Zero; + Vector2 drawOffset = GetCollapseEffectOffset(); + drawOffset.Y = -drawOffset.Y; + if (displayCondition < MaxCondition) { for (int i = 0; i < Prefab.BrokenSprites.Length; i++) @@ -426,6 +455,8 @@ namespace Barotrauma var holdable = GetComponent(); if (holdable != null && holdable.Picker?.AnimController != null) { + //don't draw the item on hands if it's also being worn + if (GetComponent() is { IsActive: true }) { return; } if (!back) { return; } float depthStep = 0.000001f; if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == this) @@ -466,12 +497,12 @@ 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; } - var ca = (float)Math.Cos(-body.Rotation); - var sa = (float)Math.Sin(-body.Rotation); + var ca = MathF.Cos(-body.DrawRotation); + var sa = MathF.Sin(-body.DrawRotation); Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + transformedOffset.X, -(DrawPosition.Y + transformedOffset.Y)), color, - -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), color, + -body.DrawRotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } } @@ -728,7 +759,7 @@ namespace Barotrauma if (!lClick && !rClick) { return; } Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - var otherEntity = mapEntityList.FirstOrDefault(e => e != this && e.IsHighlighted && e.IsMouseOn(position)); + var otherEntity = highlightedEntities.FirstOrDefault(e => e != this && e.IsMouseOn(position)); if (otherEntity != null) { if (linkedTo.Contains(otherEntity)) @@ -1385,8 +1416,9 @@ namespace Barotrauma } break; case EventType.Status: + bool loadingRound = msg.ReadBoolean(); float newCondition = msg.ReadSingle(); - SetCondition(newCondition, isNetworkEvent: true); + SetCondition(newCondition, isNetworkEvent: true, executeEffects: !loadingRound); break; case EventType.AssignCampaignInteraction: CampaignInteractionType = (CampaignMode.InteractionType)msg.ReadByte(); @@ -1672,25 +1704,24 @@ namespace Barotrauma bool hasIdCard = msg.ReadBoolean(); string ownerName = "", ownerTags = ""; int ownerBeardIndex = -1, ownerHairIndex = -1, ownerMoustacheIndex = -1, ownerFaceAttachmentIndex = -1; - Color ownerHairColor = Microsoft.Xna.Framework.Color.White, - ownerFacialHairColor = Microsoft.Xna.Framework.Color.White, - ownerSkinColor = Microsoft.Xna.Framework.Color.White; + Color ownerHairColor = Color.White, + ownerFacialHairColor = Color.White, + ownerSkinColor = Color.White; Identifier ownerJobId = Identifier.Empty; Vector2 ownerSheetIndex = Vector2.Zero; + int submarineSpecificId = 0; if (hasIdCard) { + submarineSpecificId = msg.ReadInt32(); ownerName = msg.ReadString(); - ownerTags = msg.ReadString(); - + ownerTags = msg.ReadString(); ownerBeardIndex = msg.ReadByte() - 1; ownerHairIndex = msg.ReadByte() - 1; ownerMoustacheIndex = msg.ReadByte() - 1; - ownerFaceAttachmentIndex = msg.ReadByte() - 1; - + ownerFaceAttachmentIndex = msg.ReadByte() - 1; ownerHairColor = msg.ReadColorR8G8B8(); ownerFacialHairColor = msg.ReadColorR8G8B8(); - ownerSkinColor = msg.ReadColorR8G8B8(); - + ownerSkinColor = msg.ReadColorR8G8B8(); ownerJobId = msg.ReadIdentifier(); int x = msg.ReadByte(); @@ -1794,6 +1825,7 @@ namespace Barotrauma } foreach (IdCard idCard in item.GetComponents()) { + idCard.SubmarineSpecificID = submarineSpecificId; idCard.TeamID = (CharacterTeamType)teamID; idCard.OwnerName = ownerName; idCard.OwnerTags = ownerTags; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 17cd3e848..49c3eb0e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -254,7 +254,7 @@ namespace Barotrauma if (!DefaultPrice.RequiresUnlock) { return true; } return Character.Controlled is not null && Character.Controlled.HasStoreAccessForItem(this); } - public LocalizedString GetTooltip() + public LocalizedString GetTooltip(Character character) { LocalizedString tooltip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{Name}‖color:end‖"; if (!Description.IsNullOrEmpty()) @@ -265,6 +265,10 @@ namespace Barotrauma { Wearable.AddTooltipInfo(wearableDamageModifiers, wearableSkillModifiers, ref tooltip); } + if (SkillRequirementHints != null && SkillRequirementHints.Any()) + { + tooltip += GetSkillRequirementHints(character); + } return tooltip; } @@ -376,5 +380,31 @@ namespace Barotrauma Sprite.DrawTiled(spriteBatch, new Vector2(placeRect.X, -placeRect.Y), placeRect.Size.ToVector2(), SpriteColor * 0.8f); } } + + public LocalizedString GetSkillRequirementHints(Character character) + { + LocalizedString text = ""; + if (SkillRequirementHints != null && SkillRequirementHints.Any() && character != null) + { + Color orange = GUIStyle.Orange; + // Reuse an existing, localized, text because it's what we want here: "Required skills:" + text = "\n\n" + $"‖color:{orange.ToStringHex()}‖{TextManager.Get("requiredrepairskills")}‖color:end‖"; + foreach (var hint in SkillRequirementHints) + { + int skillLevel = (int)character.GetSkillLevel(hint.Skill); + Color levelColor = GUIStyle.Yellow; + if (skillLevel >= hint.Level) + { + levelColor = GUIStyle.Green; + } + else if (skillLevel < hint.Level / 2) + { + levelColor = GUIStyle.Red; + } + text += "\n" + hint.GetFormattedText(skillLevel, levelColor.ToStringHex()); + } + } + return text; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 77189d2d9..362dafa31 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Linq; namespace Barotrauma { @@ -320,5 +321,45 @@ namespace Barotrauma return IsHorizontal ? rect.Height : rect.Width; } } + + public override void UpdateEditing(Camera cam, float deltaTime) + { + if (editingHUD == null || editingHUD.UserData != this) + { + editingHUD = CreateEditingHUD(); + } + } + private GUIComponent CreateEditingHUD(bool inGame = false) + { + + editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.15f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) + { + UserData = this + }; + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), editingHUD.RectTransform, Anchor.Center)) + { + Stretch = true, + AbsoluteSpacing = (int)(GUI.Scale * 5) + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("entityname.gap"), font: GUIStyle.LargeFont); + var hiddenInGameTickBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), paddedFrame.RectTransform), TextManager.Get("sp.hiddeningame.name")) + { + Selected = HiddenInGame + }; + hiddenInGameTickBox.OnSelected += (GUITickBox tickbox) => + { + HiddenInGame = tickbox.Selected; + return true; + }; + editingHUD.RectTransform.Resize(new Point( + editingHUD.Rect.Width, + (int)(paddedFrame.Children.Sum(c => c.Rect.Height + paddedFrame.AbsoluteSpacing) / paddedFrame.RectTransform.RelativeSize.Y * 1.25f))); + + PositionEditingHUD(); + + return editingHUD; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 095195834..5eb9bcd95 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -129,20 +129,15 @@ namespace Barotrauma Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in HighlightedEntities) { - if (entity == this || !entity.IsHighlighted) { continue; } + if (entity == this) { continue; } if (!entity.IsMouseOn(position)) { continue; } if (entity.linkedTo == null || !entity.Linkable) { continue; } if (entity.linkedTo.Contains(this) || linkedTo.Contains(entity) || rClick) { - if (entity == this || !entity.IsHighlighted) { continue; } - if (!entity.IsMouseOn(position)) { continue; } - if (entity.linkedTo.Contains(this)) - { - entity.linkedTo.Remove(this); - linkedTo.Remove(entity); - } + entity.linkedTo.Remove(this); + linkedTo.Remove(entity); } else { @@ -329,13 +324,13 @@ namespace Barotrauma } - /*GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f); + GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f); for (int i = 0; i < waveY.Length - 1; i++) { GUI.DrawLine(spriteBatch, new Vector2(drawRect.X + WaveWidth * i, -WorldSurface - waveY[i] - 10), new Vector2(drawRect.X + WaveWidth * (i + 1), -WorldSurface - waveY[i + 1] - 10), Color.Blue * 0.5f); - }*/ + } } foreach (MapEntity e in linkedTo) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 67a6e6fa2..38299827b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -114,7 +114,11 @@ namespace Barotrauma graphics.Clear(BackgroundColor); - renderer?.DrawBackground(spriteBatch, cam, LevelObjectManager, backgroundCreatureManager); + if (renderer != null) + { + GameMain.LightManager.AmbientLight = GameMain.LightManager.AmbientLight.Add(renderer.FlashColor); + renderer?.DrawBackground(spriteBatch, cam, LevelObjectManager, backgroundCreatureManager); + } } public void DrawFront(SpriteBatch spriteBatch, Camera cam) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index f5fc36a94..745af094f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -22,7 +22,7 @@ namespace Barotrauma public float CurrentRotation; - private List spriteDeformations = new List(); + private readonly List spriteDeformations = new List(); public Vector2 CurrentScale { @@ -86,6 +86,8 @@ namespace Barotrauma private set; } + public bool CanBeVisible { get; private set; } + partial void InitProjSpecific() { Sprite?.EnsureLazyLoaded(); @@ -156,6 +158,11 @@ namespace Barotrauma { SonarRadius = Triggers.Select(t => t.ColliderRadius * 1.5f).Max(); } + + CanBeVisible = + Sprite != null || + Prefab.DeformableSprite != null || + Prefab.OverrideProperties.Any(p => p != null && (p.Sprites.Any() || p.DeformableSprite != null)); } public void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index c123c22f0..d23d0b542 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -56,63 +56,84 @@ namespace Barotrauma float minSizeToDraw = MathHelper.Lerp(10.0f, 5.0f, Math.Min(zoom * 20.0f, 1.0f)); + //start from the grid cell at the center of the view + //(if objects needs to be culled, better to cull at the edges of the view) + int midIndexX = (currentIndices.X + currentIndices.Width) / 2; + int midIndexY = (currentIndices.Y + currentIndices.Height) / 2; + CheckIndex(midIndexX, midIndexY); + for (int x = currentIndices.X; x <= currentIndices.Width; x++) { for (int y = currentIndices.Y; y <= currentIndices.Height; y++) { - if (objectGrid[x, y] == null) { continue; } - foreach (LevelObject obj in objectGrid[x, y]) + if (x != midIndexX || y != midIndexY) { CheckIndex(x, y); } + } + } + + void CheckIndex(int x, int y) + { + if (objectGrid[x, y] == null) { return; } + foreach (LevelObject obj in objectGrid[x, y]) + { + if (!obj.CanBeVisible) { continue; } + if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } + + if (zoom < 0.05f) { - if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } - - if (zoom < 0.05f) + //hide if the sprite is very small when zoomed this far out + if ((obj.Sprite != null && Math.Min(obj.Sprite.size.X * zoom, obj.Sprite.size.Y * zoom) < 5.0f) || + (obj.ActivePrefab?.DeformableSprite != null && Math.Min(obj.ActivePrefab.DeformableSprite.Sprite.size.X * zoom, obj.ActivePrefab.DeformableSprite.Sprite.size.Y * zoom) < minSizeToDraw)) { - //hide if the sprite is very small when zoomed this far out - if ((obj.Sprite != null && Math.Min(obj.Sprite.size.X * zoom, obj.Sprite.size.Y * zoom) < 5.0f) || - (obj.ActivePrefab?.DeformableSprite != null && Math.Min(obj.ActivePrefab.DeformableSprite.Sprite.size.X * zoom, obj.ActivePrefab.DeformableSprite.Sprite.size.Y * zoom) < minSizeToDraw)) - { - continue; - } - - float zCutoff = MathHelper.Lerp(5000.0f, 500.0f, (0.05f - zoom) * 20.0f); - if (obj.Position.Z > zCutoff) - { - continue; - } + continue; } - var objectList = - obj.Position.Z >= 0 ? - visibleObjectsBack : - (obj.Position.Z < -1 ? visibleObjectsFront : visibleObjectsMid); - int drawOrderIndex = 0; - for (int i = 0; i < objectList.Count; i++) + float zCutoff = MathHelper.Lerp(5000.0f, 500.0f, (0.05f - zoom) * 20.0f); + if (obj.Position.Z > zCutoff) { - if (objectList[i] == obj) - { - drawOrderIndex = -1; - break; - } + continue; + } + } - if (objectList[i].Position.Z < obj.Position.Z) - { - break; - } - else - { - drawOrderIndex = i + 1; - } + var objectList = + obj.Position.Z >= 0 ? + visibleObjectsBack : + (obj.Position.Z < -1 ? visibleObjectsFront : visibleObjectsMid); + if (objectList.Count >= MaxVisibleObjects) { continue; } + + int drawOrderIndex = 0; + for (int i = 0; i < objectList.Count; i++) + { + if (objectList[i] == obj) + { + drawOrderIndex = -1; + break; } - if (drawOrderIndex >= 0) + if (objectList[i].Position.Z > obj.Position.Z) { - objectList.Insert(drawOrderIndex, obj); - if (objectList.Count >= MaxVisibleObjects) { break; } + break; } + else + { + drawOrderIndex = i + 1; + if (drawOrderIndex >= MaxVisibleObjects) { break; } + } + } + + if (drawOrderIndex >= 0 && drawOrderIndex < MaxVisibleObjects) + { + objectList.Insert(drawOrderIndex, obj); } } } + //object grid is sorted in an ascending order + //(so we prefer the objects in the foreground instead of ones in the background if some need to be culled) + //rendering needs to be done in a descending order though to get the background objects to be drawn first -> reverse the lists + visibleObjectsBack.Reverse(); + visibleObjectsMid.Reverse(); + visibleObjectsFront.Reverse(); + currentGridIndices = currentIndices; } @@ -144,14 +165,14 @@ namespace Barotrauma { Rectangle indices = Rectangle.Empty; indices.X = (int)Math.Floor(cam.WorldView.X / (float)GridSize); - if (indices.X >= objectGrid.GetLength(0)) return; + if (indices.X >= objectGrid.GetLength(0)) { return; } indices.Y = (int)Math.Floor((cam.WorldView.Y - cam.WorldView.Height - Level.Loaded.BottomPos) / (float)GridSize); - if (indices.Y >= objectGrid.GetLength(1)) return; + if (indices.Y >= objectGrid.GetLength(1)) { return; } indices.Width = (int)Math.Floor(cam.WorldView.Right / (float)GridSize) + 1; - if (indices.Width < 0) return; + if (indices.Width < 0) { return; } indices.Height = (int)Math.Floor((cam.WorldView.Y - Level.Loaded.BottomPos) / (float)GridSize) + 1; - if (indices.Height < 0) return; + if (indices.Height < 0) { return; } indices.X = Math.Max(indices.X, 0); indices.Y = Math.Max(indices.Y, 0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index f92c7a30f..9366776a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -1,4 +1,4 @@ -using FarseerPhysics; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -71,9 +71,12 @@ namespace Barotrauma { private static BasicEffect wallEdgeEffect, wallCenterEffect; - private Vector2 dustOffset; - private Vector2 defaultDustVelocity; - private Vector2 dustVelocity; + private Vector2 waterParticleOffset; + private Vector2 waterParticleVelocity; + + private float flashCooldown; + private float flashTimer; + public Color FlashColor { get; private set; } private readonly RasterizerState cullNone; @@ -81,10 +84,26 @@ namespace Barotrauma private readonly List vertexBuffers = new List(); + private float chromaticAberrationStrength; + public float ChromaticAberrationStrength + { + get { return chromaticAberrationStrength; } + set { chromaticAberrationStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } + } + public float CollapseEffectStrength + { + get; + set; + } + public Vector2 CollapseEffectOrigin + { + get; + set; + } + + public LevelRenderer(Level level) { - defaultDustVelocity = Vector2.UnitY * 10.0f; - cullNone = new RasterizerState() { CullMode = CullMode.None }; if (wallEdgeEffect == null) @@ -120,12 +139,50 @@ namespace Barotrauma level.GenerationParams.WallSprite.ReloadTexture(); wallCenterEffect.Texture = level.GenerationParams.WallSprite.Texture; } + + public void Flash() + { + flashTimer = 1.0f; + } public void Update(float deltaTime, Camera cam) { + if (CollapseEffectStrength > 0.0f) + { + CollapseEffectStrength = Math.Max(0.0f, CollapseEffectStrength - deltaTime); + } + if (ChromaticAberrationStrength > 0.0f) + { + ChromaticAberrationStrength = Math.Max(0.0f, ChromaticAberrationStrength - deltaTime * 10.0f); + } + + if (level.GenerationParams.FlashInterval.Y > 0) + { + flashCooldown -= deltaTime; + if (flashCooldown <= 0.0f) + { + flashTimer = 1.0f; + if (level.GenerationParams.FlashSound != null) + { + level.GenerationParams.FlashSound.Play(1.0f, "default"); + } + flashCooldown = Rand.Range(level.GenerationParams.FlashInterval.X, level.GenerationParams.FlashInterval.Y, Rand.RandSync.Unsynced); + } + if (flashTimer > 0.0f) + { + float brightness = flashTimer * 1.1f - PerlinNoise.GetPerlin((float)Timing.TotalTime, (float)Timing.TotalTime * 0.66f) * 0.1f; + FlashColor = level.GenerationParams.FlashColor.Multiply(MathHelper.Clamp(brightness, 0.0f, 1.0f)); + flashTimer -= deltaTime * 0.5f; + } + else + { + FlashColor = Color.TransparentBlack; + } + } + //calculate the sum of the forces of nearby level triggers - //and use it to move the dust texture and water distortion effect - Vector2 currentDustVel = defaultDustVelocity; + //and use it to move the water texture and water distortion effect + Vector2 currentWaterParticleVel = level.GenerationParams.WaterParticleVelocity; foreach (LevelObject levelObject in level.LevelObjectManager.GetVisibleObjects()) { if (levelObject.Triggers == null) { continue; } @@ -139,21 +196,21 @@ namespace Barotrauma objectMaxFlow = vel; } } - currentDustVel += objectMaxFlow; + currentWaterParticleVel += objectMaxFlow; } + + waterParticleVelocity = Vector2.Lerp(waterParticleVelocity, currentWaterParticleVel, deltaTime); - dustVelocity = Vector2.Lerp(dustVelocity, currentDustVel, deltaTime); - - WaterRenderer.Instance?.ScrollWater(dustVelocity, deltaTime); + WaterRenderer.Instance?.ScrollWater(waterParticleVelocity, deltaTime); if (level.GenerationParams.WaterParticles != null) { Vector2 waterTextureSize = level.GenerationParams.WaterParticles.size * level.GenerationParams.WaterParticleScale; - dustOffset += new Vector2(dustVelocity.X, -dustVelocity.Y) * level.GenerationParams.WaterParticleScale * deltaTime; - while (dustOffset.X <= -waterTextureSize.X) dustOffset.X += waterTextureSize.X; - while (dustOffset.X >= waterTextureSize.X) dustOffset.X -= waterTextureSize.X; - while (dustOffset.Y <= -waterTextureSize.Y) dustOffset.Y += waterTextureSize.Y; - while (dustOffset.Y >= waterTextureSize.Y) dustOffset.Y -= waterTextureSize.Y; + waterParticleOffset += new Vector2(waterParticleVelocity.X, -waterParticleVelocity.Y) * level.GenerationParams.WaterParticleScale * deltaTime; + while (waterParticleOffset.X <= -waterTextureSize.X) { waterParticleOffset.X += waterTextureSize.X; } + while (waterParticleOffset.X >= waterTextureSize.X){ waterParticleOffset.X -= waterTextureSize.X; } + while (waterParticleOffset.Y <= -waterTextureSize.Y) { waterParticleOffset.Y += waterTextureSize.Y; } + while (waterParticleOffset.Y >= waterTextureSize.Y) { waterParticleOffset.Y -= waterTextureSize.Y; } } } @@ -234,7 +291,7 @@ namespace Barotrauma Rectangle srcRect = new Rectangle(0, 0, 2048, 2048); Vector2 origin = new Vector2(cam.WorldView.X, -cam.WorldView.Y); - Vector2 offset = -origin + dustOffset; + Vector2 offset = -origin + waterParticleOffset; while (offset.X <= -srcRect.Width * textureScale) offset.X += srcRect.Width * textureScale; while (offset.X > 0.0f) offset.X -= srcRect.Width * textureScale; while (offset.Y <= -srcRect.Height * textureScale) offset.Y += srcRect.Height * textureScale; @@ -261,7 +318,7 @@ namespace Barotrauma level.GenerationParams.WaterParticles.DrawTiled( spriteBatch, origin + offsetS, new Vector2(cam.WorldView.Width - offsetS.X, cam.WorldView.Height - offsetS.Y), - color: Color.White * alpha, textureScale: new Vector2(texScale)); + color: level.GenerationParams.WaterParticleColor * alpha, textureScale: new Vector2(texScale)); } } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 27b1aaf28..3be5faa17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -1,8 +1,6 @@ -using Barotrauma.Extensions; -using Barotrauma.Items.Components; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using SharpFont; using System; using System.Collections.Generic; using System.Diagnostics; @@ -12,28 +10,14 @@ namespace Barotrauma.Lights { class ConvexHullList { - private List list; - public HashSet IsHidden; public readonly Submarine Submarine; - public List List - { - get { return list; } - set - { - Debug.Assert(value != null); - Debug.Assert(!list.Contains(null)); - list = value; - IsHidden.RemoveWhere(ch => !list.Contains(ch)); - } - } - + public HashSet IsHidden = new HashSet(); + public readonly List List = new List(); public ConvexHullList(Submarine submarine) { Submarine = submarine; - list = new List(); - IsHidden = new HashSet(); } } @@ -47,13 +31,13 @@ namespace Barotrauma.Lights public bool IsHorizontal; public bool IsAxisAligned; + public Vector2 SubmarineDrawPos; + public Segment(SegmentPoint start, SegmentPoint end, ConvexHull convexHull) { if (start.Pos.Y > end.Pos.Y) { - var temp = start; - start = end; - end = temp; + (end, start) = (start, end); } Start = start; @@ -102,14 +86,15 @@ namespace Barotrauma.Lights private readonly Segment[] segments = new Segment[4]; private readonly SegmentPoint[] vertices = new SegmentPoint[4]; - private readonly SegmentPoint[] losVertices = new SegmentPoint[4]; - private readonly VectorPair[] losOffsets = new VectorPair[4]; - - private readonly bool[] backFacing; - private readonly bool[] ignoreEdge; + private readonly SegmentPoint[] losVertices = new SegmentPoint[2]; + private readonly Vector2[] losOffsets = new Vector2[2]; private readonly bool isHorizontal; + private readonly int thickness; + + public bool IsExteriorWall; + public VertexPositionColor[] ShadowVertices { get; private set; } public VertexPositionTexture[] PenumbraVertices { get; private set; } public int ShadowVertexCount { get; private set; } @@ -145,47 +130,27 @@ namespace Barotrauma.Lights public Rectangle BoundingBox { get; private set; } - public ConvexHull(Vector2[] points, Color color, MapEntity parent) + public ConvexHull(Rectangle rect, bool? isHorizontal, MapEntity parent) { - if (shadowEffect == null) - { - shadowEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) { VertexColorEnabled = true }; - } - if (penumbraEffect == null) - { - penumbraEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + penumbraEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) { TextureEnabled = true, LightingEnabled = false, Texture = TextureLoader.FromFile("Content/Lights/penumbra.png") }; - } ParentEntity = parent; ShadowVertices = new VertexPositionColor[6 * 4]; PenumbraVertices = new VertexPositionTexture[6 * 4]; - backFacing = new bool[4]; - ignoreEdge = new bool[4]; + BoundingBox = rect; - float minX = points[0].X, minY = points[0].Y, maxX = points[0].X, maxY = points[0].Y; - - for (int i = 1; i < vertices.Length; i++) - { - if (points[i].X < minX) minX = points[i].X; - if (points[i].Y < minY) minY = points[i].Y; - - if (points[i].X > maxX) maxX = points[i].X; - if (points[i].Y > minY) maxY = points[i].Y; - } - - BoundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); - - isHorizontal = BoundingBox.Width > BoundingBox.Height; + this.isHorizontal = isHorizontal ?? BoundingBox.Width > BoundingBox.Height; if (ParentEntity is Structure structure) { System.Diagnostics.Debug.Assert(!structure.Removed); @@ -198,8 +163,26 @@ namespace Barotrauma.Lights if (door != null) { isHorizontal = door.IsHorizontal; } } - SetVertices(points); - + Vector2[] verts = new Vector2[] + { + new Vector2(rect.X, rect.Bottom), + new Vector2(rect.Right, rect.Bottom), + new Vector2(rect.Right, rect.Y), + new Vector2(rect.X, rect.Y), + }; + + Vector2[] losVerts; + if (this.isHorizontal) + { + thickness = rect.Height; + losVerts = new Vector2[] { new Vector2(rect.X, rect.Center.Y), new Vector2(rect.Right, rect.Center.Y) }; + } + else + { + thickness = rect.Width; + losVerts = new Vector2[] { new Vector2(rect.Center.X, rect.Y), new Vector2(rect.Center.X, rect.Bottom) }; + } + SetVertices(verts, losVerts); Enabled = true; var chList = HullLists.Find(h => h.Submarine == parent.Submarine); @@ -211,257 +194,72 @@ namespace Barotrauma.Lights foreach (ConvexHull ch in chList.List) { - MergeOverlappingSegments(ch); - ch.MergeOverlappingSegments(this); + MergeLosVertices(ch); + ch.MergeLosVertices(this); } chList.List.Add(this); } - private void MergeOverlappingSegments(ConvexHull ch) + private void MergeLosVertices(ConvexHull ch, bool refreshOtherOverlappingHulls = true) { if (ch == this) { return; } - if (isHorizontal == ch.isHorizontal) + //hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces) + float mergeDist = MathHelper.Clamp(ch.thickness * 0.55f, 16, 512); + mergeDist = Math.Min(mergeDist, Vector2.Distance(losVertices[0].Pos, losVertices[1].Pos) / 2); + + float mergeDistSqr = mergeDist * mergeDist; + + bool changed = false; + for (int i = 0; i < losVertices.Length; i++) { - //hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces) - float mergeDist = 16; - float mergeDistSqr = mergeDist * mergeDist; + //find the closest point on the other convex hull segment + Vector2 closest = MathUtils.GetClosestPointOnLineSegment( + ch.losVertices[0].Pos + ch.losOffsets[0], + ch.losVertices[1].Pos + ch.losOffsets[1], + losVertices[i].Pos); + if (Vector2.DistanceSquared(closest, losVertices[i].Pos) > mergeDistSqr) { continue; } - Rectangle intersection = Rectangle.Intersect(BoundingBox, ch.BoundingBox); - int intersectionArea = intersection.Width * intersection.Height; - int bboxArea = BoundingBox.Width * BoundingBox.Height; - int otherBboxArea = ch.BoundingBox.Width * ch.BoundingBox.Height; - if (Math.Abs(intersectionArea - bboxArea) < mergeDistSqr) { return; } - if (Math.Abs(intersectionArea - otherBboxArea) < mergeDistSqr) { return; } - - for (int i = 0; i < segments.Length; i++) + //find where the segments would intersect if they had infinite length + // if it's close to the closest point, let's use that instead to keep + // the direction of the segment unchanged (i.e. vertical segment stays vertical) + if (MathUtils.GetLineIntersection( + ch.losVertices[0].Pos + ch.losOffsets[0], + ch.losVertices[1].Pos + ch.losOffsets[1], + losVertices[0].Pos, + losVertices[1].Pos, + out Vector2 intersection)) { - for (int j = 0; j < ch.segments.Length; j++) + if (Vector2.DistanceSquared(intersection, losVertices[i].Pos) < mergeDistSqr || + Vector2.DistanceSquared(intersection, closest) < 16.0f * 16.0f) { - if (segments[i].IsHorizontal != ch.segments[j].IsHorizontal) { continue; } - if (ignoreEdge[i] || ch.ignoreEdge[j]) { continue; } - - //the segments must be at different sides of the convex hulls to be merged - //(e.g. the right edge of a wall piece and the left edge of another one) - var segment1Center = (segments[i].Start.Pos + segments[i].End.Pos) / 2.0f; - var segment2Center = (ch.segments[j].Start.Pos + ch.segments[j].End.Pos) / 2.0f; - if (Vector2.Dot(segment1Center - BoundingBox.Center.ToVector2(), segment2Center - ch.BoundingBox.Center.ToVector2()) > 0) { continue; } - - if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].Start.Pos) < mergeDistSqr && - Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].End.Pos) < mergeDistSqr) - { - ignoreEdge[i] = true; - ch.ignoreEdge[j] = true; - MergeSegments(segments[i], ch.segments[j], true); - } - else if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].End.Pos) < mergeDistSqr && - Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].Start.Pos) < mergeDistSqr) - { - ignoreEdge[i] = true; - ch.ignoreEdge[j] = true; - MergeSegments(segments[i], ch.segments[j], false); - } - } - } - } - - for (int i = 0; i < segments.Length; i++) - { - if (ignoreEdge[i]) { continue; } - if (Vector2.DistanceSquared(segments[i].Start.Pos, segments[i].End.Pos) < 1.0f) { continue; } - for (int j = 0; j < ch.segments.Length; j++) - { - if (ch.ignoreEdge[j]) { continue; } - if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, ch.segments[j].End.Pos) < 1.0f) { continue; } - if (IsSegmentAInB(segments[i], ch.segments[j])) - { - ignoreEdge[i] = true; - if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, segments[i].Start.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, false, segments[i].End.Pos); - } - else if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, segments[i].End.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, false, segments[i].Start.Pos); - } - - if (Vector2.DistanceSquared(ch.segments[j].End.Pos, segments[i].Start.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, true, segments[i].End.Pos); - } - else if (Vector2.DistanceSquared(ch.segments[j].End.Pos, segments[i].End.Pos) < 4.0f) - { - ch.ShiftSegmentPoint(j, true, segments[i].Start.Pos); - } - } - else if (IsSegmentAInB(ch.segments[j], segments[i])) - { - ch.ignoreEdge[j] = true; - - if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].Start.Pos) < 4.0f) - { - ShiftSegmentPoint(i, false, ch.segments[j].End.Pos); - } - else if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].End.Pos) < 4.0f) - { - ShiftSegmentPoint(i, false, ch.segments[j].Start.Pos); - } - - if (Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].Start.Pos) < 4.0f) - { - ShiftSegmentPoint(i, true, ch.segments[j].End.Pos); - } - else if (Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].End.Pos) < 4.0f) - { - ShiftSegmentPoint(i, true, ch.segments[j].Start.Pos); - } + closest = intersection; } } + + losOffsets[i] = closest - losVertices[i].Pos; + overlappingHulls.Add(ch); + ch.overlappingHulls.Add(this); + changed = true; + } - - //ignore edges that are inside some other convex hull - for (int i = 0; i < vertices.Length; i++) + if (changed && refreshOtherOverlappingHulls) { - if (ch.IsPointInside(vertices[i].Pos)) + foreach (var overlapping in overlappingHulls) { - if (ch.IsPointInside(vertices[(i + 1) % vertices.Length].Pos)) - { - ignoreEdge[i] = true; - overlappingHulls.Add(ch); - } + overlapping.MergeLosVertices(this, refreshOtherOverlappingHulls: false); } } } - private void ShiftSegmentPoint(int segmentIndex, bool end, Vector2 newPos) - { - var segment = segments[segmentIndex]; - - losOffsets[segmentIndex] ??= new VectorPair(); - bool flipped = false; - if (Vector2.DistanceSquared(vertices[segmentIndex].Pos, segment.Start.Pos) > Vector2.DistanceSquared(vertices[segmentIndex].Pos, segment.End.Pos)) - { - flipped = true; - } - if (end == !flipped) - { - losOffsets[segmentIndex].B = newPos; - } - else - { - losOffsets[segmentIndex].A = newPos; - } - } - - public bool IsSegmentAInB(Segment a, Segment b) - { - if (Vector2.DistanceSquared(a.Start.Pos, a.End.Pos) > Vector2.DistanceSquared(b.Start.Pos, b.End.Pos)) - { - return false; - } - - Vector2 min = new Vector2(Math.Min(b.Start.Pos.X, b.End.Pos.X), Math.Min(b.Start.Pos.Y, b.End.Pos.Y)); - Vector2 max = new Vector2(Math.Max(b.Start.Pos.X, b.End.Pos.X), Math.Max(b.Start.Pos.Y, b.End.Pos.Y)); - min.X -= 1.0f; min.Y -= 1.0f; - max.X += 1.0f; max.Y += 1.0f; - - if (a.Start.Pos.X < min.X) { return false; } - if (a.Start.Pos.Y < min.Y) { return false; } - if (a.End.Pos.X < min.X) { return false; } - if (a.End.Pos.Y < min.Y) { return false; } - - if (a.Start.Pos.X > max.X) { return false; } - if (a.Start.Pos.Y > max.Y) { return false; } - if (a.End.Pos.X > max.X) { return false; } - if (a.End.Pos.Y > max.Y) { return false; } - - float startDist = MathUtils.LineToPointDistanceSquared(b.Start.Pos, b.End.Pos, a.Start.Pos); - if (startDist > 1.0f) { return false; } - float endDist = MathUtils.LineToPointDistanceSquared(b.Start.Pos, b.End.Pos, a.End.Pos); - if (endDist > 1.0f) { return false; } - return true; - } - - public bool IsPointInside(Vector2 point) - { - if (!BoundingBox.Contains(point)) { return false; } - - Vector2 center = (vertices[0].Pos + vertices[1].Pos + vertices[2].Pos + vertices[3].Pos) * 0.25f; - for (int i = 0; i < 4; i++) - { - Vector2 segmentVector = vertices[(i + 1) % 4].Pos - vertices[i].Pos; - Vector2 centerToVertex = center - vertices[i].Pos; - Vector2 pointToVertex = point - vertices[i].Pos; - - float dotCenter = Vector2.Dot(centerToVertex, segmentVector); - float dotPoint = Vector2.Dot(pointToVertex, segmentVector); - - if ((dotCenter > 0f && dotPoint < 0f) || (dotCenter < 0f && dotPoint > 0f)) { return false; } - } - - return true; - } - - private void MergeSegments(Segment segment1, Segment segment2, bool startPointsMatch) - { - int startPointIndex = -1, endPointIndex = -1; - for (int i = 0; i < vertices.Length; i++) - { - if (vertices[i].Pos.NearlyEquals(segment1.Start.Pos)) - startPointIndex = i; - else if (vertices[i].Pos.NearlyEquals(segment1.End.Pos)) - endPointIndex = i; - } - if (startPointIndex == -1 || endPointIndex == -1) { return; } - - int startPoint2Index = -1, endPoint2Index = -1; - for (int i = 0; i < segment2.ConvexHull.vertices.Length; i++) - { - if (segment2.ConvexHull.vertices[i].Pos.NearlyEquals(segment2.Start.Pos)) - startPoint2Index = i; - else if (segment2.ConvexHull.vertices[i].Pos.NearlyEquals(segment2.End.Pos)) - endPoint2Index = i; - } - if (startPoint2Index == -1 || endPoint2Index == -1) { return; } - - if (startPointsMatch) - { - losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = - (segment1.Start.Pos + segment2.Start.Pos) / 2.0f; - losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = - (segment1.End.Pos + segment2.End.Pos) / 2.0f; - } - else - { - if (Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.Start.Pos) < - Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.End.Pos)) - { - losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = - (segment1.Start.Pos + segment2.End.Pos) / 2.0f; - losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = - (segment1.End.Pos + segment2.Start.Pos) / 2.0f; - } - else - { - losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = - (segment1.End.Pos + segment2.Start.Pos) / 2.0f; - losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = - (segment1.Start.Pos + segment2.End.Pos) / 2.0f; - } - } - - overlappingHulls.Add(segment2.ConvexHull); - segment2.ConvexHull.overlappingHulls.Add(this); - } - public void Rotate(Vector2 origin, float amount) { Matrix rotationMatrix = Matrix.CreateTranslation(-origin.X, -origin.Y, 0.0f) * Matrix.CreateRotationZ(amount) * Matrix.CreateTranslation(origin.X, origin.Y, 0.0f); - SetVertices(vertices.Select(v => v.Pos).ToArray(), rotationMatrix: rotationMatrix); + SetVertices(vertices.Select(v => v.Pos).ToArray(), losVertices.Select(v => v.Pos).ToArray(), rotationMatrix: rotationMatrix); } private void CalculateDimensions() @@ -470,11 +268,10 @@ namespace Barotrauma.Lights for (int i = 1; i < vertices.Length; i++) { - if (vertices[i].Pos.X < minX) minX = vertices[i].Pos.X; - if (vertices[i].Pos.Y < minY) minY = vertices[i].Pos.Y; - - if (vertices[i].Pos.X > maxX) maxX = vertices[i].Pos.X; - if (vertices[i].Pos.Y > minY) maxY = vertices[i].Pos.Y; + minX = Math.Min(minX, vertices[i].Pos.X); + minY = Math.Min(minY, vertices[i].Pos.Y); + maxX = Math.Max(maxX, vertices[i].Pos.X); + maxY = Math.Max(maxY, vertices[i].Pos.Y); } BoundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); @@ -485,21 +282,17 @@ namespace Barotrauma.Lights for (int i = 0; i < vertices.Length; i++) { vertices[i].Pos += amount; - losVertices[i].Pos += amount; - - losOffsets[i] = null; - segments[i].Start.Pos += amount; segments[i].End.Pos += amount; } + for (int i = 0; i < losVertices.Length; i++) + { + losVertices[i].Pos += amount; + } LastVertexChangeTime = (float)Timing.TotalTime; overlappingHulls.Clear(); - for (int i = 0; i < 4; i++) - { - ignoreEdge[i] = false; - } CalculateDimensions(); @@ -511,8 +304,8 @@ namespace Barotrauma.Lights overlappingHulls.Clear(); foreach (ConvexHull ch in chList.List) { - MergeOverlappingSegments(ch); - ch.MergeOverlappingSegments(this); + MergeLosVertices(ch); + ch.MergeLosVertices(this); } } } @@ -525,23 +318,23 @@ namespace Barotrauma.Lights foreach (ConvexHull ch in chList.List) { ch.overlappingHulls.Clear(); - for (int i = 0; i < 4; i++) + for (int i = 0; i < ch.losOffsets.Length; i++) { - ch.ignoreEdge[i] = false; + ch.losOffsets[i] = Vector2.Zero; } } for (int i = 0; i < chList.List.Count; i++) { for (int j = i + 1; j < chList.List.Count; j++) { - chList.List[i].MergeOverlappingSegments(chList.List[j]); - chList.List[j].MergeOverlappingSegments(chList.List[i]); + chList.List[i].MergeLosVertices(chList.List[j]); + chList.List[j].MergeLosVertices(chList.List[i]); } } } } - public void SetVertices(Vector2[] points, bool mergeOverlappingSegments = true, Matrix? rotationMatrix = null) + public void SetVertices(Vector2[] points, Vector2[] losPoints, bool mergeOverlappingSegments = true, Matrix? rotationMatrix = null) { Debug.Assert(points.Length == 4, "Only rectangular convex hulls are supported"); @@ -549,39 +342,23 @@ namespace Barotrauma.Lights for (int i = 0; i < 4; i++) { - vertices[i] = new SegmentPoint(points[i], this); - losVertices[i] = new SegmentPoint(points[i], this); - losOffsets[i] = null; + vertices[i] = new SegmentPoint(points[i], this); } - - for (int i = 0; i < 4; i++) + for (int i = 0; i < 2; i++) { - ignoreEdge[i] = false; + losVertices[i] = new SegmentPoint(losPoints[i], this); } overlappingHulls.Clear(); - int margin = 0; - if (Math.Abs(points[0].X - points[2].X) < Math.Abs(points[0].Y - points[2].Y)) - { - losVertices[0].Pos = new Vector2(points[0].X + margin, points[0].Y); - losVertices[1].Pos = new Vector2(points[1].X + margin, points[1].Y); - losVertices[2].Pos = new Vector2(points[2].X - margin, points[2].Y); - losVertices[3].Pos = new Vector2(points[3].X - margin, points[3].Y); - } - else - { - losVertices[0].Pos = new Vector2(points[0].X, points[0].Y + margin); - losVertices[1].Pos = new Vector2(points[1].X, points[1].Y - margin); - losVertices[2].Pos = new Vector2(points[2].X, points[2].Y - margin); - losVertices[3].Pos = new Vector2(points[3].X, points[3].Y + margin); - } - if (rotationMatrix.HasValue) { for (int i = 0; i < vertices.Length; i++) { vertices[i].Pos = Vector2.Transform(vertices[i].Pos, rotationMatrix.Value); + } + for (int i = 0; i < losVertices.Length; i++) + { losVertices[i].Pos = Vector2.Transform(losVertices[i].Pos, rotationMatrix.Value); } } @@ -602,7 +379,7 @@ namespace Barotrauma.Lights overlappingHulls.Clear(); foreach (ConvexHull ch in chList.List) { - MergeOverlappingSegments(ch); + MergeLosVertices(ch); } } } @@ -624,31 +401,17 @@ namespace Barotrauma.Lights /// /// Returns the segments that are facing towards viewPosition /// - public void GetVisibleSegments(Vector2 viewPosition, List visibleSegments, bool ignoreEdges) + public void GetVisibleSegments(Vector2 viewPosition, List visibleSegments) { for (int i = 0; i < 4; i++) { - if (ignoreEdge[i] && ignoreEdges) continue; - - Vector2 pos1 = vertices[i].WorldPos; - Vector2 pos2 = vertices[(i + 1) % 4].WorldPos; - - Vector2 middle = (pos1 + pos2) / 2; - - Vector2 L = viewPosition - middle; - - Vector2 N = new Vector2( - -(pos2.Y - pos1.Y), - pos2.X - pos1.X); - - if (Vector2.Dot(N, L) > 0) + if (IsSegmentFacing(vertices[i].WorldPos, vertices[(i + 1) % 4].WorldPos, viewPosition)) { visibleSegments.Add(segments[i]); } } } - public void RefreshWorldPositions() { for (int i = 0; i < 4; i++) @@ -676,34 +439,12 @@ namespace Barotrauma.Lights ShadowVertexCount = 0; - //compute facing of each edge, using N*L - for (int i = 0; i < 4; i++) + for (int i = 0; i < losVertices.Length; i++) { - if (ignoreEdge[i]) - { - backFacing[i] = false; - continue; - } - - Vector2 firstVertex = losVertices[i].Pos; - Vector2 secondVertex = losVertices[(i+1) % 4].Pos; - - Vector2 L = lightSourcePos - ((firstVertex + secondVertex) / 2.0f); - - Vector2 N = new Vector2( - -(secondVertex.Y - firstVertex.Y), - secondVertex.X - firstVertex.X); - - backFacing[i] = (Vector2.Dot(N, L) < 0); - } - - ShadowVertexCount = 0; - for (int i = 0; i < 4; i++) - { - if (!backFacing[i]) { continue; } int currentIndex = i; - Vector3 vertexPos0 = new Vector3(losOffsets[currentIndex]?.A ?? losVertices[currentIndex].Pos, 0.0f); - Vector3 vertexPos1 = new Vector3(losOffsets[currentIndex]?.B ?? losVertices[(currentIndex + 1) % 4].Pos, 0.0f); + int nextIndex = (currentIndex + 1) % 2; + Vector3 vertexPos0 = new Vector3(losVertices[currentIndex].Pos + losOffsets[currentIndex], 0.0f); + Vector3 vertexPos1 = new Vector3(losVertices[nextIndex].Pos + losOffsets[nextIndex], 0.0f); if (Vector3.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; } @@ -754,9 +495,24 @@ namespace Barotrauma.Lights ShadowVertexCount += 6; } + if (IsSegmentFacing(losVertices[0].Pos, losVertices[1].Pos, lightSourcePos)) + { + Array.Reverse(ShadowVertices); + } + CalculateLosPenumbraVertices(lightSourcePos); } + private static bool IsSegmentFacing(Vector2 segmentPos1, Vector2 segmentPos2, Vector2 viewPosition) + { + Vector2 segmentMid = (segmentPos1 + segmentPos2) / 2; + Vector2 segmentDiff = segmentPos2 - segmentPos1; + Vector2 segmentNormal = new Vector2(-segmentDiff.Y, segmentDiff.X); + + Vector2 viewDirection = viewPosition - segmentMid; + return Vector2.Dot(segmentNormal, viewDirection) > 0; + } + private void CalculateLosPenumbraVertices(Vector2 lightSourcePos) { Vector3 offset = Vector3.Zero; @@ -766,73 +522,101 @@ namespace Barotrauma.Lights } PenumbraVertexCount = 0; - for (int i = 0; i < 4; i++) + for (int i = 0; i < losVertices.Length; i++) { int currentIndex = i; - int prevIndex = (i + 3) % 4; - int nextIndex = (i + 1) % 4; - bool disjointed = losOffsets[i]?.A != null; - Vector2 vertexPos0 = losOffsets[currentIndex]?.A ?? losVertices[currentIndex].Pos; - Vector2 vertexPos1 = losOffsets[currentIndex]?.B ?? losVertices[nextIndex].Pos; + int nextIndex = (i + 1) % 2; + Vector2 vertexPos0 = losVertices[currentIndex].Pos + losOffsets[currentIndex]; + Vector2 vertexPos1 = losVertices[nextIndex].Pos + losOffsets[nextIndex]; if (Vector2.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; } + + Vector3 penumbraStart = new Vector3(vertexPos0, 0.0f); - if (backFacing[currentIndex] && (disjointed || (!backFacing[prevIndex]))) + PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture { - Vector3 penumbraStart = new Vector3(vertexPos0, 0.0f); + Position = penumbraStart + offset, + TextureCoordinate = new Vector2(0.0f, 1.0f) + }; - PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture - { - Position = penumbraStart + offset, - TextureCoordinate = new Vector2(0.0f, 1.0f) - }; + for (int j = 0; j < 2; j++) + { + PenumbraVertices[PenumbraVertexCount + j + 1] = new VertexPositionTexture(); + Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); + vertexDir.Normalize(); - for (int j = 0; j < 2; j++) - { - PenumbraVertices[PenumbraVertexCount + j + 1] = new VertexPositionTexture(); - Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); - vertexDir.Normalize(); + Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; - Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; + vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) - normal * 20.0f); + vertexDir.Normalize(); + PenumbraVertices[PenumbraVertexCount + j + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) - normal * 20.0f); - vertexDir.Normalize(); - PenumbraVertices[PenumbraVertexCount + j + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - - PenumbraVertices[PenumbraVertexCount + j + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); - } - - PenumbraVertexCount += 3; + PenumbraVertices[PenumbraVertexCount + j + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); } - disjointed = losOffsets[i]?.B != null; - if (backFacing[currentIndex] && (disjointed || (!backFacing[nextIndex]))) + PenumbraVertexCount += 3; + + penumbraStart = new Vector3(vertexPos1, 0.0f); + + PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture { - Vector3 penumbraStart = new Vector3(vertexPos1, 0.0f); + Position = penumbraStart + offset, + TextureCoordinate = new Vector2(0.0f, 1.0f) + }; - PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture - { - Position = penumbraStart + offset, - TextureCoordinate = new Vector2(0.0f, 1.0f) - }; + for (int j = 0; j < 2; j++) + { + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1] = new VertexPositionTexture(); + Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); + vertexDir.Normalize(); - for (int j = 0; j < 2; j++) - { - PenumbraVertices[PenumbraVertexCount + (1 - j) + 1] = new VertexPositionTexture(); - Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); - vertexDir.Normalize(); + Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; - Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; + vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) + normal * 20.0f); + vertexDir.Normalize(); + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) + normal * 20.0f); - vertexDir.Normalize(); - PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; - - PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); - } - - PenumbraVertexCount += 3; + PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); } + + PenumbraVertexCount += 3; + } + } + + public void DebugDraw(SpriteBatch spriteBatch) + { + //RecalculateAll(Submarine.MainSub); + //RefreshWorldPositions(); + + DrawLine(losVertices[0].Pos, losVertices[1].Pos, Color.Gray * 0.5f, width: 3); + DrawLine(losVertices[0].Pos + losOffsets[0], losVertices[1].Pos + losOffsets[1], Color.LightGreen, width: 2); + DrawLine(GameMain.GameScreen.Cam.Position + Vector2.One * 1000, GameMain.GameScreen.Cam.Position - Vector2.One * 1000, Color.Magenta, width: 2); + + if (GameMain.LightManager.LightingEnabled) + { + for (int i = 0; i < vertices.Length; i++) + { + Vector2 start = vertices[i].Pos; + Vector2 end = vertices[(i + 1) % 4].Pos; + DrawLine( + start, + end, Color.Yellow * 0.5f, + width: 4); + } + } + + void DrawLine(Vector2 vertexPos0, Vector2 vertexPos1, Color color, int width) + { + if (ParentEntity != null && ParentEntity.Submarine != null) + { + vertexPos0 += ParentEntity.Submarine.DrawPosition; + vertexPos1 += ParentEntity.Submarine.DrawPosition; + } + Vector2 viewTargetPos = LightManager.ViewTarget.WorldPosition; + float alpha = IsSegmentFacing(vertexPos0, vertexPos1, viewTargetPos) ? 1.0f : 0.5f; + vertexPos0.Y = -vertexPos0.Y; + vertexPos1.Y = -vertexPos1.Y; + GUI.DrawLine(spriteBatch, vertexPos0, vertexPos1, color * alpha, width: width); } } @@ -903,16 +687,13 @@ namespace Barotrauma.Lights { HullLists.Remove(chList); } - foreach (ConvexHull ch2 in overlappingHulls) + //create a new list because MergeLosVertices can edit overlappingHulls + foreach (ConvexHull ch2 in overlappingHulls.ToList()) { - for (int i = 0; i < 4; i++) - { - ch2.ignoreEdge[i] = false; - } ch2.overlappingHulls.Remove(this); foreach (ConvexHull ch in chList.List) { - ch.MergeOverlappingSegments(ch2); + ch.MergeLosVertices(ch2); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index d8314b1b9..95a4d3a07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System; using Barotrauma.Items.Components; using Barotrauma.Extensions; +using System.Threading; namespace Barotrauma.Lights { @@ -22,6 +23,9 @@ namespace Barotrauma.Lights /// const float ObstructLightsBehindCharactersZoomThreshold = 0.5f; + private Thread rayCastThread; + private Queue pendingRayCasts = new Queue(); + public static Entity ViewTarget { get; set; } private float currLightMapScale; @@ -58,6 +62,8 @@ namespace Barotrauma.Lights private readonly List lights; + public bool DebugLos; + public bool LosEnabled = true; public float LosAlpha = 1f; public LosMode LosMode = LosMode.Transparent; @@ -68,6 +74,8 @@ namespace Barotrauma.Lights private readonly Texture2D visionCircle; + private readonly Texture2D gapGlowTexture; + private Vector2 losOffset; private int recalculationCount; @@ -85,8 +93,16 @@ namespace Barotrauma.Lights AmbientLight = new Color(20, 20, 20, 255); + rayCastThread = new Thread(UpdateRayCasts) + { + Name = "LightManager Raycast thread", + IsBackground = true //this should kill the thread if the game crashes + }; + rayCastThread.Start(); + visionCircle = Sprite.LoadTexture("Content/Lights/visioncircle.png"); highlightRaster = Sprite.LoadTexture("Content/UI/HighlightRaster.png"); + gapGlowTexture = Sprite.LoadTexture("Content/Lights/pointlight_rays.png"); GameMain.Instance.ResolutionChanged += () => { @@ -100,15 +116,12 @@ namespace Barotrauma.Lights LosEffect = EffectLoader.Load("Effects/losshader"); SolidColorEffect = EffectLoader.Load("Effects/solidcolor"); - if (lightEffect == null) - { - lightEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + lightEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) { VertexColorEnabled = true, TextureEnabled = true, Texture = LightSource.LightTexture }; - } }); } @@ -155,7 +168,7 @@ namespace Barotrauma.Lights { foreach (LightSource light in lights) { - light.NeedsHullCheck = true; + light.HullsUpToDate.Clear(); light.NeedsRecalculation = true; } } @@ -176,6 +189,51 @@ namespace Barotrauma.Lights } } + private class RayCastTask + { + public LightSource LightSource; + public Vector2 DrawPos; + public float Rotation; + + public RayCastTask(LightSource lightSource, Vector2 drawPos, float rotation) + { + LightSource = lightSource; + DrawPos = drawPos; + Rotation = rotation; + } + + public void Calculate() + { + LightSource.RayCastTask(DrawPos, Rotation); + } + } + + private static readonly object mutex = new object(); + + public void AddRayCastTask(LightSource lightSource, Vector2 drawPos, float rotation) + { + lock (mutex) + { + if (pendingRayCasts.Any(p => p.LightSource == lightSource)) { return; } + pendingRayCasts.Enqueue(new RayCastTask(lightSource, drawPos, rotation)); + } + } + + private void UpdateRayCasts() + { + while (true) + { + lock (mutex) + { + while (pendingRayCasts.Count > 0) + { + pendingRayCasts.Dequeue().Calculate(); + } + } + Thread.Sleep(10); + } + } + public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) { if (!LightingEnabled) { return; } @@ -287,8 +345,8 @@ namespace Barotrauma.Lights foreach (LightSource light in activeLights) { if (!light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; } - light.DrawSprite(spriteBatch, cam); light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount); + light.DrawSprite(spriteBatch, cam); } GameMain.ParticleManager.Draw(spriteBatch, true, null, Particles.ParticleBlendState.Additive); spriteBatch.End(); @@ -307,15 +365,46 @@ namespace Barotrauma.Lights } spriteBatch.End(); - SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; - SolidColorEffect.Parameters["color"].SetValue(AmbientLight.Opaque().ToVector4()); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform, effect: SolidColorEffect); - Submarine.DrawDamageable(spriteBatch, null); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); + Vector3 glowColorHSV = ToolBox.RGBToHSV(AmbientLight); + glowColorHSV.Z = Math.Max(glowColorHSV.Z, 0.4f); + Color glowColor = ToolBox.HSVToRGB(glowColorHSV.X, glowColorHSV.Y, glowColorHSV.Z); + Vector2 glowSpriteSize = new Vector2(gapGlowTexture.Width, gapGlowTexture.Height); + foreach (var gap in Gap.GapList) + { + if (gap.IsRoomToRoom || gap.Open <= 0.0f || gap.ConnectedWall == null) { continue; } + + float a = MathHelper.Lerp(0.5f, 1.0f, + PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.05f, gap.GlowEffectT)); + + float scale = MathHelper.Lerp(0.5f, 2.0f, + PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.01f, gap.GlowEffectT)); + + float rot = PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.001f, gap.GlowEffectT) * MathHelper.TwoPi; + + Vector2 spriteScale = new Vector2(gap.Rect.Width, gap.Rect.Height) / glowSpriteSize; + Vector2 drawPos = new Vector2(gap.DrawPosition.X, -gap.DrawPosition.Y); + + spriteBatch.Draw(gapGlowTexture, + drawPos, + null, + glowColor * a, + rot, + glowSpriteSize / 2, + scale: Math.Max(spriteScale.X, spriteScale.Y) * scale, + SpriteEffects.None, + layerDepth: 0); + } + spriteBatch.End(); + + GameMain.GameScreen.DamageEffect.CurrentTechnique = GameMain.GameScreen.DamageEffect.Techniques["StencilShaderSolidColor"]; + GameMain.GameScreen.DamageEffect.Parameters["solidColor"].SetValue(Color.Black.ToVector4()); + spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, transformMatrix: spriteBatchTransform, effect: GameMain.GameScreen.DamageEffect); + Submarine.DrawDamageable(spriteBatch, GameMain.GameScreen.DamageEffect); spriteBatch.End(); graphics.BlendState = BlendState.Additive; - //draw the focused item and character to highlight them, //and light sprites (done before drawing the actual light volumes so we can make characters obstruct the highlights and sprites) //--------------------------------------------------------------------------------------------------- @@ -449,9 +538,9 @@ namespace Barotrauma.Lights { highlightedEntities.Add(Character.Controlled.FocusedCharacter); } - foreach (Item item in Item.ItemList) + foreach (MapEntity me in MapEntity.HighlightedEntities) { - if ((item.IsHighlighted || item.IconStyle != null) && !highlightedEntities.Contains(item)) + if (me is Item item && item != Character.Controlled.FocusedItem) { highlightedEntities.Add(item); } @@ -565,7 +654,7 @@ namespace Barotrauma.Lights public void UpdateObstructVision(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, Vector2 lookAtPosition) { - if ((!LosEnabled || LosMode == LosMode.None) && !ObstructVision) return; + if ((!LosEnabled || LosMode == LosMode.None) && !ObstructVision) { return; } if (ViewTarget == null) return; graphics.SetRenderTarget(LosTexture); @@ -597,23 +686,30 @@ namespace Barotrauma.Lights if (LosEnabled && LosMode != LosMode.None && ViewTarget != null) { Vector2 pos = ViewTarget.DrawPosition; + if (ViewTarget is Character character && + character.AnimController?.GetLimb(LimbType.Head) is Limb head && + !head.IsSevered && !head.Removed) + { + pos = head.body.DrawPosition; + } Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height); Matrix shadowTransform = cam.ShaderTransform * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f; - var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width*0.75f, ViewTarget.Submarine); + var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width * 0.75f, ViewTarget.Submarine); if (convexHulls != null) { List shadowVerts = new List(); List penumbraVerts = new List(); foreach (ConvexHull convexHull in convexHulls) { - if (!convexHull.Enabled || !convexHull.Intersects(camView)) continue; + if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } + if (LosMode == LosMode.BlockOutsideView && !convexHull.IsExteriorWall) { continue; }; Vector2 relativeLightPos = pos; - if (convexHull.ParentEntity?.Submarine != null) relativeLightPos -= convexHull.ParentEntity.Submarine.Position; + if (convexHull.ParentEntity?.Submarine != null) { relativeLightPos -= convexHull.ParentEntity.Submarine.Position; } convexHull.CalculateLosVertices(relativeLightPos); @@ -646,6 +742,20 @@ namespace Barotrauma.Lights graphics.SetRenderTarget(null); } + public void DebugDrawLos(SpriteBatch spriteBatch, Camera cam) + { + if (ViewTarget == null) { return; } + spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform); + var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width * 0.75f, ViewTarget?.Submarine); + Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height); + foreach (ConvexHull convexHull in convexHulls) + { + if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } + convexHull.DebugDraw(spriteBatch); + } + spriteBatch.End(); + } + public void ClearLights() { lights.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 7f981e74f..d4aa9dd17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -15,6 +15,7 @@ namespace Barotrauma.Lights public bool Persistent; + public Dictionary SerializableProperties { get; private set; } = new Dictionary(); [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), Editable] @@ -52,6 +53,10 @@ namespace Barotrauma.Lights [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] public float Rotation { get; set; } + [Serialize(false, IsPropertySaveable.Yes, "Directional lights only shine in \"one direction\", meaning no shadows are cast behind them."+ + " Note that this does not affect how the light texture is drawn: if you want something like a conical spotlight, you should use an appropriate texture for that.")] + public bool Directional { get; set; } + public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(MathHelper.ToRadians(Rotation))); private float flicker; @@ -203,7 +208,7 @@ namespace Barotrauma.Lights private VertexPositionColorTexture[] vertices; private short[] indices; - private readonly List hullsInRange; + private readonly List convexHullsInRange; public Texture2D texture; @@ -222,9 +227,10 @@ namespace Barotrauma.Lights private float prevCalculatedRange; private Vector2 prevCalculatedPosition; - //do we need to recheck which convex hulls are within range - //(e.g. position or range of the lightsource has changed) - public bool NeedsHullCheck = true; + //Which submarines' convex hulls are up to date? Resets when the item moves/rotates relative to the submarine. + //Can contain null (means convex hulls that aren't part of any submarine). + public HashSet HullsUpToDate = new HashSet(); + //do we need to recalculate the vertices of the light volume private bool needsRecalculation; public bool NeedsRecalculation @@ -234,7 +240,7 @@ namespace Barotrauma.Lights { if (!needsRecalculation && value) { - foreach (ConvexHullList chList in hullsInRange) + foreach (ConvexHullList chList in convexHullsInRange) { chList.IsHidden.Clear(); } @@ -246,6 +252,18 @@ namespace Barotrauma.Lights //when were the vertices of the light volume last calculated public float LastRecalculationTime { get; private set; } + + private enum LightVertexState + { + UpToDate, + PendingRayCasts, + PendingVertexRecalculation, + } + + private LightVertexState state; + + private Vector2 calculatedDrawPos; + private readonly Dictionary diffToSub; private DynamicVertexBuffer lightVolumeBuffer; @@ -254,7 +272,6 @@ namespace Barotrauma.Lights private int indexCount; private Vector2 translateVertices; - private float rotateVertices; private readonly LightSourceParams lightSourceParams; @@ -277,7 +294,7 @@ namespace Barotrauma.Lights return; } - NeedsHullCheck = true; + HullsUpToDate.Clear(); NeedsRecalculation = true; } } @@ -292,17 +309,20 @@ namespace Barotrauma.Lights if (Math.Abs(value - rotation) < 0.001f) { return; } rotation = value; + dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation)); + if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null) { - rotateVertices = rotation - prevCalculatedRotation; return; } - NeedsHullCheck = true; + HullsUpToDate.Clear(); NeedsRecalculation = true; } } + private Vector2 dir = Vector2.UnitX; + private Vector2 _spriteScale = Vector2.One; public Vector2 SpriteScale @@ -368,7 +388,7 @@ namespace Barotrauma.Lights lightSourceParams.Range = value; if (Math.Abs(prevCalculatedRange - lightSourceParams.Range) < 10.0f) return; - NeedsHullCheck = true; + HullsUpToDate.Clear(); NeedsRecalculation = true; prevCalculatedRange = lightSourceParams.Range; } @@ -384,8 +404,8 @@ namespace Barotrauma.Lights set { NeedsRecalculation = true; - NeedsHullCheck = true; lightTextureTargetSize = value; + HullsUpToDate.Clear(); } } @@ -424,7 +444,7 @@ namespace Barotrauma.Lights public bool Enabled = true; private readonly ISerializableEntity conditionalTarget; - private readonly PropertyConditional.Comparison comparison; + private readonly PropertyConditional.LogicalOperatorType logicalOperator; private readonly List conditionals = new List(); public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null) @@ -432,11 +452,8 @@ namespace Barotrauma.Lights { lightSourceParams = new LightSourceParams(element); CastShadows = element.GetAttributeBool("castshadows", true); - string comparison = element.GetAttributeString("comparison", null); - if (comparison != null) - { - Enum.TryParse(comparison, ignoreCase: true, out this.comparison); - } + logicalOperator = element.GetAttributeEnum(nameof(logicalOperator), + element.GetAttributeEnum("comparison", logicalOperator)); if (lightSourceParams.DeformableLightSpriteElement != null) { @@ -449,13 +466,7 @@ namespace Barotrauma.Lights switch (subElement.Name.ToString().ToLowerInvariant()) { case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionals.Add(new PropertyConditional(attribute)); - } - } + conditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; } } @@ -474,7 +485,7 @@ namespace Barotrauma.Lights public LightSource(Vector2 position, float range, Color color, Submarine submarine, bool addLight=true) { - hullsInRange = new List(); + convexHullsInRange = new List(); this.ParentSub = submarine; this.position = position; lightSourceParams = new LightSourceParams(range, color); @@ -515,19 +526,46 @@ namespace Barotrauma.Lights /// private void RefreshConvexHullList(ConvexHullList chList, Vector2 lightPos, Submarine sub) { - var fullChList = ConvexHull.HullLists.Find(x => x.Submarine == sub); + var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub); if (fullChList == null) { return; } - chList.List = fullChList.List.FindAll(ch => ch.Enabled && MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, ch.BoundingBox)); + //used to check whether the lightsource hits the target hull if the light is directional + Vector2 ray = new Vector2(dir.X, -dir.Y) * TextureRange; + Vector2 normal = new Vector2(-ray.Y, ray.X); - NeedsHullCheck = true; + chList.List.Clear(); + foreach (var convexHull in fullChList.List) + { + if (!convexHull.Enabled) { continue; } + if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; } + if (lightSourceParams.Directional && false) + { + Rectangle bounds = convexHull.BoundingBox; + //invert because GetLineRectangleIntersection uses the messed up rects that start from top-left + bounds.Y -= bounds.Height; + + //the ray can't hit if + // center is in the opposite direction from the ray (cheapest check first) + if (Vector2.Dot(ray, convexHull.BoundingBox.Center.ToVector2() - lightPos) <= 0 && + /*ray doesn't hit the convex hull*/ + !MathUtils.GetLineRectangleIntersection(lightPos, lightPos + ray, bounds, out _) && + /*normal vectors of the ray don't hit the convex hull */ + !MathUtils.GetLineRectangleIntersection(lightPos + normal, lightPos - normal, bounds, out _)) + { + continue; + } + } + chList.List.Add(convexHull); + } + chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch)); + HullsUpToDate.Add(sub); } /// /// Recheck which convex hulls are in range (if needed), /// and check if we need to recalculate vertices due to changes in the convex hulls /// - private void CheckHullsInRange() + private void CheckConvexHullsInRange() { foreach (Submarine sub in Submarine.Loaded) { @@ -540,21 +578,13 @@ namespace Barotrauma.Lights private void CheckHullsInRange(Submarine sub) { //find the list of convexhulls that belong to the sub - ConvexHullList chList = null; - foreach (var ch in hullsInRange) - { - if (ch.Submarine == sub) - { - chList = ch; - break; - } - } - + ConvexHullList chList = convexHullsInRange.FirstOrDefault(chList => chList.Submarine == sub); + //not found -> create one if (chList == null) { chList = new ConvexHullList(sub); - hullsInRange.Add(chList); + convexHullsInRange.Add(chList); NeedsRecalculation = true; } @@ -573,7 +603,7 @@ namespace Barotrauma.Lights //light and the convexhulls are both outside if (sub == null) { - if (NeedsHullCheck) + if (!HullsUpToDate.Contains(null)) { RefreshConvexHullList(chList, lightPos, null); } @@ -605,7 +635,7 @@ namespace Barotrauma.Lights //light and convexhull are both inside the same sub if (sub == ParentSub) { - if (NeedsHullCheck) + if (!HullsUpToDate.Contains(sub)) { RefreshConvexHullList(chList, lightPos, sub); } @@ -613,7 +643,7 @@ namespace Barotrauma.Lights //light and convexhull are inside different subs else { - if (sub.DockedTo.Contains(ParentSub) && !NeedsHullCheck) { return; } + if (sub.DockedTo.Contains(ParentSub) && HullsUpToDate.Contains(sub)) { return; } lightPos -= (sub.Position - ParentSub.Position); @@ -646,19 +676,45 @@ namespace Barotrauma.Lights } } - private List FindRaycastHits() + private static readonly object mutex = new object(); + + private readonly List visibleSegments = new List(); + private readonly List points = new List(); + private readonly List verts = new List(); + private readonly SegmentPoint[] boundaryCorners = new SegmentPoint[4]; + private void FindRaycastHits() { - if (!CastShadows || Range < 1.0f || Color.A < 1) { return null; } + if (!CastShadows || Range < 1.0f || Color.A < 1) + { + state = LightVertexState.PendingVertexRecalculation; + return; + } Vector2 drawPos = position; if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } - var hulls = new List(); - foreach (ConvexHullList chList in hullsInRange) + visibleSegments.Clear(); + foreach (ConvexHullList chList in convexHullsInRange) { foreach (ConvexHull hull in chList.List) { - if (!chList.IsHidden.Contains(hull)) { hulls.Add(hull); } + if (!chList.IsHidden.Contains(hull)) + { + //find convexhull segments that are close enough and facing towards the light source + lock (mutex) + { + hull.RefreshWorldPositions(); + hull.GetVisibleSegments(drawPos, visibleSegments); + foreach (var visibleSegment in visibleSegments) + { + if (visibleSegment.ConvexHull?.ParentEntity?.Submarine != null) + { + visibleSegment.SubmarineDrawPos = visibleSegment.ConvexHull.ParentEntity.Submarine.DrawPosition; + } + } + } + + } } foreach (ConvexHull hull in chList.List) { @@ -666,27 +722,19 @@ namespace Barotrauma.Lights } } - float bounds = TextureRange; - //find convexhull segments that are close enough and facing towards the light source - List visibleSegments = new List(); - List points = new List(); - foreach (ConvexHull hull in hulls) - { - hull.RefreshWorldPositions(); - hull.GetVisibleSegments(drawPos, visibleSegments, ignoreEdges: false); - } + state = LightVertexState.PendingRayCasts; + GameMain.LightManager.AddRayCastTask(this, drawPos, rotation); + } - //add a square-shaped boundary to make sure we've got something to construct the triangles from - //even if there aren't enough hull segments around the light source - - //(might be more effective to calculate if we actually need these extra points) + public void RayCastTask(Vector2 drawPos, float rotation) + { Vector2 drawOffset = Vector2.Zero; - float boundsExtended = bounds; + float boundsExtended = TextureRange; if (OverrideLightTexture != null) { - float cosAngle = (float)Math.Cos(Rotation); - float sinAngle = -(float)Math.Sin(Rotation); + float cosAngle = (float)Math.Cos(rotation); + float sinAngle = -(float)Math.Sin(rotation); var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); @@ -706,12 +754,16 @@ namespace Barotrauma.Lights drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle; } - var boundaryCorners = new SegmentPoint[] { - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X + boundsExtended, drawPos.Y + drawOffset.Y + boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X + boundsExtended, drawPos.Y + drawOffset.Y - boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X - boundsExtended, drawPos.Y + drawOffset.Y - boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X - boundsExtended, drawPos.Y + drawOffset.Y + boundsExtended), null) - }; + //add a square-shaped boundary to make sure we've got something to construct the triangles from + //even if there aren't enough hull segments around the light source + + //(might be more effective to calculate if we actually need these extra points) + Vector2 boundsMin = drawPos + drawOffset + new Vector2(-boundsExtended, -boundsExtended); + Vector2 boundsMax = drawPos + drawOffset + new Vector2(boundsExtended, boundsExtended); + boundaryCorners[0] = new SegmentPoint(boundsMax, null); + boundaryCorners[1] = new SegmentPoint(new Vector2(boundsMax.X, boundsMin.Y), null); + boundaryCorners[2] = new SegmentPoint(boundsMin, null); + boundaryCorners[3] = new SegmentPoint(new Vector2(boundsMin.X, boundsMax.Y), null); for (int i = 0; i < 4; i++) { @@ -719,199 +771,200 @@ namespace Barotrauma.Lights visibleSegments.Add(s); } - //Generate new points at the intersections between segments - //This is necessary for the light volume to generate properly on some subs - for (int i = 0; i < visibleSegments.Count; i++) + lock (mutex) { - Vector2 p1a = visibleSegments[i].Start.WorldPos; - Vector2 p1b = visibleSegments[i].End.WorldPos; - - for (int j = i + 1; j < visibleSegments.Count; j++) + //Generate new points at the intersections between segments + //This is necessary for the light volume to generate properly on some subs + for (int i = 0; i < visibleSegments.Count; i++) { - //ignore intersections between parallel axis-aligned segments - if (visibleSegments[i].IsAxisAligned && visibleSegments[j].IsAxisAligned && - visibleSegments[i].IsHorizontal == visibleSegments[j].IsHorizontal) - { - continue; - } + Vector2 p1a = visibleSegments[i].Start.WorldPos; + Vector2 p1b = visibleSegments[i].End.WorldPos; - Vector2 p2a = visibleSegments[j].Start.WorldPos; - Vector2 p2b = visibleSegments[j].End.WorldPos; - - if (Vector2.DistanceSquared(p1a, p2a) < 5.0f || - Vector2.DistanceSquared(p1a, p2b) < 5.0f || - Vector2.DistanceSquared(p1b, p2a) < 5.0f || - Vector2.DistanceSquared(p1b, p2b) < 5.0f) + for (int j = i + 1; j < visibleSegments.Count; j++) { - continue; - } - - bool intersects; - Vector2 intersection = Vector2.Zero; - if (visibleSegments[i].IsAxisAligned) - { - intersects = MathUtils.GetAxisAlignedLineIntersection(p2a, p2b, p1a, p1b, visibleSegments[i].IsHorizontal, out intersection); - } - else if (visibleSegments[j].IsAxisAligned) - { - intersects = MathUtils.GetAxisAlignedLineIntersection(p1a, p1b, p2a, p2b, visibleSegments[j].IsHorizontal, out intersection); - } - else - { - intersects = MathUtils.GetLineIntersection(p1a, p1b, p2a, p2b, out intersection); - } - - if (intersects) - { - SegmentPoint start = visibleSegments[i].Start; - SegmentPoint end = visibleSegments[i].End; - SegmentPoint mid = new SegmentPoint(intersection, null); - if (visibleSegments[i].ConvexHull?.ParentEntity?.Submarine != null) - { - mid.Pos -= visibleSegments[i].ConvexHull.ParentEntity.Submarine.DrawPosition; - } - - if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 5.0f || - Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 5.0f) + //ignore intersections between parallel axis-aligned segments + if (visibleSegments[i].IsAxisAligned && visibleSegments[j].IsAxisAligned && + visibleSegments[i].IsHorizontal == visibleSegments[j].IsHorizontal) { continue; } - Segment seg1 = new Segment(start, mid, visibleSegments[i].ConvexHull) - { - IsHorizontal = visibleSegments[i].IsHorizontal, - }; + Vector2 p2a = visibleSegments[j].Start.WorldPos; + Vector2 p2b = visibleSegments[j].End.WorldPos; - Segment seg2 = new Segment(mid, end, visibleSegments[i].ConvexHull) + if (Vector2.DistanceSquared(p1a, p2a) < 5.0f || + Vector2.DistanceSquared(p1a, p2b) < 5.0f || + Vector2.DistanceSquared(p1b, p2a) < 5.0f || + Vector2.DistanceSquared(p1b, p2b) < 5.0f) { - IsHorizontal = visibleSegments[i].IsHorizontal - }; + continue; + } - visibleSegments[i] = seg1; - visibleSegments.Insert(i + 1, seg2); + bool intersects; + Vector2 intersection = Vector2.Zero; + if (visibleSegments[i].IsAxisAligned) + { + intersects = MathUtils.GetAxisAlignedLineIntersection(p2a, p2b, p1a, p1b, visibleSegments[i].IsHorizontal, out intersection); + } + else if (visibleSegments[j].IsAxisAligned) + { + intersects = MathUtils.GetAxisAlignedLineIntersection(p1a, p1b, p2a, p2b, visibleSegments[j].IsHorizontal, out intersection); + } + else + { + intersects = MathUtils.GetLineIntersection(p1a, p1b, p2a, p2b, out intersection); + } + + if (intersects) + { + SegmentPoint start = visibleSegments[i].Start; + SegmentPoint end = visibleSegments[i].End; + SegmentPoint mid = new SegmentPoint(intersection, null); + mid.Pos -= visibleSegments[i].SubmarineDrawPos; + + if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 5.0f || + Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 5.0f) + { + continue; + } + + Segment seg1 = new Segment(start, mid, visibleSegments[i].ConvexHull) + { + IsHorizontal = visibleSegments[i].IsHorizontal, + }; + + Segment seg2 = new Segment(mid, end, visibleSegments[i].ConvexHull) + { + IsHorizontal = visibleSegments[i].IsHorizontal + }; + + visibleSegments[i] = seg1; + visibleSegments.Insert(i + 1, seg2); + i--; + break; + } + } + } + + points.Clear(); + //remove segments that fall out of bounds + for (int i = 0; i < visibleSegments.Count; i++) + { + Segment s = visibleSegments[i]; + if (Math.Abs(s.Start.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || + Math.Abs(s.Start.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f || + Math.Abs(s.End.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || + Math.Abs(s.End.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f) + { + visibleSegments.RemoveAt(i); i--; - break; + } + else + { + points.Add(s.Start); + points.Add(s.End); } } - } - //remove segments that fall out of bounds - for (int i = 0; i < visibleSegments.Count; i++) - { - Segment s = visibleSegments[i]; - if (Math.Abs(s.Start.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || - Math.Abs(s.Start.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f || - Math.Abs(s.End.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f || - Math.Abs(s.End.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f) + //remove points that are very close to each other + for (int i = 0; i < points.Count; i++) { - visibleSegments.RemoveAt(i); - i--; + for (int j = Math.Min(i + 4, points.Count - 1); j > i; j--) + { + if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && + Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) + { + points.RemoveAt(j); + } + } } - else + + var compareCCW = new CompareSegmentPointCW(drawPos); + try { - points.Add(s.Start); - points.Add(s.End); + points.Sort(compareCCW); } - } - - visibleSegments = visibleSegments.OrderBy(s => MathUtils.LineToPointDistanceSquared(s.Start.WorldPos, s.End.WorldPos, drawPos)).ToList(); - - var compareCCW = new CompareSegmentPointCW(drawPos); - try - { - points.Sort(compareCCW); - } - catch (Exception e) - { - StringBuilder sb = new StringBuilder("Constructing light volumes failed! Light pos: " + drawPos + ", Hull verts:\n"); - foreach (SegmentPoint sp in points) + catch (Exception e) { - sb.AppendLine(sp.Pos.ToString()); + StringBuilder sb = new StringBuilder("Constructing light volumes failed! Light pos: " + drawPos + ", Hull verts:\n"); + foreach (SegmentPoint sp in points) + { + sb.AppendLine(sp.Pos.ToString()); + } + DebugConsole.ThrowError(sb.ToString(), e); } - DebugConsole.ThrowError(sb.ToString(), e); - } - List output = new List(); - //List> preOutput = new List>(); + visibleSegments.Sort((s1, s2) => + MathUtils.LineToPointDistanceSquared(s1.Start.WorldPos, s1.End.WorldPos, drawPos) + .CompareTo(MathUtils.LineToPointDistanceSquared(s2.Start.WorldPos, s2.End.WorldPos, drawPos))); + + verts.Clear(); + foreach (SegmentPoint p in points) + { + Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); + Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * 3; + + //do two slightly offset raycasts to hit the segment itself and whatever's behind it + var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments); + if (intersection1.index < 0) { return; } + var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments); + if (intersection2.index < 0) { return; } + + Segment seg1 = visibleSegments[intersection1.index]; + Segment seg2 = visibleSegments[intersection2.index]; + + bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f; + bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f; + + if (isPoint1 && isPoint2) + { + //hit at the current segmentpoint -> place the segmentpoint into the list + verts.Add(p.WorldPos); + + foreach (ConvexHullList hullList in convexHullsInRange) + { + hullList.IsHidden.Remove(p.ConvexHull); + hullList.IsHidden.Remove(seg1.ConvexHull); + hullList.IsHidden.Remove(seg2.ConvexHull); + } + } + else if (intersection1.index != intersection2.index) + { + //the raycasts landed on different segments + //we definitely want to generate new geometry here + verts.Add(isPoint1 ? p.WorldPos : intersection1.pos); + verts.Add(isPoint2 ? p.WorldPos : intersection2.pos); + + foreach (ConvexHullList hullList in convexHullsInRange) + { + hullList.IsHidden.Remove(p.ConvexHull); + hullList.IsHidden.Remove(seg1.ConvexHull); + hullList.IsHidden.Remove(seg2.ConvexHull); + } + } + //if neither of the conditions above are met, we just assume + //that the raycasts both resulted on the same segment + //and creating geometry here would be wasteful + } + } //remove points that are very close to each other - for (int i = 0; i < points.Count; i++) + for (int i = 0; i < verts.Count - 1; i++) { - for (int j = Math.Min(i + 4, points.Count-1); j > i; j--) + for (int j = Math.Min(i + 4, verts.Count - 1); j > i; j--) { - if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && - Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) + if (Math.Abs(verts[i].X - verts[j].X) < 6 && + Math.Abs(verts[i].Y - verts[j].Y) < 6) { - points.RemoveAt(j); + verts.RemoveAt(j); } } } - - foreach (SegmentPoint p in points) - { - Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); - Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * 3; - - //do two slightly offset raycasts to hit the segment itself and whatever's behind it - var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments); - var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments); - - if (intersection1.index < 0) return null; - if (intersection2.index < 0) return null; - Segment seg1 = visibleSegments[intersection1.index]; - Segment seg2 = visibleSegments[intersection2.index]; - - bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f; - bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f; - - if (isPoint1 && isPoint2) - { - //hit at the current segmentpoint -> place the segmentpoint into the list - output.Add(p.WorldPos); - - foreach (ConvexHullList hullList in hullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } - } - else if (intersection1.index != intersection2.index) - { - //the raycasts landed on different segments - //we definitely want to generate new geometry here - output.Add(isPoint1 ? p.WorldPos : intersection1.pos); - output.Add(isPoint2 ? p.WorldPos : intersection2.pos); - - foreach (ConvexHullList hullList in hullsInRange) - { - hullList.IsHidden.Remove(p.ConvexHull); - hullList.IsHidden.Remove(seg1.ConvexHull); - hullList.IsHidden.Remove(seg2.ConvexHull); - } - } - //if neither of the conditions above are met, we just assume - //that the raycasts both resulted on the same segment - //and creating geometry here would be wasteful - } - - //remove points that are very close to each other - for (int i = 0; i < output.Count - 1; i++) - { - for (int j = Math.Min(i + 4, output.Count - 1); j > i; j--) - { - if (Math.Abs(output[i].X - output[j].X) < 6 && - Math.Abs(output[i].Y - output[j].Y) < 6) - { - output.RemoveAt(j); - } - } - } - - return output; + calculatedDrawPos = drawPos; + state = LightVertexState.PendingVertexRecalculation; } - private (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List segments) + private static (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List segments) { Vector2? closestIntersection = null; int segment = -1; @@ -936,13 +989,13 @@ namespace Barotrauma.Lights //same for the x-axis if (s.Start.WorldPos.X > s.End.WorldPos.X) { - if (s.Start.WorldPos.X < minX) continue; - if (s.End.WorldPos.X > maxX) continue; + if (s.Start.WorldPos.X < minX) { continue; } + if (s.End.WorldPos.X > maxX) { continue; } } else { - if (s.End.WorldPos.X < minX) continue; - if (s.Start.WorldPos.X > maxX) continue; + if (s.End.WorldPos.X < minX) { continue; } + if (s.Start.WorldPos.X > maxX) { continue; } } bool intersects; @@ -986,14 +1039,11 @@ namespace Barotrauma.Lights indices = new short[indexCount]; } - Vector2 drawPos = position; - if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } - - float cosAngle = (float)Math.Cos(Rotation); - float sinAngle = -(float)Math.Sin(Rotation); + Vector2 drawPos = calculatedDrawPos; Vector2 uvOffset = Vector2.Zero; Vector2 overrideTextureDims = Vector2.One; + Vector2 dir = this.dir; if (OverrideLightTexture != null) { overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); @@ -1002,8 +1052,7 @@ namespace Barotrauma.Lights if (LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = OverrideLightTexture.SourceRect.Width - origin.X; - cosAngle = -cosAngle; - sinAngle = -sinAngle; + dir = -dir; } if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; } uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f); @@ -1041,7 +1090,7 @@ namespace Barotrauma.Lights //calculate normal of first segment Vector2 nDiff1 = vertex - nextVertex; - float tx = nDiff1.X; nDiff1.X = -nDiff1.Y; nDiff1.Y = tx; + nDiff1 = new Vector2(-nDiff1.Y, nDiff1.X); nDiff1 /= Math.Max(Math.Abs(nDiff1.X), Math.Abs(nDiff1.Y)); //if the normal is pointing towards the light origin //rather than away from it, invert it @@ -1049,21 +1098,23 @@ namespace Barotrauma.Lights //calculate normal of second segment Vector2 nDiff2 = prevVertex - vertex; - tx = nDiff2.X; nDiff2.X = -nDiff2.Y; nDiff2.Y = tx; - nDiff2 /= Math.Max(Math.Abs(nDiff2.X),Math.Abs(nDiff2.Y)); + nDiff2 = new Vector2(-nDiff2.Y, nDiff2.X); + nDiff2 /= Math.Max(Math.Abs(nDiff2.X), Math.Abs(nDiff2.Y)); //if the normal is pointing towards the light origin //rather than away from it, invert it if (Vector2.DistanceSquared(nDiff2, rawDiff) > Vector2.DistanceSquared(-nDiff2, rawDiff)) nDiff2 = -nDiff2; //add the normals together and use some magic numbers to create //a somewhat useful/good-looking blur - Vector2 nDiff = nDiff1 * 40.0f; - if (MathUtils.GetLineIntersection(vertex + (nDiff1 * 40.0f), nextVertex + (nDiff1 * 40.0f), vertex + (nDiff2 * 40.0f), prevVertex + (nDiff2 * 40.0f), true, out Vector2 intersection)) + float blurDistance = 40.0f; + Vector2 nDiff = nDiff1 * blurDistance; + if (MathUtils.GetLineIntersection(vertex + (nDiff1 * blurDistance), nextVertex + (nDiff1 * blurDistance), vertex + (nDiff2 * blurDistance), prevVertex + (nDiff2 * blurDistance), true, out Vector2 intersection)) { nDiff = intersection - vertex; - if (nDiff.LengthSquared() > 10000.0f) + if (nDiff.LengthSquared() > 100.0f * 100.0f) { - nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y)); nDiff *= 100.0f; + nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y)); + nDiff *= 100.0f; } } @@ -1074,8 +1125,8 @@ namespace Barotrauma.Lights //calculate texture coordinates based on the light's rotation Vector2 originDiff = diff; - diff.X = originDiff.X * cosAngle - originDiff.Y * sinAngle; - diff.Y = originDiff.X * sinAngle + originDiff.Y * cosAngle; + diff.X = originDiff.X * dir.X - originDiff.Y * dir.Y; + diff.Y = originDiff.X * dir.Y + originDiff.Y * dir.X; diff *= (overrideTextureDims / OverrideLightTexture.size);// / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y))); diff += uvOffset; } @@ -1161,7 +1212,6 @@ namespace Barotrauma.Lights } translateVertices = Vector2.Zero; - rotateVertices = 0.0f; prevCalculatedPosition = position; prevCalculatedRotation = rotation; } @@ -1181,9 +1231,6 @@ namespace Barotrauma.Lights } drawPos.Y = -drawPos.Y; - float cosAngle = (float)Math.Cos(Rotation); - float sinAngle = -(float)Math.Sin(Rotation); - float bounds = TextureRange; if (OverrideLightTexture != null) @@ -1195,8 +1242,8 @@ namespace Barotrauma.Lights origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y); origin *= TextureRange; - drawPos.X += origin.X * sinAngle + origin.Y * cosAngle; - drawPos.Y += origin.X * cosAngle + origin.Y * sinAngle; + drawPos.X += origin.X * dir.Y + origin.Y * dir.X; + drawPos.Y += origin.X * dir.X + origin.Y * dir.Y; } //add a square-shaped boundary to make sure we've got something to construct the triangles from @@ -1302,7 +1349,7 @@ namespace Barotrauma.Lights { if (conditionals.None()) { return; } if (conditionalTarget == null) { return; } - if (comparison == PropertyConditional.Comparison.And) + if (logicalOperator == PropertyConditional.LogicalOperatorType.And) { Enabled = conditionals.All(c => c.Matches(conditionalTarget)); } @@ -1335,35 +1382,43 @@ namespace Barotrauma.Lights return; } - CheckHullsInRange(); + CheckConvexHullsInRange(); if (NeedsRecalculation && allowRecalculation) { - recalculationCount++; - var verts = FindRaycastHits(); - if (verts == null) + if (state == LightVertexState.UpToDate) { -#if DEBUG - DebugConsole.ThrowError($"Failed to generate vertices for a light source. Range: {Range}, color: {Color}, brightness: {CurrentBrightness}, parent: {ParentBody?.UserData ?? "Unknown"}"); -#endif - Enabled = false; - return; + recalculationCount++; + FindRaycastHits(); } + else if (state == LightVertexState.PendingVertexRecalculation) + { + if (verts == null) + { + #if DEBUG + DebugConsole.ThrowError($"Failed to generate vertices for a light source. Range: {Range}, color: {Color}, brightness: {CurrentBrightness}, parent: {ParentBody?.UserData ?? "Unknown"}"); + #endif + Enabled = false; + return; + } - CalculateLightVertices(verts); + CalculateLightVertices(verts); - LastRecalculationTime = (float)Timing.TotalTime; - NeedsRecalculation = false; + LastRecalculationTime = (float)Timing.TotalTime; + NeedsRecalculation = false; + state = LightVertexState.UpToDate; + } } + if (vertexCount == 0) { return; } + Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition; lightEffect.World = Matrix.CreateTranslation(-new Vector3(position, 0.0f)) * - Matrix.CreateRotationZ(rotateVertices - MathHelper.ToRadians(LightSourceParams.Rotation)) * + Matrix.CreateRotationZ(MathHelper.ToRadians(LightSourceParams.Rotation)) * Matrix.CreateTranslation(new Vector3(position + offset + translateVertices, 0.0f)) * transform; - if (vertexCount == 0) { return; } lightEffect.DiffuseColor = (new Vector3(Color.R, Color.G, Color.B) * (Color.A / 255.0f * CurrentBrightness)) / 255.0f; if (OverrideLightTexture != null) @@ -1387,9 +1442,9 @@ namespace Barotrauma.Lights public void Reset() { - hullsInRange.Clear(); + HullsUpToDate.Clear(); + convexHullsInRange.Clear(); diffToSub.Clear(); - NeedsHullCheck = true; NeedsRecalculation = true; vertexCount = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 573c648c4..2170143c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -70,9 +70,9 @@ namespace Barotrauma Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in HighlightedEntities) { - if (entity == this || !entity.IsHighlighted || !(entity is Item) || !entity.IsMouseOn(position)) { continue; } + if (entity == this|| entity is not Item || !entity.IsMouseOn(position)) { continue; } if (((Item)entity).GetComponent() == null) { continue; } if (linkedTo.Contains(entity)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index bdc94b8de..783c6d66a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.Xna.Framework.Input; +using Barotrauma.Extensions; namespace Barotrauma { @@ -72,6 +73,8 @@ namespace Barotrauma private RichString beaconStationActiveText, beaconStationInactiveText; + private GUIComponent locationInfoOverlay; + /*private (Rectangle targetArea, string tip)? connectionTooltip; private string sanitizedConnectionTooltip; private List connectionTooltipRichTextData; @@ -98,7 +101,7 @@ namespace Barotrauma OnClicked = (btn, userData) => { Rand.SetSyncedSeed(ToolBox.StringToInt(this.Seed)); - Generate(GameMain.GameSession.GameMode is CampaignMode campaign ? campaign.Settings : CampaignSettings.Empty); + Generate(GameMain.GameSession?.Campaign); InitProjectSpecific(); return true; } @@ -186,7 +189,7 @@ namespace Barotrauma private void LocationChanged(Location prevLocation, Location newLocation) { - if (prevLocation == newLocation) return; + if (prevLocation == newLocation) { return; } //focus on starting location if (prevLocation != null) { @@ -210,11 +213,17 @@ namespace Barotrauma currLocationIndicatorPos = CurrentLocation.MapPosition; } - RemoveFogOfWar(newLocation); + if (newLocation.Visited) + { + RemoveFogOfWar(newLocation); + } } + partial void RemoveFogOfWarProjSpecific(Location location) => RemoveFogOfWar(location); + private void RemoveFogOfWar(Location location, bool removeFromAdjacentLocations = true) { + if (mapTiles == null) { return; } if (location == null) { return; } var mapTile = generationParams.MapTiles.Values.FirstOrDefault().FirstOrDefault(); @@ -252,27 +261,223 @@ namespace Barotrauma return !tileDiscovered[MathHelper.Clamp(x, 0, tileDiscovered.Length), MathHelper.Clamp(y, 0, tileDiscovered.Length)]; } + private class MapNotification + { + public readonly RichString Text; + public readonly GUIFont Font; + + public readonly Vector2 TextSize; + + public int TimesShown; + + public float Offset; + + public readonly Location RelatedLocation; + + public bool IsCurrentlyVisible; + + public MapNotification(string text, GUIFont font, List existingNotifications, Location relatedLocation) + { + Text = RichString.Rich(text); + Font = font; + TextSize = Font.MeasureString(Font.ForceUpperCase ? Text.SanitizedValue.ToUpper() : Text.SanitizedValue); + if (existingNotifications.Any()) + { + Offset = existingNotifications.Max(n => n.Offset + n.TextSize.X + GUI.IntScale(60)); + } + RelatedLocation = relatedLocation; + } + } + + private readonly List mapNotifications = new List(); + partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change) { - if (change.Messages.Any()) + var messages = change.GetMessages(location.Faction); + if (!messages.Any()) { return; } + + string msg = messages.GetRandom(Rand.RandSync.Unsynced) + .Replace("[previousname]", $"‖color:gui.yellow‖{prevName}‖end‖") + .Replace("[name]", $"‖color:gui.yellow‖{location.Name}‖end‖"); + location.LastTypeChangeMessage = msg; + + mapNotifications.Add(new MapNotification(msg, GUIStyle.SubHeadingFont, mapNotifications, location)); + } + + public void DrawNotifications(SpriteBatch spriteBatch, GUICustomComponent container) + { + Vector2 pos = new Vector2(container.Rect.Right, container.Rect.Center.Y); + foreach (var notification in mapNotifications) { - string msg = change.Messages[Rand.Range(0, change.Messages.Count)] - .Replace("[previousname]", $"‖color:gui.orange‖{prevName}‖end‖") - .Replace("[name]", $"‖color:gui.orange‖{location.Name}‖end‖"); - location.LastTypeChangeMessage = msg; - if (GameMain.Client != null) + Vector2 textPos = pos + new Vector2(notification.Offset, -notification.TextSize.Y / 2); + + notification.Font.DrawStringWithColors( + spriteBatch, + notification.Text.SanitizedValue, + textPos, + Color.White, 0.0f, Vector2.Zero, 1.0f, SpriteEffects.None, 0, + notification.Text.RichTextData); + + int margin = container.Rect.Width / 5; + notification.IsCurrentlyVisible = + textPos.X < container.Rect.Right - margin && + textPos.X + notification.TextSize.X > container.Rect.X + margin; + } + } + + private void UpdateNotifications(float deltaTime, GUICustomComponent mapContainer) + { + if (mapNotifications.Count < 5) + { + int maxIndex = 1; + while (TextManager.ContainsTag("randomnews" + maxIndex)) { - GameMain.Client.AddChatMessage(msg, Networking.ChatMessageType.Default, TextManager.Get("RadioAnnouncerName").Value); + maxIndex++; } - else + string textTag = "randomnews" + Rand.Range(0, maxIndex); + if (TextManager.ContainsTag(textTag)) { - GameMain.GameSession?.GameMode.CrewManager.AddSinglePlayerChatMessage( - TextManager.Get("RadioAnnouncerName").Value, - msg, - Networking.ChatMessageType.Default, - sender: null); + mapNotifications.Add(new MapNotification(TextManager.Get(textTag).Value, GUIStyle.SubHeadingFont, mapNotifications, relatedLocation: null)); } - } + } + + for (int i = mapNotifications.Count - 1; i >= 0; i--) + { + var notification = mapNotifications[i]; + notification.Offset -= deltaTime * 75.0f; + if (notification.Offset < -notification.TextSize.X - mapContainer.Rect.Width) + { + notification.Offset = Math.Max(mapNotifications.Max(n => n.Offset + n.TextSize.X) + GUI.IntScale(60), 0); + notification.TimesShown++; + if (mapNotifications.Count > 5) + { + mapNotifications.RemoveAt(i); + } + else if (mapNotifications.Count > 3 && notification.TimesShown > 2) + { + mapNotifications.RemoveAt(i); + } + } + } + } + + private void CreateLocationInfoOverlay(Location location) + { + locationInfoOverlay = new GUIFrame(new RectTransform(new Point(GUI.IntScale(350), GUI.IntScale(350)), GUI.Canvas), style: "GUIToolTip") + { + UserData = location + }; + locationInfoOverlay.Color *= 0.8f; + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), locationInfoOverlay.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + bool showReputation = hudVisibility > 0.0f && location.Type.HasOutpost && location.Reputation != null; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUIStyle.LargeFont) { Padding = Vector4.Zero }; + if (!location.Type.Name.IsNullOrEmpty()) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; + } + + CreateSpacing(10); + + if (!location.Type.Description.IsNullOrEmpty()) + { + CreateTextWithIcon(location.Type.Description, location.Type.Sprite); + } + + int highestSubTier = location.HighestSubmarineTierAvailable(); + List<(SubmarineClass subClass, int tier)> overrideTiers = null; + if (location.CanHaveSubsForSale()) + { + overrideTiers = new List<(SubmarineClass subClass, int tier)>(); + foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) + { + if (subClass == SubmarineClass.Undefined) { continue; } + int highestClassTier = location.HighestSubmarineTierAvailable(subClass); + if (highestClassTier > 0 && highestClassTier > highestSubTier) + { + overrideTiers.Add((subClass, highestClassTier)); + } + } + } + if (highestSubTier > 0) + { + CreateTextWithIcon(TextManager.GetWithVariable("advancedsub.all", "[tiernumber]", highestSubTier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon"); + } + if (overrideTiers != null) + { + foreach (var (subClass, tier) in overrideTiers) + { + CreateTextWithIcon(TextManager.GetWithVariable($"advancedsub.{subClass}", "[tiernumber]", tier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon"); + } + } + + CreateSpacing(10); + + void CreateTextWithIcon(LocalizedString text, Sprite icon, string style = null) + { + var textHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, (int)GUIStyle.Font.MeasureString(text).Y), content.RectTransform), isHorizontal: true) + { + Stretch = true, + CanBeFocused = true + }; + var guiIcon = + style == null ? + new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), icon) : + new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style); + var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.9f, 1.0f), textHolder.RectTransform), text); + textBlock.RectTransform.MinSize = new Point((int)textBlock.TextSize.X, 0); + textHolder.RectTransform.MinSize = new Point((int)textBlock.TextSize.X + guiIcon.Rect.Width, 0); + } + + void CreateSpacing(int height) + { + new GUIFrame(new RectTransform(new Point(content.Rect.Width, GUI.IntScale(height)), content.RectTransform), style: null); + } + + if (location.Faction != null) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), + RichString.Rich(TextManager.GetWithVariables("reputationgainnotification", + ("[value]", string.Empty), + ("[reputationname]", $"‖color:{XMLExtensions.ToStringHex(location.Faction.Prefab.IconColor)}‖{location.Faction.Prefab.Name}‖end‖")))) + { + Padding = Vector4.Zero + }; + + CreateSpacing(10); + + var repBarHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, GUI.IntScale(25)), content.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + new GUICustomComponent(new RectTransform(new Vector2(0.6f, 1.0f), repBarHolder.RectTransform), onDraw: (sb, component) => + { + if (location.Reputation == null) { return; } + RoundSummary.DrawReputationBar(sb, component.Rect, location.Reputation.NormalizedValue); + }); + + new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), repBarHolder.RectTransform), + location.Reputation.GetFormattedReputationText(), textAlignment: Alignment.CenterRight); + + new GUIImage(new RectTransform(new Vector2(0.25f, 0.5f), locationInfoOverlay.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.05f) }, + location.Faction.Prefab.Icon, scaleToFit: true) + { + Color = location.Faction.Prefab.IconColor * 0.5f + }; + CreateSpacing(20); + } + + locationInfoOverlay.RectTransform.NonScaledSize = + new Point( + Math.Max(locationInfoOverlay.Rect.Width, (int)(content.Children.Max(c => c is GUITextBlock textBlock ? textBlock.TextSize.X : c.RectTransform.MinSize.X) * 1.2f)), + (int)(content.Children.Sum(c => c.Rect.Height) / content.RectTransform.RelativeSize.Y)); } partial void ClearAnimQueue() @@ -280,12 +485,13 @@ namespace Barotrauma mapAnimQueue.Clear(); } - public void Update(float deltaTime, GUICustomComponent mapContainer) + public void Update(CampaignMode campaign, float deltaTime, GUICustomComponent mapContainer) { Rectangle rect = mapContainer.Rect; - var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); + UpdateNotifications(deltaTime, mapContainer); + var currentDisplayLocation = campaign?.GetCurrentDisplayLocation(); if (currentDisplayLocation != null) { if (!currentDisplayLocation.Discovered) @@ -345,10 +551,39 @@ namespace Barotrauma Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); Vector2 viewOffset = DrawOffset + drawOffsetNoise; + if (HighlightedLocation != null) + { + Vector2 highlightedLocationDrawPos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; + if (locationInfoOverlay == null || locationInfoOverlay.UserData != HighlightedLocation) + { + CreateLocationInfoOverlay(HighlightedLocation); + } + + Point offsetFromLocationIcon = new Point(GUI.IntScale(25)); + var locationInfoRt = locationInfoOverlay.RectTransform; + if (locationInfoRt.Pivot == Pivot.BottomLeft || locationInfoRt.Pivot == Pivot.BottomRight) + { + offsetFromLocationIcon.Y = -offsetFromLocationIcon.Y; + } + if (locationInfoRt.Pivot == Pivot.TopRight || locationInfoRt.Pivot == Pivot.BottomRight) + { + offsetFromLocationIcon.X = -offsetFromLocationIcon.X; + } + locationInfoRt.ScreenSpaceOffset = highlightedLocationDrawPos.ToPoint() + offsetFromLocationIcon; + if (locationInfoOverlay.Rect.Bottom > rect.Bottom) + { + locationInfoRt.Pivot = Pivot.BottomLeft; + } + if (locationInfoOverlay.Rect.Right > rect.Right) + { + locationInfoRt.Pivot = locationInfoRt.Pivot == Pivot.TopLeft ? Pivot.TopRight : Pivot.BottomRight; + } + locationInfoOverlay?.AddToGUIUpdateList(order: 1); + } float closestDist = 0.0f; HighlightedLocation = null; - if (GUI.MouseOn == null || GUI.MouseOn == mapContainer) + if ((GUI.MouseOn == null || GUI.MouseOn == mapContainer)) { for (int i = 0; i < Locations.Count; i++) { @@ -374,7 +609,7 @@ namespace Barotrauma if (HighlightedLocation == null || dist < closestDist) { closestDist = dist; - HighlightedLocation = location; + HighlightedLocation = location; } } } @@ -453,12 +688,13 @@ namespace Barotrauma Level.Loaded.DebugSetEndLocation(null); Discover(CurrentLocation); + Visit(CurrentLocation); OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation)); SelectLocation(-1); if (GameMain.Client == null) { CurrentLocation.CreateStores(); - ProgressWorld(); + ProgressWorld(campaign); Radiation?.OnStep(1); } else @@ -467,12 +703,6 @@ namespace Barotrauma } } - if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftShift) && PlayerInput.PrimaryMouseButtonClicked() && HighlightedLocation != null) - { - int distance = DistanceToClosestLocationWithOutpost(HighlightedLocation, out Location foundLocation); - DebugConsole.NewMessage($"Distance to closest outpost from {HighlightedLocation.Name} to {foundLocation?.Name} is {distance}"); - } - if (PlayerInput.PrimaryMouseButtonClicked() && HighlightedLocation == null) { SelectLocation(-1); @@ -481,10 +711,10 @@ namespace Barotrauma } } - public void Draw(SpriteBatch spriteBatch, GUICustomComponent mapContainer) + public void Draw(CampaignMode campaign, SpriteBatch spriteBatch, GUICustomComponent mapContainer) { tooltip = null; - var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); + var currentDisplayLocation = campaign?.GetCurrentDisplayLocation(); Rectangle rect = mapContainer.Rect; @@ -501,13 +731,15 @@ namespace Barotrauma Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); + float missionIconScale = generationParams.MissionIcon != null ? 18.0f / generationParams.MissionIcon.SourceRect.Width : 1.0f; + Rectangle prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, rect); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - Vector2 topLeft = rectCenter + viewOffset; - Vector2 bottomRight = rectCenter + (viewOffset + new Vector2(Width, Height)); + Vector2 topLeft = rectCenter + viewOffset - rect.Location.ToVector2(); + Vector2 bottomRight = topLeft + new Vector2(Width, Height); Vector2 mapTileSize = mapTiles[0, 0].size * generationParams.MapTileScale; int startX = (int)Math.Floor(-topLeft.X / mapTileSize.X) - 1; @@ -568,7 +800,9 @@ namespace Barotrauma for (int i = 0; i < Locations.Count; i++) { Location location = Locations[i]; - if (IsInFogOfWar(location)) { continue; } + if (!location.Discovered && IsInFogOfWar(location)) { continue; } + bool isEndLocation = endLocations.Contains(location); + if (!GameMain.DebugDraw && isEndLocation && location != endLocations.First()) { continue; } Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite; @@ -577,24 +811,54 @@ namespace Barotrauma drawRect.X = (int)pos.X - drawRect.Width / 2; drawRect.Y = (int)pos.Y - drawRect.Width / 2; + if (drawRect.X > rect.Right - GUI.IntScale(100) && generationParams.MissionIcon != null && location.AvailableMissions.Any()) + { + Vector2 offScreenMissionIconPos = new Vector2(rect.Right - GUI.IntScale(50), drawRect.Center.Y); + generationParams.MissionIcon.Draw(spriteBatch, + offScreenMissionIconPos, + generationParams.IndicatorColor, scale: missionIconScale * zoom); + GUI.Arrow.Draw(spriteBatch, + offScreenMissionIconPos + Vector2.UnitX * generationParams.MissionIcon.size.X * missionIconScale * zoom, + generationParams.IndicatorColor, MathHelper.PiOver2, scale: 0.5f); + } + + if (!rect.Intersects(drawRect)) { continue; } Color color = location.Type.SpriteColor; - if (!location.Discovered) { color = Color.White; } + if (!location.Visited) { color = Color.White; } if (location.Connections.Find(c => c.Locations.Contains(currentDisplayLocation)) == null) { color *= 0.5f; } float iconScale = location == currentDisplayLocation ? 1.2f : 1.0f; - if (location == HighlightedLocation) + if (location == HighlightedLocation) { iconScale *= 1.2f; } + if (isEndLocation) { iconScale *= 2.0f; } + + float notificationPulseAmount = 1.0f; + float notificationColorLerp = 0.0f; + if (mapNotifications.Any(n => n.RelatedLocation == location && n.IsCurrentlyVisible)) { - iconScale *= 1.2f; + float sin = MathF.Sin((float)Timing.TotalTime * 2.0f); + notificationPulseAmount = Math.Max(sin + 0.5f, 1.0f); + notificationColorLerp = (notificationPulseAmount - 1.0f) * 4.0f; + color = Color.Lerp(color, GUIStyle.Yellow, notificationColorLerp); + iconScale *= notificationPulseAmount; } - locationSprite.Draw(spriteBatch, pos, color, + locationSprite.Draw(spriteBatch, pos, color, scale: generationParams.LocationIconSize / locationSprite.size.X * iconScale * zoom); + if (location.Faction != null) + { + float factionIconScale = iconScale * 0.7f; + Sprite factionIcon = location.Faction.Prefab.IconSmall ?? location.Faction.Prefab.Icon; + Color factionIconColor = Color.Lerp(color, location.Faction.Prefab.IconColor, notificationColorLerp); + factionIcon.Draw(spriteBatch, pos + new Vector2(-15, 15) * zoom, factionIconColor, + scale: generationParams.LocationIconSize / factionIcon.size.X * factionIconScale * zoom); + } + if (location == currentDisplayLocation) { if (SelectedLocation != null) @@ -626,7 +890,10 @@ namespace Barotrauma { Vector2 typeChangeIconPos = pos + new Vector2(1.35f, -0.35f) * generationParams.LocationIconSize * 0.5f * zoom; float typeChangeIconScale = 18.0f / generationParams.TypeChangeIcon.SourceRect.Width; - generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, GUIStyle.Red, scale: typeChangeIconScale * zoom); + Color iconColor = GUIStyle.Red; + color = Color.Lerp(color, GUIStyle.Yellow, notificationColorLerp); + iconScale *= notificationPulseAmount; + generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, iconColor, scale: typeChangeIconScale * zoom); if (Vector2.Distance(PlayerInput.MousePosition, typeChangeIconPos) < generationParams.TypeChangeIcon.SourceRect.Width * zoom && (tooltip == null || IsPreferredTooltip(typeChangeIconPos))) { @@ -635,14 +902,17 @@ namespace Barotrauma } if (location != CurrentLocation && generationParams.MissionIcon != null) { - if ((CurrentLocation == currentDisplayLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location))) || location.AvailableMissions.Any(m => m.Prefab.Type == MissionType.GoTo)) + if ((CurrentLocation == currentDisplayLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location))) || + location.AvailableMissions.Any(m => m.Locations[0] == m.Locations[1])) { Vector2 missionIconPos = pos + new Vector2(1.35f, 0.35f) * generationParams.LocationIconSize * 0.5f * zoom; - float missionIconScale = 18.0f / generationParams.MissionIcon.SourceRect.Width; generationParams.MissionIcon.Draw(spriteBatch, missionIconPos, generationParams.IndicatorColor, scale: missionIconScale * zoom); if (Vector2.Distance(PlayerInput.MousePosition, missionIconPos) < generationParams.MissionIcon.SourceRect.Width * zoom && IsPreferredTooltip(missionIconPos)) { - var availableMissions = CurrentLocation.AvailableMissions.Where(m => m.Locations.Contains(location)).Concat(location.AvailableMissions.Where(m => m.Prefab.Type == MissionType.GoTo)).Distinct(); + var availableMissions = CurrentLocation.AvailableMissions + .Where(m => m.Locations.Contains(location)) + .Concat(location.AvailableMissions.Where(m => m.Locations[0] == m.Locations[1])) + .Distinct(); tooltip = (new Rectangle(missionIconPos.ToPoint(), new Point(30)), TextManager.Get("mission") + '\n'+ string.Join('\n', availableMissions.Select(m => "- " + m.Name))); } } @@ -651,23 +921,19 @@ namespace Barotrauma if (GameMain.DebugDraw) { Vector2 dPos = pos; - if (location == HighlightedLocation && (!location.Discovered || !location.HasOutpost()) && location.Reputation != null) + 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; - string name = $"Reputation: {location.Name}"; - Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, dPos, name, Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); - dPos.Y += nameSize.Y + 16; - Rectangle bgRect = new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32); - bgRect.Inflate(8,8); - Color barColor = ToolBox.GradientLerp(location.Reputation.NormalizedValue, Color.Red, Color.Yellow, Color.LightGreen); - GUI.DrawRectangle(spriteBatch, bgRect, Color.Black * 0.8f, isFilled: true); - GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, (int)(location.Reputation.NormalizedValue * 255), 32), barColor, isFilled: true); - string reputationValue = ((int)location.Reputation.Value).ToString(); - Vector2 repValueSize = GUIStyle.SubHeadingFont.MeasureString(reputationValue); - GUI.DrawString(spriteBatch, dPos + (new Vector2(256, 32) / 2) - (repValueSize / 2), reputationValue, Color.White, Color.Black, font: GUIStyle.SubHeadingFont); - GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32), Color.White); + if (PlayerInput.KeyDown(Keys.LeftShift)) + { + GUI.DrawString(spriteBatch, new Vector2(150,150), "Dist: " + + GetDistanceToClosestLocationOrConnection(CurrentLocation, int.MaxValue, loc => loc == location), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); + + } } dPos.Y += 48; GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatSingleDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); @@ -684,97 +950,6 @@ namespace Barotrauma GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.tip, tooltip.Value.targetArea); drawRadiationTooltip = false; } - else if (HighlightedLocation != null) - { - drawRadiationTooltip = false; - Vector2 pos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; - pos.X += 50 * zoom; - pos.X = (int)pos.X; - pos.Y = (int)pos.Y; - Vector2 nameSize = GUIStyle.LargeFont.MeasureString(HighlightedLocation.Name); - Vector2 typeSize = HighlightedLocation.Type.Name.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.Font.MeasureString(HighlightedLocation.Type.Name); - Vector2 descSize = HighlightedLocation.Type.Description.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.SmallFont.MeasureString(HighlightedLocation.Type.Description); - Vector2 size = new Vector2(Math.Max(nameSize.X, Math.Max(typeSize.X, descSize.X)), nameSize.Y + typeSize.Y + descSize.Y); - - int highestSubTier = HighlightedLocation.HighestSubmarineTierAvailable(); - List<(SubmarineClass subClass, int tier)> overrideTiers = null; - if (HighlightedLocation.CanHaveSubsForSale()) - { - overrideTiers = new List<(SubmarineClass subClass, int tier)>(); - foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) - { - if (subClass == SubmarineClass.Undefined) { continue; } - int highestClassTier = HighlightedLocation.HighestSubmarineTierAvailable(subClass); - if (highestClassTier > 0 && highestClassTier > highestSubTier) - { - overrideTiers.Add((subClass, highestClassTier)); - } - } - } - int subAvailabilityTextCount = (highestSubTier > 0 ? 1 : 0) + (overrideTiers?.Count ?? 0); - size.Y += subAvailabilityTextCount * GUIStyle.SmallFont.MeasureString(TextManager.Get("advancedsub.all")).Y; - - bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; - LocalizedString repLabelText = null, repValueText = null; - Vector2 repLabelSize = Vector2.Zero, repBarSize = Vector2.Zero; - if (showReputation) - { - repLabelText = TextManager.Get("reputation"); - repLabelSize = GUIStyle.Font.MeasureString(repLabelText); - repBarSize = new Vector2(GUI.IntScale(200), repLabelSize.Y); - size.Y += 2 * repLabelSize.Y + GUI.IntScale(5) + repBarSize.Y; - repValueText = HighlightedLocation.Reputation.GetFormattedReputationText(addColorTags: false); - size.X = Math.Max(size.X, repBarSize.X + GUIStyle.Font.MeasureString(repValueText).X + GUI.IntScale(10)); - } - - GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, - new Rectangle( - (int)(pos.X - 60 * GUI.Scale), - (int)(pos.Y - size.Y), - (int)(size.X + 120 * GUI.Scale), - (int)(size.Y * 2.2f)), - Color.Black * hudVisibility); - - var topLeftPos = pos - new Vector2(0.0f, size.Y / 2); - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: GUIStyle.LargeFont); - topLeftPos += new Vector2(0.0f, nameSize.Y); - DrawText(HighlightedLocation.Type.Name); - if (!HighlightedLocation.Type.Description.IsNullOrEmpty()) - { - topLeftPos += new Vector2(0.0f, descSize.Y); - DrawText(HighlightedLocation.Type.Description, font: GUIStyle.SmallFont); - } - - if (highestSubTier > 0) - { - DrawSubAvailabilityText("advancedsub.all", highestSubTier); - } - if (overrideTiers != null) - { - foreach (var (subClass, tier) in overrideTiers) - { - DrawSubAvailabilityText($"advancedsub.{subClass}", tier); - } - } - void DrawSubAvailabilityText(string tag, int tier) - { - topLeftPos += new Vector2(0.0f, typeSize.Y); - DrawText(TextManager.GetWithVariable(tag, "[tiernumber]", tier.ToString()), font: GUIStyle.SmallFont); - } - - if (showReputation) - { - topLeftPos += new Vector2(0.0f, typeSize.Y + repLabelSize.Y); - DrawText(repLabelText.Value); - topLeftPos += new Vector2(0.0f, repLabelSize.Y + GUI.IntScale(10)); - Rectangle repBarRect = new Rectangle(new Point((int)topLeftPos.X, (int)topLeftPos.Y), new Point((int)repBarSize.X, (int)repBarSize.Y)); - RoundSummary.DrawReputationBar(spriteBatch, repBarRect, HighlightedLocation.Reputation.NormalizedValue); - GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + GUI.IntScale(5), repBarRect.Top), repValueText.Value, Reputation.GetReputationColor(HighlightedLocation.Reputation.NormalizedValue)); - } - - void DrawText(LocalizedString text, GUIFont font = null) => GUI.DrawString(spriteBatch, topLeftPos, text, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: font); - } if (drawRadiationTooltip) { @@ -892,7 +1067,7 @@ namespace Barotrauma } float a = 1.0f; - if (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered) + if (!connection.Locations[0].Visited && !connection.Locations[1].Visited) { if (IsInFogOfWar(connection.Locations[0])) { @@ -961,25 +1136,25 @@ namespace Barotrauma if (connection.Locked) { var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; - var unlockEvent = - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == Identifier.Empty); + var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction); if (unlockEvent != null) { Reputation unlockReputation = CurrentLocation.Reputation; Faction unlockFaction = null; - if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) + if (!unlockEvent.Faction.IsEmpty) { - unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.UnlockPathFaction); + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction); unlockReputation = unlockFaction?.Reputation; } - - DrawIcon( - "LockedLocationConnection", (int)(28 * zoom), - RichString.Rich(TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip", - ("[requiredreputation]", Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true)), - ("[currentreputation]", unlockReputation.GetFormattedReputationText(addColorTags: true))))); + if (unlockReputation != null) + { + DrawIcon( + "LockedLocationConnection", (int)(28 * zoom), + RichString.Rich(TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip", + ("[requiredreputation]", Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true)), + ("[currentreputation]", unlockReputation.GetFormattedReputationText(addColorTags: true))))); + } } else { @@ -1042,13 +1217,14 @@ namespace Barotrauma private void DrawDecorativeHUD(SpriteBatch spriteBatch, Rectangle rect) { generationParams.DecorativeGraphSprite.Draw(spriteBatch, (int)((Timing.TotalTime * 5.0f) % generationParams.DecorativeGraphSprite.FrameCount), - new Vector2(rect.Left, rect.Top), Color.White, Vector2.Zero, 0, Vector2.One * GUI.Scale); + new Vector2(rect.X, rect.Bottom - (generationParams.DecorativeGraphSprite.FrameSize.Y + 30) * GUI.Scale), + Color.White, Vector2.Zero, 0, Vector2.One * GUI.Scale, SpriteEffects.FlipVertically); GUI.DrawString(spriteBatch, new Vector2(rect.Right - GUI.IntScale(170), rect.Y + GUI.IntScale(5)), "JOVIAN FLUX " + ((cameraNoiseStrength + Rand.Range(-0.02f, 0.02f)) * 500), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont); GUI.DrawString(spriteBatch, - new Vector2(rect.X + GUI.IntScale(15), rect.Bottom - GUI.IntScale(25)), + new Vector2(rect.X + GUI.IntScale(5), rect.Y + GUI.IntScale(5)), "LAT " + (-DrawOffset.Y / 100.0f) + " LON " + (-DrawOffset.X / 100.0f), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 51fa11094..151e15a14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -36,7 +36,7 @@ namespace Barotrauma public static List CopiedList = new List(); - private static List highlightedList = new List(); + private static List highlightedInEditorList = new List(); private static float highlightTimer; @@ -99,11 +99,24 @@ namespace Barotrauma { float depth = baseDepth //take texture into account to get entities with (roughly) the same base depth and texture to render consecutively to minimize texture swaps - + (sprite?.Texture?.SortingKey ?? 0) % 100 * 0.00001f - + ID % 100 * 0.000001f; + + (sprite?.Texture?.SortingKey ?? 0) % 100 * 0.000001f + + ID % 100 * 0.0000001f; return Math.Min(depth, 1.0f); } + protected Vector2 GetCollapseEffectOffset() + { + if (Level.Loaded?.Renderer?.CollapseEffectStrength is float collapseEffectStrength and > 0.0f && Submarine is not { Info.Type: SubmarineType.Player }) + { + Vector2 noisePos = new Vector2( + (float)PerlinNoise.GetPerlin((float)(Timing.TotalTime + ID) * 0.1f, (float)(Timing.TotalTime + ID) * 0.5f) - 0.5f, + (float)PerlinNoise.GetPerlin((float)(Timing.TotalTime + ID) * 0.1f, (float)(Timing.TotalTime + ID) * 0.1f) - 0.5f); + Vector2 offsetFromOrigin = Level.Loaded.Renderer.CollapseEffectOrigin - DrawPosition; + return offsetFromOrigin * MathF.Pow(collapseEffectStrength, MathHelper.Lerp(1, 4, ID % 1000 / 1000.0f)) + (noisePos * 100.0f * collapseEffectStrength); + } + return Vector2.Zero; + } + /// /// Update the selection logic in submarine editor /// @@ -118,10 +131,7 @@ namespace Barotrauma return; } - foreach (MapEntity e in mapEntityList) - { - e.isHighlighted = false; - } + ClearHighlightedEntities(); if (DisableSelect) { @@ -249,11 +259,10 @@ namespace Barotrauma if (i == 0) highLightedEntity = e; } } - UpdateHighlighting(highlightedEntities); } - if (highLightedEntity != null) highLightedEntity.isHighlighted = true; + if (highLightedEntity != null) { highLightedEntity.IsHighlighted = true; } } if (GUI.KeyboardDispatcher.Subscriber == null) @@ -275,7 +284,6 @@ namespace Barotrauma if (startMovingPos != Vector2.Zero) { Item targetContainer = GetPotentialContainer(position, SelectedList); - if (targetContainer != null) { targetContainer.IsHighlighted = true; } if (PlayerInput.PrimaryMouseButtonReleased()) @@ -597,10 +605,10 @@ namespace Barotrauma if (highlightedListBox != null) { if (GUI.MouseOn == highlightedListBox || highlightedListBox.IsParentOf(GUI.MouseOn)) return; - if (highlightedEntities.SequenceEqual(highlightedList)) return; + if (highlightedEntities.SequenceEqual(highlightedInEditorList)) return; } - highlightedList = highlightedEntities; + highlightedInEditorList = highlightedEntities; highlightedListBox = new GUIListBox(new RectTransform(new Point(180, highlightedEntities.Count * 18 + 5), GUI.Canvas) { @@ -1083,7 +1091,7 @@ namespace Barotrauma private void UpdateResizing(Camera cam) { - isHighlighted = true; + IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; int StartY = ResizeVertical ? -1 : 0; @@ -1184,7 +1192,7 @@ namespace Barotrauma private void DrawResizing(SpriteBatch spriteBatch, Camera cam) { - isHighlighted = true; + IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; int StartY = ResizeVertical ? -1 : 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index b41a0f0a6..d82698f9d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -55,21 +55,14 @@ namespace Barotrauma { if (!CastShadow) { return; } - if (convexHulls == null) + convexHulls ??= new List(); + var h = new ConvexHull( + new Rectangle((position - size / 2).ToPoint(), size.ToPoint()), + IsHorizontal, + this) { - convexHulls = new List(); - } - - Vector2 halfSize = size / 2; - Vector2[] verts = new Vector2[] - { - position + new Vector2(-halfSize.X, halfSize.Y), - position + new Vector2(halfSize.X, halfSize.Y), - position + new Vector2(halfSize.X, -halfSize.Y), - position + new Vector2(-halfSize.X, -halfSize.Y), + IsExteriorWall = IsExteriorWall }; - - var h = new ConvexHull(verts, Color.Black, this); if (Math.Abs(rotation) > 0.001f) { h.Rotate(position, rotation); @@ -226,6 +219,9 @@ namespace Barotrauma 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 offset = GetCollapseEffectOffset(); + min += offset; + max += offset; if (min.X > worldView.Right || max.X < worldView.X) { return false; } if (min.Y > worldView.Y || max.Y < worldView.Y - worldView.Height) { return false; } @@ -295,6 +291,7 @@ namespace Barotrauma if (isWiringMode) { color *= 0.15f; } Vector2 drawOffset = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; + drawOffset += GetCollapseEffectOffset(); float depth = GetDrawDepth(); @@ -504,7 +501,7 @@ namespace Barotrauma private bool ConditionalMatches(PropertyConditional conditional) { - if (!string.IsNullOrEmpty(conditional.TargetItemComponentName)) + if (!string.IsNullOrEmpty(conditional.TargetItemComponent)) { return false; } @@ -533,7 +530,7 @@ namespace Barotrauma float damage = msg.ReadRangedSingle(0.0f, 1.0f, 8) * MaxHealth; if (!invalidMessage && i < Sections.Length) { - SetDamage(i, damage); + SetDamage(i, damage, isNetworkEvent: true); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 06cfbc539..614365566 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -35,6 +35,13 @@ namespace Barotrauma Rectangle camView = cam.WorldView; camView = new Rectangle(camView.X - CullMargin, camView.Y + CullMargin, camView.Width + CullMargin * 2, camView.Height + CullMargin * 2); + if (Level.Loaded?.Renderer?.CollapseEffectStrength is > 0.0f) + { + //force everything to be visible when the collapse effect (which moves everything to a single point) is active + camView = Rectangle.Union(AbsRect(camView.Location.ToVector2(), camView.Size.ToVector2()), new Rectangle(Point.Zero, Level.Loaded.Size)); + camView.Y += camView.Height; + } + if (Math.Abs(camView.X - prevCullArea.X) < CullMoveThreshold && Math.Abs(camView.Y - prevCullArea.Y) < CullMoveThreshold && Math.Abs(camView.Right - prevCullArea.Right) < CullMoveThreshold && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index b3f830140..8491dd736 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -116,7 +116,7 @@ namespace Barotrauma bool isMouseOnComponent = GUI.MouseOn == component; camera.MoveCamera(deltaTime, allowZoom: isMouseOnComponent, followSub: false); if (isMouseOnComponent && - (PlayerInput.MidButtonHeld() || PlayerInput.LeftButtonHeld())) + (PlayerInput.MidButtonHeld() || PlayerInput.PrimaryMouseButtonHeld())) { Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / camera.Zoom; moveSpeed.X = -moveSpeed.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 1d862e8a6..cded39e96 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -39,7 +39,7 @@ namespace Barotrauma { Color clr = CurrentHull == null ? Color.DodgerBlue : GUIStyle.Green; if (spawnType != SpawnType.Path) { clr = Color.Gray; } - if (isObstructed) + if (!IsTraversable) { clr = Color.Black; } @@ -84,7 +84,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, drawPos, new Vector2(e.DrawPosition.X, -e.DrawPosition.Y), - (isObstructed ? Color.Gray : GUIStyle.Green) * 0.7f, width: 5, depth: 0.002f); + (IsTraversable ? GUIStyle.Green : Color.Gray) * 0.7f, width: 5, depth: 0.002f); } if (ConnectedGap != null) { @@ -123,6 +123,11 @@ namespace Barotrauma } } } + else if (spawnType == SpawnType.ExitPoint && ExitPointSize != Point.Zero) + { + GUI.DrawRectangle(spriteBatch, drawPos - ExitPointSize.ToVector2() / 2, ExitPointSize.ToVector2(), Color.Cyan, thickness: 5); + } + GUIStyle.SmallFont.DrawString(spriteBatch, ID.ToString(), new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30), @@ -170,9 +175,9 @@ namespace Barotrauma if (PlayerInput.KeyDown(Keys.Space)) { - foreach (MapEntity e in mapEntityList) + foreach (MapEntity e in HighlightedEntities) { - if (!(e is WayPoint) || e == this || !e.IsHighlighted) { continue; } + if (e is not WayPoint || e == this) { continue; } if (linkedTo.Contains(e)) { @@ -251,6 +256,7 @@ namespace Barotrauma private bool ChangeSpawnType(GUIButton button, object obj) { + var prevSpawnType = spawnType; GUITextBlock spawnTypeText = button.Parent.GetChildByUserData("spawntypetext") as GUITextBlock; var values = (SpawnType[])Enum.GetValues(typeof(SpawnType)); int currIndex = values.IndexOf(spawnType); @@ -267,6 +273,7 @@ namespace Barotrauma } spawnType = values[currIndex]; spawnTypeText.Text = spawnType.ToString(); + if (spawnType == SpawnType.ExitPoint || prevSpawnType == SpawnType.ExitPoint) { CreateEditingHUD(); } return true; } @@ -412,6 +419,28 @@ namespace Barotrauma textBox.Text = string.Join(",", tags); textBox.Flash(GUIStyle.Green); }; + + if (SpawnType == SpawnType.ExitPoint) + { + var sizeField = GUI.CreatePointField(ExitPointSize, GUI.IntScale(20), TextManager.Get("dimensions"), paddedFrame.RectTransform); + GUINumberInput xField = null, yField = null; + foreach (GUIComponent child in sizeField.GetAllChildren()) + { + if (yField == null) + { + yField = child as GUINumberInput; + } + else + { + xField = child as GUINumberInput; + if (xField != null) { break; } + } + } + xField.MinValueInt = 0; + xField.OnValueChanged = (numberInput) => { ExitPointSize = new Point(numberInput.IntValue, ExitPointSize.Y); }; + yField.MinValueInt = 0; + yField.OnValueChanged = (numberInput) => { ExitPointSize = new Point(ExitPointSize.X, numberInput.IntValue); }; + } } editingHUD.RectTransform.Resize(new Point( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 460d06df1..6a5bfb3ee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -75,7 +75,7 @@ namespace Barotrauma.Networking float gain = 1.0f; float noiseGain = 0.0f; Vector3? position = null; - if (character != null) + if (character != null && !character.IsDead) { if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 1809817cb..f86c9cd48 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -332,7 +332,6 @@ namespace Barotrauma.Networking FileSize = 0 }; - Md5Hash.Cache.Remove(directTransfer.FilePath); OnFinished(directTransfer); } break; @@ -414,7 +413,6 @@ namespace Barotrauma.Networking { finishedTransfers.Add((transferId, Timing.TotalTime)); StopTransfer(activeTransfer); - Md5Hash.Cache.Remove(activeTransfer.FilePath); OnFinished(activeTransfer); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 34449232a..adf9fd222 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -76,6 +76,8 @@ namespace Barotrauma.Networking Interrupted } + private UInt16? debugStartGameCampaignSaveID; + private RoundInitStatus roundInitStatus = RoundInitStatus.NotStarted; public bool RoundStarting => roundInitStatus == RoundInitStatus.Starting || roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize; @@ -326,7 +328,7 @@ namespace Barotrauma.Networking return serverEndpoint switch { LidgrenEndpoint lidgrenEndpoint => new LidgrenClientPeer(lidgrenEndpoint, callbacks, ownerKey), - SteamP2PEndpoint _ when ownerKey is Some { Value: var key } => new SteamP2POwnerPeer(callbacks, key), + SteamP2PEndpoint _ when ownerKey.TryUnwrap(out var key) => new SteamP2POwnerPeer(callbacks, key), SteamP2PEndpoint steamP2PServerEndpoint when ownerKey.IsNone() => new SteamP2PClientPeer(steamP2PServerEndpoint, callbacks), _ => throw new ArgumentOutOfRangeException() }; @@ -859,17 +861,24 @@ namespace Barotrauma.Networking ContentFile file = ContentPackageManager.EnabledPackages.All .Select(p => p.Files.FirstOrDefault(f => f.Path == filePath)) - .FirstOrDefault(f => !(f is null)); + .FirstOrDefault(f => f is not null); contentToPreload.AddIfNotNull(file); } + string campaignErrorInfo = string.Empty; + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign) + { + campaignErrorInfo = $" Round start save ID: {debugStartGameCampaignSaveID}, last save id: {campaign.LastSaveID}, pending save id: {campaign.PendingSaveID}."; + } + GameMain.GameSession.EventManager.PreloadContent(contentToPreload); int subEqualityCheckValue = inc.ReadInt32(); if (subEqualityCheckValue != (Submarine.MainSub?.Info?.EqualityCheckVal ?? 0)) { - string errorMsg = "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server." + - " There may have been an error in receiving the up-to-date submarine file from the server."; + string errorMsg = + "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server. " + + $"There may have been an error in receiving the up-to-date submarine file from the server. Round init status: {roundInitStatus}." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:SubsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -886,7 +895,7 @@ namespace Barotrauma.Networking $"Mission equality check failed. Mission count doesn't match the server. " + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + - $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; + $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -899,7 +908,7 @@ namespace Barotrauma.Networking $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + - $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; + $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } @@ -922,7 +931,7 @@ namespace Barotrauma.Networking ", level value count: " + levelEqualityCheckValues.Count + ", seed: " + Level.Loaded.Seed + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + - ", mirrored: " + Level.Loaded.Mirrored + ")."; + ", 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); } @@ -988,7 +997,7 @@ namespace Barotrauma.Networking GameMain.ModDownloadScreen.Reset(); ContentPackageManager.EnabledPackages.Restore(); - CampaignMode.StartRoundCancellationToken?.Cancel(); + GameMain.GameSession?.Campaign?.CancelStartRound(); if (SteamManager.IsInitialized) { @@ -1322,6 +1331,8 @@ namespace Barotrauma.Networking eventErrorWritten = false; GameMain.NetLobbyScreen.StopWaitingForStartRound(); + debugStartGameCampaignSaveID = null; + while (CoroutineManager.IsCoroutineRunning("EndGame")) { EndCinematic?.Stop(); @@ -1471,19 +1482,12 @@ namespace Barotrauma.Networking roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } - else if (campaign.Map == null) - { - GameStarted = true; - DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet)."); - GameMain.NetLobbyScreen.Select(); - roundInitStatus = RoundInitStatus.Interrupted; - yield return CoroutineStatus.Failure; - } - if (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID)) + if (NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID) || + NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID)) { campaign.PendingSaveID = campaignSaveID; - DateTime saveFileTimeOut = DateTime.Now + new TimeSpan(0,0,60); + DateTime saveFileTimeOut = DateTime.Now + new TimeSpan(0, 0, 60); while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID)) { if (DateTime.Now > saveFileTimeOut) @@ -1494,16 +1498,27 @@ namespace Barotrauma.Networking roundInitStatus = RoundInitStatus.Interrupted; yield return CoroutineStatus.Failure; } - yield return new WaitForSeconds(0.1f); + yield return new WaitForSeconds(0.1f); } } + if (campaign.Map == null) + { + GameStarted = true; + DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet)."); + GameMain.NetLobbyScreen.Select(); + roundInitStatus = RoundInitStatus.Interrupted; + yield return CoroutineStatus.Failure; + } + campaign.Map.SelectLocation(selectedLocationIndex); LevelData levelData = nextLocationIndex > -1 ? campaign.Map.Locations[nextLocationIndex].LevelData : campaign.Map.Connections[nextConnectionIndex].LevelData; + debugStartGameCampaignSaveID = campaign.LastSaveID; + if (roundSummary != null) { loadTask = campaign.SelectSummaryScreen(roundSummary, levelData, mirrorLevel, null); @@ -2587,31 +2602,24 @@ namespace Barotrauma.Networking public void WriteCharacterInfo(IWriteMessage msg, string newName = null) { msg.WriteBoolean(characterInfo == null); + msg.WritePadBits(); if (characterInfo == null) { return; } - msg.WriteString(newName ?? string.Empty); + var head = characterInfo.Head; - msg.WriteByte((byte)characterInfo.Head.Preset.TagSet.Count); - foreach (Identifier tag in characterInfo.Head.Preset.TagSet) - { - msg.WriteIdentifier(tag); - } - msg.WriteByte((byte)characterInfo.Head.HairIndex); - msg.WriteByte((byte)characterInfo.Head.BeardIndex); - msg.WriteByte((byte)characterInfo.Head.MoustacheIndex); - msg.WriteByte((byte)characterInfo.Head.FaceAttachmentIndex); - msg.WriteColorR8G8B8(characterInfo.Head.SkinColor); - msg.WriteColorR8G8B8(characterInfo.Head.HairColor); - msg.WriteColorR8G8B8(characterInfo.Head.FacialHairColor); + var netInfo = new NetCharacterInfo( + NewName: newName ?? string.Empty, + Tags: head.Preset.TagSet.ToImmutableArray(), + HairIndex: (byte)head.HairIndex, + BeardIndex: (byte)head.BeardIndex, + MoustacheIndex: (byte)head.MoustacheIndex, + FaceAttachmentIndex: (byte)head.FaceAttachmentIndex, + SkinColor: head.SkinColor, + HairColor: head.HairColor, + FacialHairColor: head.FacialHairColor, + JobVariants: GameMain.NetLobbyScreen.JobPreferences.Select(NetJobVariant.FromJobVariant).ToImmutableArray()); - var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; - int count = Math.Min(jobPreferences.Count, 3); - msg.WriteByte((byte)count); - for (int i = 0; i < count; i++) - { - msg.WriteIdentifier(jobPreferences[i].Prefab.Identifier); - msg.WriteByte((byte)jobPreferences[i].Variant); - } + msg.WriteNetSerializableStruct(netInfo); } public void Vote(VoteType voteType, object data) @@ -2623,7 +2631,13 @@ namespace Barotrauma.Networking using (var segmentTable = SegmentTableWriter.StartWriting(msg)) { segmentTable.StartNewSegment(ClientNetSegment.Vote); - Voting.ClientWrite(msg, voteType, data); + bool succeeded = Voting.ClientWrite(msg, voteType, data); + if (!succeeded) + { + throw new Exception( + $"Failed to write vote of type {voteType}: " + + $"data was of invalid type {data?.GetType().Name ?? "NULL"}"); + } } ClientPeer.Send(msg, DeliveryMethod.Reliable); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index c51b66457..3cff1e7b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -92,10 +92,10 @@ namespace Barotrauma.Networking Name = GameMain.Client.Name, OwnerKey = ownerKey, SteamId = SteamManager.GetSteamId().Select(id => (AccountId)id), - SteamAuthTicket = steamAuthTicket switch + SteamAuthTicket = steamAuthTicket?.Data switch { null => Option.None(), - var ticket => Option.Some(ticket.Data) + var ticketData => Option.Some(ticketData) }, GameVersion = GameMain.Version.ToString(), Language = GameSettings.CurrentConfig.Language.Value diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 54c4d7ca2..5c77d37c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -156,7 +156,7 @@ namespace Barotrauma.Networking var packet = INetSerializableStruct.Read(inc); - packet.SteamAuthTicket.TryUnwrap(out byte[] ticket); + packet.SteamAuthTicket.TryUnwrap(out var ticket); Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId); if (authSessionStartState != Steamworks.BeginAuthResult.OK) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index adbf863df..23a9f6bef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -1,5 +1,6 @@ #nullable enable +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -69,6 +70,9 @@ namespace Barotrauma.Networking [Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)] public PlayStyle PlayStyle { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public LanguageIdentifier Language { get; set; } public Version GameVersion { get; set; } = new Version(0, 0, 0, 0); @@ -281,7 +285,7 @@ namespace Barotrauma.Networking // ----------------------------------------------------------------------------- - float elementHeight = 0.075f; + const float elementHeight = 0.075f; // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); @@ -294,6 +298,11 @@ namespace Barotrauma.Networking serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); }; msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); }; + var languageLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("Language")); + new GUITextBlock(new RectTransform(Vector2.One, languageLabel.RectTransform), + ServerLanguageOptions.Options.FirstOrNull(o => o.Identifier == Language)?.Label ?? TextManager.Get("Unknown"), + textAlignment: Alignment.Right); + var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode")); new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform), TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), @@ -363,7 +372,7 @@ namespace Barotrauma.Networking packageText.Selected = true; } //workshop download link found - else if (package.Id is Some { Value: var ugcId } && ugcId is SteamWorkshopId) + else if (package.Id.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId) { packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", package.Name); } @@ -417,8 +426,9 @@ namespace Barotrauma.Networking GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty; if (Enum.TryParse(valueGetter("traitors"), out YesNoMaybe traitorsEnabled)) { TraitorsEnabled = traitorsEnabled; } if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } + Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None; - ContentPackages = ExtractContentPackageInfo(valueGetter).ToImmutableArray(); + ContentPackages = ExtractContentPackageInfo(ServerName, valueGetter).ToImmutableArray(); bool getBool(string key) { @@ -427,8 +437,34 @@ namespace Barotrauma.Networking } } - private static ContentPackageInfo[] ExtractContentPackageInfo(Func valueGetter) + private static ContentPackageInfo[] ExtractContentPackageInfo(string serverName, Func valueGetter) { + //workaround to ServerRules queries truncating the values to 255 bytes + int individualPackageIndex = 0; + string? individualPackage = valueGetter($"contentpackage{individualPackageIndex}"); + if (!individualPackage.IsNullOrEmpty()) + { + List contentPackages = new List(); + do + { + string[] splitPackageInfo = individualPackage.Split(','); + if (splitPackageInfo.Length != 3) + { + DebugConsole.Log( + $"Error in a server's content package list: malformed content package info ({individualPackage})."); + return Array.Empty(); + } + string name = splitPackageInfo[0]; + string hash = splitPackageInfo[1]; + ulong.TryParse(splitPackageInfo[2], out ulong id); + contentPackages.Add(new ContentPackageInfo(name, hash, Option.Some(new SteamWorkshopId(id)))); + + individualPackageIndex++; + individualPackage = valueGetter($"contentpackage{individualPackageIndex}"); + } while (!individualPackage.IsNullOrEmpty()); + return contentPackages.ToArray(); + } + string? joinedNames = valueGetter("contentpackage"); string? joinedHashes = valueGetter("contentpackagehash"); string? joinedWorkshopIds = valueGetter("contentpackageid"); @@ -438,9 +474,11 @@ namespace Barotrauma.Networking #warning TODO: genericize ulong[] contentPackageIds = joinedWorkshopIds.IsNullOrEmpty() ? new ulong[1] : SteamManager.ParseWorkshopIds(joinedWorkshopIds).ToArray(); - if (contentPackageNames.Length != contentPackageHashes.Length - || contentPackageHashes.Length != contentPackageIds.Length) + if (contentPackageNames.Length != contentPackageHashes.Length || contentPackageHashes.Length != contentPackageIds.Length) { + DebugConsole.Log( + $"The number of names, hashes and Workshop IDs on server \"{serverName}\"" + + $" doesn't match: {contentPackageNames.Length} names ({string.Join(", ", contentPackageNames)}), {contentPackageHashes.Length} hashes, {contentPackageIds.Length} ids)"); return Array.Empty(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs index ff9079caf..678d89cdc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamDedicatedServerProvider.cs @@ -35,7 +35,7 @@ namespace Barotrauma } private static Option InfoFromListEntry(Steamworks.Data.ServerInfo entry) => - entry.Name.IsNullOrEmpty() + entry.Name.IsNullOrEmpty() || entry.Address is null ? Option.None() : Option.Some(new ServerInfo(new LidgrenEndpoint(entry.Address, entry.ConnectionPort)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs index fe80749e5..caf2e0a20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerProviders/SteamP2PServerProvider.cs @@ -71,10 +71,10 @@ namespace Barotrauma foreach (var lobby in lobbies) { - string lobbyOwnerStr = lobby.GetData("lobbyowner"); + string lobbyOwnerStr = lobby.GetData("lobbyowner") ?? ""; lobbyQuery = lobbyQuery.WithoutKeyValue("lobbyowner", lobbyOwnerStr); - string serverName = lobby.GetData("name"); + string serverName = lobby.GetData("name") ?? ""; if (string.IsNullOrEmpty(serverName)) { continue; } var ownerId = SteamId.Parse(lobbyOwnerStr); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index c6a97de44..2039ad51d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -3,11 +3,15 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using Barotrauma.Steam; namespace Barotrauma.Networking { partial class ServerSettings : ISerializableEntity { + private static readonly LocalizedString packetAmountTooltip = TextManager.Get("ServerSettingsMaxPacketAmountTooltip"); + private static readonly RichString packetAmountTooltipWarning = RichString.Rich($"{packetAmountTooltip}\n\n‖color:gui.red‖{TextManager.Get("PacketLimitWarning")}‖end‖"); + partial class NetPropertyData { public GUIComponent GUIComponent; @@ -27,7 +31,15 @@ namespace Barotrauma.Networking if (GUIComponent == null) return null; else if (GUIComponent is GUITickBox tickBox) return tickBox.Selected; else if (GUIComponent is GUITextBox textBox) return textBox.Text; - else if (GUIComponent is GUIScrollBar scrollBar) return scrollBar.BarScrollValue; + else if (GUIComponent is GUIScrollBar scrollBar) + { + if (property.PropertyType == typeof(int)) + { + return (int)MathF.Floor(scrollBar.BarScrollValue); + } + + return scrollBar.BarScrollValue; + } else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) return radioButtonGroup.Selected; else if (GUIComponent is GUIDropDown dropdown) return dropdown.SelectedData; else if (GUIComponent is GUINumberInput numInput) @@ -43,9 +55,9 @@ namespace Barotrauma.Networking else if (GUIComponent is GUITextBox textBox) textBox.Text = (string)value; else if (GUIComponent is GUIScrollBar scrollBar) { - if (value.GetType() == typeof(int)) + if (value is int i) { - scrollBar.BarScrollValue = (int)value; + scrollBar.BarScrollValue = i; } else { @@ -78,7 +90,7 @@ namespace Barotrauma.Networking } } private Dictionary tempMonsterEnabled; - + partial void InitProjSpecific() { var properties = TypeDescriptor.GetProperties(GetType()).Cast(); @@ -367,6 +379,15 @@ namespace Barotrauma.Networking //*********************************************** + // Language + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("Language"), font: GUIStyle.SubHeadingFont); + var languageDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform)); + foreach (var language in ServerLanguageOptions.Options) + { + languageDD.AddItem(language.Label, language.Identifier); + } + GetPropertyData(nameof(Language)).AssignGUIComponent(languageDD); + //changing server visibility on the fly is not supported in dedicated servers if (GameMain.Client?.ClientPeer is not LidgrenClientPeer) { @@ -931,11 +952,58 @@ namespace Barotrauma.Networking return true; }; + + GUILayoutGroup karmaAndDosLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), antigriefingTab.RectTransform), isHorizontal: false); + GUILayoutGroup lowerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true); + GUILayoutGroup upperLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true); + // karma -------------------------------------------------------------------------- - var karmaBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), TextManager.Get("ServerSettingsUseKarma")); + var karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsUseKarma")); GetPropertyData(nameof(KarmaEnabled)).AssignGUIComponent(karmaBox); + var enableDosProtection = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsEnableDoSProtection")) + { + ToolTip = TextManager.Get("ServerSettingsEnableDoSProtectionTooltip") + }; + GetPropertyData(nameof(EnableDoSProtection)).AssignGUIComponent(enableDosProtection); + + CreateLabeledSlider(lowerLayout, "ServerSettingsMaxPacketAmount", out GUIScrollBar maxPacketSlider, out GUITextBlock maxPacketSliderLabel); + LocalizedString maxPacketCountLabel = maxPacketSliderLabel.Text; + maxPacketSlider.Step = 0.001f; + maxPacketSlider.Range = new Vector2(PacketLimitMin, PacketLimitMax); + maxPacketSlider.ToolTip = packetAmountTooltip; + maxPacketSlider.OnMoved = (scrollBar, _) => + { + GUITextBlock textBlock = (GUITextBlock)scrollBar.UserData; + int value = (int)MathF.Floor(scrollBar.BarScrollValue); + + LocalizedString valueText = value > PacketLimitMin + ? value.ToString() + : TextManager.Get("ServerSettingsNoLimit"); + + switch (value) + { + case <= PacketLimitMin: + textBlock.TextColor = GUIStyle.Green; + scrollBar.ToolTip = packetAmountTooltip; + break; + case < PacketLimitWarning: + textBlock.TextColor = GUIStyle.Red; + scrollBar.ToolTip = packetAmountTooltipWarning; + break; + default: + textBlock.TextColor = GUIStyle.TextColorNormal; + scrollBar.ToolTip = packetAmountTooltip; + break; + } + + textBlock.Text = $"{maxPacketCountLabel} {valueText}"; + return true; + }; + GetPropertyData(nameof(MaxPacketAmount)).AssignGUIComponent(maxPacketSlider); + maxPacketSlider.OnMoved(maxPacketSlider, maxPacketSlider.BarScroll); + karmaPresetDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform)); foreach (string karmaPreset in GameMain.NetworkMember.KarmaManager.Presets.Keys) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index cde031084..765d1a5d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -276,6 +276,8 @@ namespace Barotrauma.Networking if (GameMain.Client?.Character != null) { var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; + if (GameMain.Client.Character.IsDead) { messageType = ChatMessageType.Dead; } + GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); } //encode audio and enqueue it diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index bf262be14..28d0461ad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -116,7 +116,9 @@ namespace Barotrauma.Networking bool spectating = Character.Controlled == null; float rangeMultiplier = spectating ? 2.0f : 1.0f; WifiComponent radio = null; - var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default; + var messageType = + !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) && ChatMessage.CanUseRadio(Character.Controlled) ? + ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; @@ -144,7 +146,7 @@ namespace Barotrauma.Networking if ((client.VoipSound.CurrentAmplitude * client.VoipSound.Gain * GameMain.SoundManager.GetCategoryGainMultiplier("voip")) > 0.1f) //TODO: might need to tweak { - if (client.Character != null && !client.Character.Removed) + if (client.Character != null && !client.Character.Removed && !client.Character.IsDead) { Vector3 clientPos = new Vector3(client.Character.WorldPosition.X, client.Character.WorldPosition.Y, 0.0f); Vector3 listenerPos = GameMain.SoundManager.ListenerPosition; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 67ba366d8..466b9418b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -13,13 +13,11 @@ namespace Barotrauma { public SubmarineInfo SubmarineInfo { get; set; } public bool TransferItems { get; set; } - public int DeliveryFee { get; set; } - public SubmarineVoteInfo(SubmarineInfo submarineInfo, bool transferItems, int deliveryFee) + public SubmarineVoteInfo(SubmarineInfo submarineInfo, bool transferItems) { SubmarineInfo = submarineInfo; TransferItems = transferItems; - DeliveryFee = deliveryFee; } } @@ -128,64 +126,72 @@ namespace Barotrauma UpdateVoteTexts(connectedClients, VoteType.Sub); } - public void ClientWrite(IWriteMessage msg, VoteType voteType, object data) + /// + /// Returns true if the given data is valid for the given vote type, + /// returns false otherwise. If it returns false, the message must + /// be discarded or reset by the caller, as it is now malformed :) + /// + public bool ClientWrite(IWriteMessage msg, VoteType voteType, object data) { msg.WriteByte((byte)voteType); switch (voteType) { case VoteType.Sub: - if (!(data is SubmarineInfo sub)) { return; } + if (data is not SubmarineInfo sub) { return false; } msg.WriteInt32(sub.EqualityCheckVal); - if (sub.EqualityCheckVal == 0) + if (sub.EqualityCheckVal <= 0) { //sub doesn't exist client-side, use hash to let the server know which one we voted for msg.WriteString(sub.MD5Hash.StringRepresentation); } break; case VoteType.Mode: - if (!(data is GameModePreset gameMode)) { return; } + if (data is not GameModePreset gameMode) { return false; } msg.WriteIdentifier(gameMode.Identifier); break; case VoteType.EndRound: - if (!(data is bool)) { return; } - msg.WriteBoolean((bool)data); + if (data is not bool endRound) { return false; } + msg.WriteBoolean(endRound); break; case VoteType.Kick: - if (!(data is Client votedClient)) { return; } + if (data is not Client votedClient) { return false; } msg.WriteByte(votedClient.SessionId); break; case VoteType.StartRound: - if (!(data is bool)) { return; } - msg.WriteBoolean((bool)data); + if (data is not bool startRound) { return false; } + msg.WriteBoolean(startRound); break; case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: case VoteType.SwitchSub: - if (data is (SubmarineInfo voteSub, bool transferItems)) - { - //initiate sub vote - msg.WriteBoolean(true); - msg.WriteString(voteSub.Name); - msg.WriteBoolean(transferItems); - } - else + switch (data) { - // vote - if (!(data is int)) { return; } - msg.WriteBoolean(false); - msg.WriteInt32((int)data); + case (SubmarineInfo voteSub, bool transferItems): + //initiate sub vote + msg.WriteBoolean(true); + msg.WriteString(voteSub.Name); + msg.WriteBoolean(transferItems); + break; + case int vote: + // vote + msg.WriteBoolean(false); + msg.WriteInt32(vote); + break; + default: + return false; } break; case VoteType.TransferMoney: - if (!(data is int)) { return; } + if (data is not int money) { return false; } msg.WriteBoolean(false); //not initiating a vote - msg.WriteInt32((int)data); + msg.WriteInt32(money); break; } msg.WritePadBits(); + return true; } public void ClientRead(IReadMessage inc) @@ -322,33 +328,34 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: string subName2 = inc.ReadString(); - var submarineInfo = GameMain.GameSession.OwnedSubmarines.FirstOrDefault(s => s.Name == subName2) ?? GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); bool transferItems = inc.ReadBoolean(); - int deliveryFee = inc.ReadInt16(); - if (submarineInfo == null) + if (GameMain.GameSession != null) { - DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); - return; + var submarineInfo = GameMain.GameSession.OwnedSubmarines.FirstOrDefault(s => s.Name == subName2) ?? GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); + if (submarineInfo == null) + { + DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); + return; + } + submarineVoteInfo = new SubmarineVoteInfo(submarineInfo, transferItems); } - submarineVoteInfo = new SubmarineVoteInfo(submarineInfo, transferItems, deliveryFee); break; } - GameMain.Client.VotingInterface?.EndVote(passed, yesClientCount, noClientCount); - + GameMain.Client.VotingInterface?.EndVote(passed, yesClientCount, noClientCount); if (passed && submarineVoteInfo.SubmarineInfo is { } subInfo) { switch (voteType) { case VoteType.PurchaseAndSwitchSub: GameMain.GameSession.PurchaseSubmarine(subInfo); - GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, 0); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); break; case VoteType.PurchaseSub: GameMain.GameSession.PurchaseSubmarine(subInfo); break; case VoteType.SwitchSub: - GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, submarineVoteInfo.DeliveryFee); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 468f07e54..55d0d15cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -85,6 +85,9 @@ namespace Barotrauma.Particles [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the entity heading direction be applied to the particle rotation? Only affects after flipping the texture and when CopyEntityAngle is true.")] public bool CopyEntityDir { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Only relevant for status effects. Makes the emitter copy the angle from the target of the effect instead of the entity applying the effect.")] + public bool CopyTargetAngle { get; set; } + [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] public Color ColorMultiplier { get; set; } @@ -203,7 +206,7 @@ namespace Barotrauma.Particles position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); } - var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); + var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); if (particle != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index dfdd7342a..9aa60c592 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -75,21 +75,21 @@ 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, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { if (prefab == null || prefab.Sprites.Count == 0) { return null; } - if (particleCount >= MaxParticles) { for (int i = 0; i < particleCount; i++) { - if (particles[i].Prefab.Priority < prefab.Priority) + if (particles[i].Prefab.Priority < prefab.Priority || + (!particles[i].Prefab.DrawAlways && prefab.DrawAlways)) { RemoveParticle(i); break; } } - if (particleCount >= MaxParticles) { return null; } + if (particleCount >= MaxParticles) { return null; } } Vector2 particleEndPos = prefab.CalculateEndPosition(position, velocity); @@ -109,26 +109,30 @@ namespace Barotrauma.Particles Rectangle expandedViewRect = MathUtils.ExpandRect(cam.WorldView, MaxOutOfViewDist); - if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } - if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } + if (!prefab.DrawAlways) + { + if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } + if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } + } if (particles[particleCount] == null) { particles[particleCount] = new Particle(); } - particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); + particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, prefab.DrawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); particleCount++; return particles[particleCount - 1]; } - public List GetPrefabList() + public static List GetPrefabList() { return ParticlePrefab.Prefabs.ToList(); } - public ParticlePrefab FindPrefab(string prefabName) + public static ParticlePrefab FindPrefab(string prefabName) { - return ParticlePrefab.Prefabs.Find(p => p.Identifier == prefabName); + ParticlePrefab.Prefabs.TryGet(prefabName, out ParticlePrefab prefab); + return prefab; } private void RemoveParticle(int index) @@ -170,7 +174,7 @@ namespace Barotrauma.Particles remove = true; } - if (remove) RemoveParticle(i); + if (remove) { RemoveParticle(i); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index e758564c5..0b7aa21af 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -185,6 +185,9 @@ namespace Barotrauma.Particles [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the particle be always rendered on top of entities?")] public bool DrawOnTop { 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; } + [Editable, Serialize(ParticleBlendState.AlphaBlend, IsPropertySaveable.No, description: "The type of blending to use when rendering the particle.")] public ParticleBlendState BlendState { get; private set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index a0ac4f61a..8f61bf354 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -19,7 +19,7 @@ namespace Barotrauma public void Draw(DeformableSprite deformSprite, Camera cam, Vector2 scale, Color color, bool invert = false) { - if (!Enabled) return; + if (!Enabled) { return; } UpdateDrawPosition(); deformSprite?.Draw(cam, new Vector3(DrawPosition, MathHelper.Clamp(deformSprite.Sprite.Depth, 0, 1)), @@ -30,9 +30,9 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Sprite sprite, Color color, float? depth = null, float scale = 1.0f, bool mirrorX = false, bool mirrorY = false) { - if (!Enabled) return; + if (!Enabled) { return; } UpdateDrawPosition(); - if (sprite == null) return; + if (sprite == null) { return; } SpriteEffects spriteEffect = (Dir == 1.0f) ? SpriteEffects.None : SpriteEffects.FlipHorizontally; if (mirrorX) { @@ -79,13 +79,13 @@ namespace Barotrauma new Vector2(DrawPosition.X, -DrawPosition.Y), Color.Cyan, 0, 5); } - if (bodyShapeTexture == null && IsValidShape(radius, height, width)) + if (bodyShapeTexture == null && IsValidShape(Radius, Height, Width)) { switch (BodyShape) { case Shape.Rectangle: { - float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(width), ConvertUnits.ToDisplayUnits(height)); + float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(Width), ConvertUnits.ToDisplayUnits(Height)); if (maxSize > 128.0f) { bodyShapeTextureScale = 128.0f / maxSize; @@ -96,14 +96,14 @@ namespace Barotrauma } bodyShapeTexture = GUI.CreateRectangle( - (int)ConvertUnits.ToDisplayUnits(width * bodyShapeTextureScale), - (int)ConvertUnits.ToDisplayUnits(height * bodyShapeTextureScale)); + (int)ConvertUnits.ToDisplayUnits(Width * bodyShapeTextureScale), + (int)ConvertUnits.ToDisplayUnits(Height * bodyShapeTextureScale)); break; } case Shape.Capsule: case Shape.HorizontalCapsule: { - float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(radius), ConvertUnits.ToDisplayUnits(Math.Max(height, width))); + float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(Radius), ConvertUnits.ToDisplayUnits(Math.Max(Height, Width))); if (maxSize > 128.0f) { bodyShapeTextureScale = 128.0f / maxSize; @@ -114,20 +114,20 @@ namespace Barotrauma } bodyShapeTexture = GUI.CreateCapsule( - (int)ConvertUnits.ToDisplayUnits(radius * bodyShapeTextureScale), - (int)ConvertUnits.ToDisplayUnits(Math.Max(height, width) * bodyShapeTextureScale)); + (int)ConvertUnits.ToDisplayUnits(Radius * bodyShapeTextureScale), + (int)ConvertUnits.ToDisplayUnits(Math.Max(Height, Width) * bodyShapeTextureScale)); break; } case Shape.Circle: - if (ConvertUnits.ToDisplayUnits(radius) > 128.0f) + if (ConvertUnits.ToDisplayUnits(Radius) > 128.0f) { - bodyShapeTextureScale = 128.0f / ConvertUnits.ToDisplayUnits(radius); + bodyShapeTextureScale = 128.0f / ConvertUnits.ToDisplayUnits(Radius); } else { bodyShapeTextureScale = 1.0f; } - bodyShapeTexture = GUI.CreateCircle((int)ConvertUnits.ToDisplayUnits(radius * bodyShapeTextureScale)); + bodyShapeTexture = GUI.CreateCircle((int)ConvertUnits.ToDisplayUnits(Radius * bodyShapeTextureScale)); break; default: throw new NotImplementedException(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index 23c90ff23..07ebfee4b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -11,15 +11,13 @@ namespace Barotrauma public enum MouseButton { None = -1, - LeftMouse = 0, - RightMouse = 1, + PrimaryMouse = 0, + SecondaryMouse = 1, MiddleMouse = 2, MouseButton4 = 3, MouseButton5 = 4, MouseWheelUp = 5, - MouseWheelDown = 6, - PrimaryMouse, - SecondaryMouse + MouseWheelDown = 6 } public class KeyOrMouse @@ -65,10 +63,6 @@ namespace Barotrauma return PlayerInput.PrimaryMouseButtonHeld(); case MouseButton.SecondaryMouse: return PlayerInput.SecondaryMouseButtonHeld(); - case MouseButton.LeftMouse: - return PlayerInput.LeftButtonHeld(); - case MouseButton.RightMouse: - return PlayerInput.RightButtonHeld(); case MouseButton.MiddleMouse: return PlayerInput.MidButtonHeld(); case MouseButton.MouseButton4: @@ -95,10 +89,6 @@ namespace Barotrauma return PlayerInput.PrimaryMouseButtonClicked(); case MouseButton.SecondaryMouse: return PlayerInput.SecondaryMouseButtonClicked(); - case MouseButton.LeftMouse: - return PlayerInput.LeftButtonClicked(); - case MouseButton.RightMouse: - return PlayerInput.RightButtonClicked(); case MouseButton.MiddleMouse: return PlayerInput.MidButtonClicked(); case MouseButton.MouseButton4: @@ -218,11 +208,11 @@ namespace Barotrauma switch (MouseButton) { case MouseButton.PrimaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse"); + return PlayerInput.PrimaryMouseLabel; case MouseButton.SecondaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse"); + return PlayerInput.SecondaryMouseLabel; default: - return TextManager.Get("input." + MouseButton.ToString().ToLowerInvariant()); + return TextManager.Get($"Input.{MouseButton}"); } } else @@ -270,6 +260,9 @@ namespace Barotrauma } #endif + public static readonly LocalizedString PrimaryMouseLabel = TextManager.Get($"Input.{(!MouseButtonsSwapped() ? "Left" : "Right")}Mouse"); + public static readonly LocalizedString SecondaryMouseLabel = TextManager.Get($"Input.{(!MouseButtonsSwapped() ? "Right" : "Left")}Mouse"); + public static Vector2 MousePosition { get { return new Vector2(mouseState.Position.X, mouseState.Position.Y); } @@ -317,120 +310,48 @@ namespace Barotrauma } public static bool PrimaryMouseButtonHeld() - { - if (MouseButtonsSwapped()) - { - return RightButtonHeld(); - } - return LeftButtonHeld(); - } - - public static bool PrimaryMouseButtonDown() - { - if (MouseButtonsSwapped()) - { - return RightButtonDown(); - } - return LeftButtonDown(); - } - - public static bool PrimaryMouseButtonReleased() - { - if (MouseButtonsSwapped()) - { - return RightButtonReleased(); - } - return LeftButtonReleased(); - } - - public static bool PrimaryMouseButtonClicked() - { - if (MouseButtonsSwapped()) - { - return RightButtonClicked(); - } - return LeftButtonClicked(); - } - - public static bool SecondaryMouseButtonHeld() - { - if (!MouseButtonsSwapped()) - { - return RightButtonHeld(); - } - return LeftButtonHeld(); - } - - public static bool SecondaryMouseButtonDown() - { - if (!MouseButtonsSwapped()) - { - return RightButtonDown(); - } - return LeftButtonDown(); - } - - public static bool SecondaryMouseButtonReleased() - { - if (!MouseButtonsSwapped()) - { - return RightButtonReleased(); - } - return LeftButtonReleased(); - } - - public static bool SecondaryMouseButtonClicked() - { - if (!MouseButtonsSwapped()) - { - return RightButtonClicked(); - } - return LeftButtonClicked(); - } - - public static bool LeftButtonHeld() { return AllowInput && mouseState.LeftButton == ButtonState.Pressed; } - public static bool LeftButtonDown() + public static bool PrimaryMouseButtonDown() { return AllowInput && oldMouseState.LeftButton == ButtonState.Released && mouseState.LeftButton == ButtonState.Pressed; } - public static bool LeftButtonReleased() + public static bool PrimaryMouseButtonReleased() { return AllowInput && mouseState.LeftButton == ButtonState.Released; } - public static bool LeftButtonClicked() + public static bool PrimaryMouseButtonClicked() { return (AllowInput && oldMouseState.LeftButton == ButtonState.Pressed && mouseState.LeftButton == ButtonState.Released); } - public static bool RightButtonHeld() + public static bool SecondaryMouseButtonHeld() { return AllowInput && mouseState.RightButton == ButtonState.Pressed; } - public static bool RightButtonDown() + public static bool SecondaryMouseButtonDown() { return AllowInput && oldMouseState.RightButton == ButtonState.Released && mouseState.RightButton == ButtonState.Pressed; } - public static bool RightButtonReleased() + public static bool SecondaryMouseButtonReleased() { return AllowInput && mouseState.RightButton == ButtonState.Released; } - public static bool RightButtonClicked() + public static bool SecondaryMouseButtonClicked() { return (AllowInput && oldMouseState.RightButton == ButtonState.Pressed diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 3da38f7df..1440b125b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -169,7 +169,10 @@ namespace Barotrauma sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); if (ContentPackageManager.EnabledPackages.All != null) { - sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); + sb.AppendLine("Selected content packages: " + + (!ContentPackageManager.EnabledPackages.All.Any() ? + "None" : + string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => $"{c.Name} ({c.Hash?.ShortRepresentation ?? "unknown"})")))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs index 17e268d1e..fc8859439 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs @@ -7,17 +7,13 @@ namespace Barotrauma { class CampaignEndScreen : Screen { - private Video video; - private readonly CreditsPlayer creditsPlayer; private readonly Camera cam; public Action OnFinished; - private LocalizedString textOverlay; - private float textOverlayTimer; - private Vector2 textOverlaySize; + protected SlideshowPlayer slideshowPlayer; public CampaignEndScreen() { @@ -27,43 +23,45 @@ namespace Barotrauma ScrollBarEnabled = false, AllowMouseWheelScroll = false }; - new GUIButton(new RectTransform(new Vector2(0.1f), creditsPlayer.RectTransform, Anchor.BottomRight, maxSize: new Point(300, 50)) { AbsoluteOffset = new Point(GUI.IntScale(20)) }, - TextManager.Get("close")) + creditsPlayer.CloseButton.OnClicked = (btn, userdata) => { - OnClicked = (btn, userdata) => - { - creditsPlayer.Scroll = 1.0f; - return true; - } + creditsPlayer.Scroll = 1.0f; + return true; }; + cam = new Camera(); } public override void Select() { base.Select(); - - textOverlay = ToolBox.WrapText(TextManager.Get("campaignend1"), GameMain.GraphicsWidth / 3, GUIStyle.Font); - textOverlaySize = GUIStyle.Font.MeasureString(textOverlay); - textOverlayTimer = 0.0f; - - video = Video.Load(GameMain.GraphicsDeviceManager.GraphicsDevice, GameMain.SoundManager, "Content/SplashScreens/Ending.webm"); - video.Play(); + if (SlideshowPrefab.Prefabs.TryGet("campaignending".ToIdentifier(), out var slideshow)) + { + slideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow); + } creditsPlayer.Restart(); creditsPlayer.Visible = false; - SteamAchievementManager.UnlockAchievement("campaigncompleted".ToIdentifier(), unlockClients: true); + UnlockAchievement("campaigncompleted"); + UnlockAchievement( + GameMain.GameSession is { Campaign.Settings.RadiationEnabled: true } ? + "campaigncompleted_radiationenabled" : + "campaigncompleted_radiationdisabled"); + + static void UnlockAchievement(string id) + { + SteamAchievementManager.UnlockAchievement(id.ToIdentifier(), unlockClients: true); + } } public override void Deselect() { - video?.Dispose(); - video = null; GUI.HideCursor = false; SoundPlayer.OverrideMusicType = Identifier.Empty; } public override void Update(double deltaTime) { + slideshowPlayer?.UpdateManually((float)deltaTime); if (creditsPlayer.Finished) { OnFinished?.Invoke(); @@ -73,46 +71,18 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { - spriteBatch.Begin(); + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); graphics.Clear(Color.Black); - if (video.IsPlaying) + SoundPlayer.OverrideMusicType = "ending".ToIdentifier(); + if (slideshowPlayer != null && !slideshowPlayer.Finished) { - GUI.HideCursor = !GUI.PauseMenuOpen; - spriteBatch.Draw(video.GetTexture(), new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); + slideshowPlayer.DrawManually(spriteBatch); } else { - SoundPlayer.OverrideMusicType = "ending".ToIdentifier(); - float duration = 20.0f; - float creditsDelay = 3.0f; - if (textOverlayTimer < duration + creditsDelay) - { - float textAlpha; - float fadeInTime = 5.0f, fadeOutTime = 3.0f; - textOverlayTimer += (float)deltaTime; - if (textOverlayTimer < fadeInTime) - { - textAlpha = textOverlayTimer / fadeInTime; - } - else if (textOverlayTimer > duration - fadeOutTime) - { - textAlpha = Math.Min((duration - textOverlayTimer) / fadeOutTime, 1.0f); - } - else - { - textAlpha = 1.0f; - } - GUIStyle.Font.DrawString(spriteBatch, textOverlay, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 - textOverlaySize / 2, Color.White * textAlpha); - } - else - { - GUI.HideCursor = false; - creditsPlayer.Visible = true; - } + GUI.HideCursor = false; + creditsPlayer.Visible = true; } - spriteBatch.End(); - - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); GUI.Draw(cam, spriteBatch); spriteBatch.End(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 0bc8adcd3..bb8f98570 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -38,7 +38,6 @@ namespace Barotrauma protected set; } - public CampaignSettings CurrentSettings = new CampaignSettings(element: null); public GUIButton CampaignCustomizeButton { get; set; } public GUIMessageBox CampaignCustomizeSettings { get; set; } @@ -124,6 +123,7 @@ namespace Barotrauma public struct CampaignSettingElements { + public SettingValue SelectedPreset; public SettingValue TutorialEnabled; public SettingValue RadiationEnabled; public SettingValue MaxMissionCount; @@ -135,6 +135,7 @@ namespace Barotrauma { return new CampaignSettings(element: null) { + PresetName = SelectedPreset.GetValue(), TutorialEnabled = TutorialEnabled.GetValue(), RadiationEnabled = RadiationEnabled.GetValue(), MaxMissionCount = MaxMissionCount.GetValue(), @@ -185,9 +186,13 @@ namespace Barotrauma { const float verticalSize = 0.14f; + bool loadingPreset = false; + GUILayoutGroup presetDropdownLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), TextManager.Get("campaignsettingpreset")); - GUIDropDown presetDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), elementCount: CampaignModePresets.List.Length); + GUIDropDown presetDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), elementCount: CampaignModePresets.List.Length + 1); + presetDropdown.AddItem(TextManager.Get("karmapreset.custom"), null); + presetDropdown.Select(0); presetDropdownLayout.RectTransform.MinSize = new Point(0, presetDropdown.Rect.Height); @@ -195,21 +200,30 @@ namespace Barotrauma { string name = settings.PresetName; presetDropdown.AddItem(TextManager.Get($"preset.{name}").Fallback(name), settings); + + if (settings.PresetName.Equals(prevSettings.PresetName, StringComparison.OrdinalIgnoreCase)) + { + presetDropdown.SelectItem(settings); + } } + var presetValue = new SettingValue( + get: () => presetDropdown.SelectedData is CampaignSettings settings ? settings.PresetName : string.Empty, + set: static _ => { }); // we do not need a way to set this value + GUIListBox settingsList = new GUIListBox(new RectTransform(new Vector2(1f, 1f - verticalSize), parent.RectTransform)) { Spacing = GUI.IntScale(5) }; SettingValue tutorialEnabled = isSinglePlayer ? - CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize) : - new SettingValue(() => false, b => { }); - SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize); + CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize, OnValuesChanged) : + new SettingValue(static () => false, static _ => { }); + SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize, OnValuesChanged); ImmutableArray> startingSetOptions = StartItemSet.Sets.OrderBy(s => s.Order).Select(set => new SettingCarouselElement(set.Identifier, $"startitemset.{set.Identifier}")).ToImmutableArray(); SettingCarouselElement prevStartingSet = startingSetOptions.FirstOrNull(element => element.Value == prevSettings.StartItemSet) ?? startingSetOptions[1]; - SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions); + SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions, OnValuesChanged); ImmutableArray> fundOptions = ImmutableArray.Create( new SettingCarouselElement(StartingBalanceAmount.Low, "startingfunds.low"), @@ -218,7 +232,7 @@ namespace Barotrauma ); SettingCarouselElement prevStartingFund = fundOptions.FirstOrNull(element => element.Value == prevSettings.StartingBalanceAmount) ?? fundOptions[1]; - SettingValue startingFundsInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startingfundsdescription"), TextManager.Get("startingfundstooltip"), prevStartingFund, verticalSize, fundOptions); + SettingValue startingFundsInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startingfundsdescription"), TextManager.Get("startingfundstooltip"), prevStartingFund, verticalSize, fundOptions, OnValuesChanged); ImmutableArray> difficultyOptions = ImmutableArray.Create( new SettingCarouselElement(GameDifficulty.Easy, "difficulty.easy"), @@ -228,30 +242,38 @@ namespace Barotrauma ); SettingCarouselElement prevDifficulty = difficultyOptions.FirstOrNull(element => element.Value == prevSettings.Difficulty) ?? difficultyOptions[1]; - SettingValue difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions); + SettingValue difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions, OnValuesChanged); SettingValue maxMissionCountInput = CreateGUINumberInputCarousel(settingsList.Content, TextManager.Get("maxmissioncount"), TextManager.Get("maxmissioncounttooltip"), prevSettings.MaxMissionCount, valueStep: 1, minValue: CampaignSettings.MinMissionCountLimit, maxValue: CampaignSettings.MaxMissionCountLimit, - verticalSize); + verticalSize, + OnValuesChanged); - presetDropdown.OnSelected = (selected, o) => + presetDropdown.OnSelected = (_, o) => { - if (o is CampaignSettings settings) - { - tutorialEnabled.SetValue(isSinglePlayer && settings.TutorialEnabled); - radiationEnabled.SetValue(settings.RadiationEnabled); - maxMissionCountInput.SetValue(settings.MaxMissionCount); - startingFundsInput.SetValue(settings.StartingBalanceAmount); - difficultyInput.SetValue(settings.Difficulty); - startingSetInput.SetValue(settings.StartItemSet); - return true; - } - return false; + if (o is not CampaignSettings settings) { return false; } + + loadingPreset = true; + tutorialEnabled.SetValue(isSinglePlayer && settings.TutorialEnabled); + radiationEnabled.SetValue(settings.RadiationEnabled); + maxMissionCountInput.SetValue(settings.MaxMissionCount); + startingFundsInput.SetValue(settings.StartingBalanceAmount); + difficultyInput.SetValue(settings.Difficulty); + startingSetInput.SetValue(settings.StartItemSet); + loadingPreset = false; + return true; }; + void OnValuesChanged() + { + if (loadingPreset) { return; } + presetDropdown.Select(0); + } + return new CampaignSettingElements { + SelectedPreset = presetValue, TutorialEnabled = tutorialEnabled, RadiationEnabled = radiationEnabled, MaxMissionCount = maxMissionCountInput, @@ -261,7 +283,7 @@ namespace Barotrauma }; // Create a number input with plus and minus buttons because for some reason the default GUINumberInput buttons don't work when in a GUIMessageBox - static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, int minValue, int maxValue, float verticalSize) + static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, int minValue, int maxValue, float verticalSize, Action onChanged) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); @@ -286,9 +308,11 @@ namespace Barotrauma minusButton.OnClicked = plusButton.OnClicked = ChangeValue; + numberInput.OnValueChanged += _ => onChanged(); + bool ChangeValue(GUIButton btn, object userData) { - if (!(userData is int change)) { return false; } + if (userData is not int change) { return false; } numberInput.IntValue += change; return true; @@ -298,7 +322,7 @@ namespace Barotrauma } static SettingValue CreateSelectionCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, SettingCarouselElement defaultValue, float verticalSize, - ImmutableArray> options) + ImmutableArray> options, Action onChanged) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); @@ -349,6 +373,8 @@ namespace Barotrauma return true; } + numberInput.OnValueChanged += _ => onChanged(); + void SetValue(int value) { numberInput.IntValue = value; @@ -358,7 +384,7 @@ namespace Barotrauma return new SettingValue(() => options[numberInput.IntValue].Value, t => SetValue(options.IndexOf(e => Equals(e.Value, t)))); } - static SettingValue CreateTickbox(GUIComponent parent, LocalizedString description, LocalizedString tooltip, bool defaultValue, float verticalSize) + static SettingValue CreateTickbox(GUIComponent parent, LocalizedString description, LocalizedString tooltip, bool defaultValue, float verticalSize, Action onChanged) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, 0.7f, verticalSize); GUILayoutGroup tickboxContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), inputContainer.RectTransform), childAnchor: Anchor.Center); @@ -370,6 +396,13 @@ namespace Barotrauma tickBox.Box.IgnoreLayoutGroups = true; tickBox.Box.RectTransform.SetPosition(Anchor.CenterRight); inputContainer.RectTransform.Parent.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); + + tickBox.OnSelected += _ => + { + onChanged(); + return true; + }; + return new SettingValue(() => tickBox.Selected, b => tickBox.Selected = b); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index a3ae05b14..c23b2b190 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -194,7 +194,7 @@ namespace Barotrauma { TextGetter = () => { - int initialMoney = CurrentSettings.InitialMoney; + int initialMoney = CampaignSettings.CurrentSettings.InitialMoney; if (subList.SelectedData is SubmarineInfo subInfo) { initialMoney -= subInfo.Price; @@ -208,15 +208,15 @@ namespace Barotrauma { OnClicked = (tb, userdata) => { - CreateCustomizeWindow(CurrentSettings, settings => + CreateCustomizeWindow(CampaignSettings.CurrentSettings, settings => { - CampaignSettings prevSettings = CurrentSettings; - CurrentSettings = settings; + CampaignSettings prevSettings = CampaignSettings.CurrentSettings; + CampaignSettings.CurrentSettings = settings; if (prevSettings.InitialMoney != settings.InitialMoney) { object selectedData = subList.SelectedData; UpdateSubList(SubmarineInfo.SavedSubmarines); - if (selectedData is SubmarineInfo selectedSub && selectedSub.Price <= CurrentSettings.InitialMoney) + if (selectedData is SubmarineInfo selectedSub && selectedSub.Price <= CampaignSettings.CurrentSettings.InitialMoney) { subList.Select(selectedData); } @@ -375,6 +375,7 @@ namespace Barotrauma { onClosed?.Invoke(elements.CreateSettings()); + GameSettings.SaveCurrentConfig(); return CampaignCustomizeSettings.Close(button, o); }; } @@ -399,7 +400,7 @@ namespace Barotrauma SubmarineInfo selectedSub = null; - if (!(subList.SelectedData is SubmarineInfo)) { return false; } + if (subList.SelectedData is not SubmarineInfo) { return false; } selectedSub = subList.SelectedData as SubmarineInfo; if (selectedSub.SubmarineClass == SubmarineClass.Undefined) @@ -419,7 +420,7 @@ namespace Barotrauma string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - CampaignSettings settings = CurrentSettings; + CampaignSettings settings = CampaignSettings.CurrentSettings; if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { @@ -476,7 +477,7 @@ namespace Barotrauma { foreach (GUIComponent child in subList.Content.Children) { - if (!(child.UserData is SubmarineInfo sub)) { return; } + if (child.UserData is not SubmarineInfo sub) { return; } child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.Contains(filter.ToLower(), StringComparison.OrdinalIgnoreCase); } } @@ -487,9 +488,9 @@ namespace Barotrauma (subPreviewContainer.Parent as GUILayoutGroup)?.Recalculate(); subPreviewContainer.ClearChildren(); - if (!(obj is SubmarineInfo sub)) { return true; } + if (obj is not SubmarineInfo sub) { return true; } #if !DEBUG - if (sub.Price > CurrentSettings.InitialMoney && !GameMain.DebugDraw) + if (sub.Price > CampaignSettings.CurrentSettings.InitialMoney && !GameMain.DebugDraw) { SetPage(0); nextButton.Enabled = false; @@ -551,7 +552,7 @@ namespace Barotrauma 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) { - TextColor = sub.Price > CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, + TextColor = sub.Price > CampaignSettings.CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, ToolTip = textBlock.ToolTip }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), @@ -563,7 +564,7 @@ namespace Barotrauma #if !DEBUG if (!GameMain.DebugDraw) { - if (sub.Price > CurrentSettings.InitialMoney || !sub.IsCampaignCompatible) + if (sub.Price > CampaignSettings.CurrentSettings.InitialMoney || !sub.IsCampaignCompatible) { textBlock.CanBeFocused = false; textBlock.TextColor *= 0.5f; @@ -573,7 +574,7 @@ namespace Barotrauma } if (SubmarineInfo.SavedSubmarines.Any()) { - var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CurrentSettings.InitialMoney).ToList(); + var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignSettings.CurrentSettings.InitialMoney).ToList(); if (validSubs.Count > 0) { subList.Select(validSubs[Rand.Int(validSubs.Count)]); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 00591d5b6..6cb2671ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -81,11 +81,21 @@ namespace Barotrauma tabs[(int)CampaignMode.InteractionType.Map] = CreateDefaultTabContainer(container, new Vector2(0.9f)); var mapFrame = new GUIFrame(new RectTransform(Vector2.One, GetTabContainer(CampaignMode.InteractionType.Map).RectTransform, Anchor.TopLeft), color: Color.Black * 0.9f); - new GUICustomComponent(new RectTransform(Vector2.One, mapFrame.RectTransform), DrawMap, UpdateMap); + var mapContainer = new GUICustomComponent(new RectTransform(Vector2.One, mapFrame.RectTransform), DrawMap, UpdateMap); + var notificationFrame = new GUIFrame(new RectTransform(new Point(mapContainer.Rect.Width, GUI.IntScale(40)), mapContainer.RectTransform, Anchor.BottomCenter), style: "ChatBox"); + new GUIFrame(new RectTransform(Vector2.One, mapFrame.RectTransform), style: "InnerGlow", color: Color.Black * 0.9f) { CanBeFocused = false + }; + + var notificationContainer = new GUICustomComponent(new RectTransform(new Vector2(0.98f, 1.0f), notificationFrame.RectTransform, Anchor.Center), DrawMapNotifications, null) + { + HideElementsOutsideFrame = true }; + var notificationHeader = new GUIImage(new RectTransform(new Vector2(0.1f, 1.0f), notificationFrame.RectTransform, Anchor.CenterLeft), style: "GUISlopedHeaderRight"); + var text = new GUITextBlock(new RectTransform(Vector2.One, notificationHeader.RectTransform, Anchor.Center), TextManager.Get("breakingnews"), font: GUIStyle.LargeFont); + notificationHeader.RectTransform.MinSize = new Point((int)(text.TextSize.X * 1.3f), 0); // crew tab ------------------------------------------------------------------------- @@ -152,18 +162,23 @@ namespace Barotrauma CreateUI(tabs[(int)CampaignMode.InteractionType.Map].Parent); } - GameMain.GameSession?.Map?.Draw(spriteBatch, mapContainer); + Campaign?.Map?.Draw(Campaign, spriteBatch, mapContainer); + } + + private void DrawMapNotifications(SpriteBatch spriteBatch, GUICustomComponent notificationContainer) + { + Campaign?.Map?.DrawNotifications(spriteBatch, notificationContainer); } private void UpdateMap(float deltaTime, GUICustomComponent mapContainer) { - var map = GameMain.GameSession?.Map; + var map = Campaign?.Map; if (map == null) { return; } - if (selectedLocation != null && selectedLocation == GameMain.GameSession.Campaign.GetCurrentDisplayLocation()) + if (selectedLocation != null && selectedLocation == Campaign.GetCurrentDisplayLocation()) { map.SelectLocation(-1); } - map.Update(deltaTime, mapContainer); + map.Update(Campaign, deltaTime, mapContainer); foreach (GUITickBox tickBox in missionTickBoxes) { bool disable = hasMaxMissions && !tickBox.Selected; @@ -260,14 +275,20 @@ namespace Barotrauma if (connection?.LevelData != null) { + if (location.Faction?.Prefab != null) + { + var factionLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), + TextManager.Get("Faction"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), factionLabel.RectTransform), location.Faction.Prefab.Name, textAlignment: Alignment.CenterRight, textColor: location.Faction.Prefab.IconColor); + } var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), connection.Biome.DisplayName, textAlignment: Alignment.CenterRight); var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)connection.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), TextManager.GetWithVariable("percentageformat", "[value]", ((int)connection.LevelData.Difficulty).ToString()), textAlignment: Alignment.CenterRight); + if (connection.LevelData.HasBeaconStation) { var beaconStationContent = new GUILayoutGroup(new RectTransform(biomeLabel.RectTransform.NonScaledSize, textContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -328,12 +349,31 @@ namespace Barotrauma if (connection != null && connection.Locations.Contains(currentDisplayLocation)) { List availableMissions = currentDisplayLocation.GetMissionsInConnection(connection).ToList(); - if (!availableMissions.Contains(null)) { availableMissions.Insert(0, null); } + + if (!availableMissions.Any()) { availableMissions.Insert(0, null); } + + availableMissions.AddRange(location.AvailableMissions.Where(m => m.Locations[0] == m.Locations[1])); missionList.Content.ClearChildren(); + bool isPrevMissionInNextLocation = false; foreach (Mission mission in availableMissions) { + bool isMissionInNextLocation = mission != null && location.AvailableMissions.Contains(mission); + if (isMissionInNextLocation && !isPrevMissionInNextLocation) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionList.Content.RectTransform), TextManager.Get("outpostmissions"), + textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) + { + CanBeFocused = false + }; + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionList.Content.RectTransform), style: "HorizontalLine") + { + CanBeFocused = false + }; + } + isPrevMissionInNextLocation = isMissionInNextLocation; + var missionPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), missionList.Content.RectTransform), style: null) { UserData = mission @@ -347,45 +387,54 @@ namespace Barotrauma var missionName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission?.Name ?? TextManager.Get("NoMission"), font: GUIStyle.SubHeadingFont, wrap: true); missionName.RectTransform.MinSize = new Point(0, GUI.IntScale(15)); - if (mission != null) - { - var tickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.X, 0) }, label: string.Empty) + if (mission == null) + { + missionTextContent.RectTransform.MinSize = missionName.RectTransform.MinSize = new Point(0, GUI.IntScale(35)); + missionTextContent.ChildAnchor = Anchor.CenterLeft; + } + else + { + GUITickBox tickBox = null; + if (!isMissionInNextLocation) { - UserData = mission, - Selected = Campaign.Map.CurrentLocation?.SelectedMissions.Contains(mission) ?? false - }; - tickBox.RectTransform.MinSize = new Point(tickBox.Rect.Height, 0); - tickBox.RectTransform.IsFixedSize = true; - tickBox.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap); - tickBox.OnSelected += (GUITickBox tb) => - { - if (!CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; } - - if (tb.Selected) + tickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.X, 0) }, label: string.Empty) { - Campaign.Map.CurrentLocation.SelectMission(mission); - } - else + UserData = mission, + Selected = Campaign.Map.CurrentLocation?.SelectedMissions.Contains(mission) ?? false + }; + tickBox.RectTransform.MinSize = new Point(tickBox.Rect.Height, 0); + tickBox.RectTransform.IsFixedSize = true; + tickBox.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap); + tickBox.OnSelected += (GUITickBox tb) => { - Campaign.Map.CurrentLocation.DeselectMission(mission); - } + if (!CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; } - foreach (GUITextBlock rewardText in missionRewardTexts) - { - Mission otherMission = rewardText.UserData as Mission; - rewardText.Text = otherMission.GetMissionRewardText(Submarine.MainSub); - } + if (tb.Selected) + { + Campaign.Map.CurrentLocation.SelectMission(mission); + } + else + { + Campaign.Map.CurrentLocation.DeselectMission(mission); + } - UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation)); + foreach (GUITextBlock rewardText in missionRewardTexts) + { + Mission otherMission = rewardText.UserData as Mission; + rewardText.Text = otherMission.GetMissionRewardText(Submarine.MainSub); + } - if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && - CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) - { - GameMain.Client?.SendCampaignState(); - } - return true; - }; - missionTickBoxes.Add(tickBox); + UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation)); + + if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && + CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) + { + GameMain.Client?.SendCampaignState(); + } + return true; + }; + missionTickBoxes.Add(tickBox); + } GUILayoutGroup difficultyIndicatorGroup = null; if (mission.Difficulty.HasValue) @@ -410,7 +459,7 @@ namespace Barotrauma float extraPadding = 0;// 0.8f * tickBox.Rect.Width; float extraZPadding = difficultyIndicatorGroup != null ? mission.Difficulty.Value * (difficultyIndicatorGroup.Children.First().Rect.Width + difficultyIndicatorGroup.AbsoluteSpacing) : 0; - missionName.Padding = new Vector4(missionName.Padding.X + tickBox.Rect.Width * 1.2f + extraPadding, + missionName.Padding = new Vector4(missionName.Padding.X + (tickBox?.Rect.Width ?? 0) * 1.2f + extraPadding, missionName.Padding.Y, missionName.Padding.Z + extraZPadding + extraPadding, missionName.Padding.W); @@ -425,9 +474,11 @@ namespace Barotrauma }; missionRewardTexts.Add(rewardText); - LocalizedString reputationText = mission.GetReputationRewardText(mission.Locations[0]); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(reputationText), wrap: true); - + LocalizedString reputationText = mission.GetReputationRewardText(); + if (!reputationText.IsNullOrEmpty()) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(reputationText), wrap: true); + } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(mission.Description), wrap: true); } missionPanel.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Children.Sum(c => c.Rect.Height + missionTextContent.AbsoluteSpacing) / missionTextContent.RectTransform.RelativeSize.Y) + GUI.IntScale(0)); @@ -487,7 +538,7 @@ namespace Barotrauma OnClicked = (GUIButton btn, object obj) => { if (missionList.Content.FindChild(c => c is GUITickBox tickBox && tickBox.Selected, recursive: true) == null && - missionList.Content.Children.Any(c => c.UserData is Mission)) + missionList.Content.Children.Any(c => c.UserData is Mission mission && mission.Locations.Contains(Campaign?.Map?.CurrentLocation))) { var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); noMissionVerification.Buttons[0].OnClicked = (btn, userdata) => @@ -520,7 +571,7 @@ namespace Barotrauma //locationInfoPanel?.UpdateAuto(1.0f); } - public void SelectTab(CampaignMode.InteractionType tab, Identifier storeIdentifier = default) + public void SelectTab(CampaignMode.InteractionType tab, Character npc = null) { if (Campaign.ShowCampaignUI || (Campaign.ForceMapUI && tab == CampaignMode.InteractionType.Map)) { @@ -541,7 +592,7 @@ namespace Barotrauma switch (selectedTab) { case CampaignMode.InteractionType.Store: - Store.SelectStore(storeIdentifier); + Store.SelectStore(npc); break; case CampaignMode.InteractionType.Crew: CrewManagement.UpdateCrew(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 102992cae..b3e163a16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -87,26 +87,26 @@ namespace Barotrauma.CharacterEditor private float spriteSheetZoom = 1; private float spriteSheetMinZoom = 0.25f; private float spriteSheetMaxZoom = 1; - private int spriteSheetOffsetY = 20; - private int spriteSheetOffsetX = 30; + private const int spriteSheetOffsetY = 20; + private const int spriteSheetOffsetX = 30; private bool hideBodySheet; private Color backgroundColor = new Color(0.2f, 0.2f, 0.2f, 1.0f); private Vector2 cameraOffset; - private List selectedJoints = new List(); - private List selectedLimbs = new List(); - private HashSet editedCharacters = new HashSet(); + private readonly List selectedJoints = new List(); + private readonly List selectedLimbs = new List(); + private readonly HashSet editedCharacters = new HashSet(); private bool isEndlessRunner; private Rectangle spriteSheetRect; - private Rectangle CalculateSpritesheetRectangle() => + private Rectangle CalculateSpritesheetRectangle() => Textures == null || Textures.None() ? Rectangle.Empty : new Rectangle( - spriteSheetOffsetX, - spriteSheetOffsetY, - (int)(Textures.OrderByDescending(t => t.Width).First().Width * spriteSheetZoom), + spriteSheetOffsetX, + spriteSheetOffsetY, + (int)(Textures.OrderByDescending(t => t.Width).First().Width * spriteSheetZoom), (int)(Textures.Sum(t => t.Height) * spriteSheetZoom)); private const string screenTextTag = "CharacterEditor."; @@ -143,7 +143,7 @@ namespace Barotrauma.CharacterEditor var humanSpeciesName = CharacterPrefab.HumanSpeciesName; if (humanSpeciesName.IsEmpty) { - SpawnCharacter(AllSpecies.First()); + SpawnCharacter(VisibleSpecies.First()); } else { @@ -192,7 +192,7 @@ namespace Barotrauma.CharacterEditor jointEndLimb = null; anchor1Pos = null; jointStartLimb = null; - allSpecies = null; + visibleSpecies = null; onlyShowSourceRectForSelectedLimbs = false; unrestrictSpritesheet = false; editedCharacters.Clear(); @@ -214,15 +214,12 @@ namespace Barotrauma.CharacterEditor private void Reset(IEnumerable characters = null) { - if (characters == null) - { - characters = editedCharacters; - } + characters ??= editedCharacters; characters.ForEach(c => ResetParams(c)); ResetVariables(); } - private void ResetParams(Character character) + private static void ResetParams(Character character) { character.Params.Reset(true); foreach (var animation in character.AnimController.AllAnimParams) @@ -719,7 +716,7 @@ namespace Barotrauma.CharacterEditor cameraOffset = Vector2.Clamp(cameraOffset, min, max); } Cam.Position = targetPos + cameraOffset; - MapEntity.mapEntityList.ForEach(e => e.IsHighlighted = false); + MapEntity.ClearHighlightedEntities(); // Update widgets jointSelectionWidgets.Values.ForEach(w => w.Update((float)deltaTime)); limbEditWidgets.Values.ForEach(w => w.Update((float)deltaTime)); @@ -994,7 +991,7 @@ namespace Barotrauma.CharacterEditor var collider = character.AnimController.Collider; var colliderDrawPos = SimToScreen(collider.SimPosition); Vector2 forward = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation)); - var endPos = SimToScreen(collider.SimPosition + forward * collider.radius); + var endPos = SimToScreen(collider.SimPosition + forward * collider.Radius); GUI.DrawLine(spriteBatch, colliderDrawPos, endPos, GUIStyle.Green); GUI.DrawLine(spriteBatch, colliderDrawPos, SimToScreen(collider.SimPosition + forward * 0.25f), Color.Blue); Vector2 left = forward.Left(); @@ -1363,7 +1360,7 @@ namespace Barotrauma.CharacterEditor private class WallGroup { public readonly List walls; - + public WallGroup(List walls) { this.walls = walls; @@ -1374,7 +1371,7 @@ namespace Barotrauma.CharacterEditor var clones = new List(); walls.ForEachMod(w => clones.Add(w.Clone() as Structure)); return new WallGroup(clones); - } + } } private void CloneWalls() @@ -1391,7 +1388,7 @@ namespace Barotrauma.CharacterEditor else if (i == 2) { clones[i].walls[j].Move(new Vector2(-originalWall.walls[j].Rect.Width, 0)); - } + } } } } @@ -1404,8 +1401,8 @@ namespace Barotrauma.CharacterEditor private WallGroup SelectLastClone(bool right) { - var lastWall = right - ? clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Right).Last() + var lastWall = right + ? clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Right).Last() : clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Left).First(); return clones.Where(c => c.walls.Contains(lastWall)).FirstOrDefault(); } @@ -1440,33 +1437,35 @@ namespace Barotrauma.CharacterEditor private Identifier currentCharacterIdentifier; private Identifier selectedJob = Identifier.Empty; - private List allSpecies; - private List AllSpecies + private List visibleSpecies; + private List VisibleSpecies { get { - if (allSpecies == null) - { -#if DEBUG - allSpecies = CharacterPrefab.Prefabs.Keys.OrderBy(p => p).ToList(); -#else - allSpecies = CharacterPrefab.Prefabs.Keys.Where(p => !p.Contains("variant")).OrderBy(p => p).ToList(); -#endif - allSpecies.ForEach(f => DebugConsole.NewMessage(f.Value, Color.White)); - } - return allSpecies; + visibleSpecies ??= CharacterPrefab.Prefabs.Where(ShowCreature).OrderBy(p => p.Identifier).Select(p => p.Identifier).ToList(); + return visibleSpecies; } } - private List vanillaCharacters; - private List VanillaCharacters + private bool ShowCreature(CharacterPrefab prefab) + { + Identifier speciesName = prefab.Identifier; + if (speciesName == CharacterPrefab.HumanSpeciesName) { return true; } + if (!VanillaCharacters.Contains(prefab.ContentFile)) + { + // Always show all custom characters. + return true; + } + if (CreatureMetrics.UnlockAll) { return true; } + return CreatureMetrics.Unlocked.Contains(speciesName); + } + + private IEnumerable vanillaCharacters; + private IEnumerable VanillaCharacters { get { - if (vanillaCharacters == null) - { - vanillaCharacters = GameMain.VanillaContent.GetFiles().ToList(); - } + vanillaCharacters ??= GameMain.VanillaContent.GetFiles(); return vanillaCharacters; } } @@ -1475,7 +1474,7 @@ namespace Barotrauma.CharacterEditor { GetCurrentCharacterIndex(); IncreaseIndex(); - currentCharacterIdentifier = AllSpecies[characterIndex]; + currentCharacterIdentifier = VisibleSpecies[characterIndex]; return currentCharacterIdentifier; } @@ -1483,19 +1482,19 @@ namespace Barotrauma.CharacterEditor { GetCurrentCharacterIndex(); ReduceIndex(); - currentCharacterIdentifier = AllSpecies[characterIndex]; + currentCharacterIdentifier = VisibleSpecies[characterIndex]; return currentCharacterIdentifier; } private void GetCurrentCharacterIndex() { - characterIndex = AllSpecies.IndexOf(character.SpeciesName); + characterIndex = VisibleSpecies.IndexOf(character.SpeciesName); } private void IncreaseIndex() { characterIndex++; - if (characterIndex > AllSpecies.Count - 1) + if (characterIndex > VisibleSpecies.Count - 1) { characterIndex = 0; } @@ -1506,7 +1505,7 @@ namespace Barotrauma.CharacterEditor characterIndex--; if (characterIndex < 0) { - characterIndex = AllSpecies.Count - 1; + characterIndex = VisibleSpecies.Count - 1; } } @@ -1687,7 +1686,7 @@ namespace Barotrauma.CharacterEditor XElement overrideElement = null; if (duplicate != null) { - allSpecies = null; + visibleSpecies = null; if (!File.Exists(configFilePath)) { // If the file exists, we just want to overwrite it. @@ -1823,9 +1822,9 @@ namespace Barotrauma.CharacterEditor AnimationParams.Create(fullPath, name, animType, type); } } - if (!AllSpecies.Contains(name)) + if (!VisibleSpecies.Contains(name)) { - AllSpecies.Add(name); + VisibleSpecies.Add(name); } SpawnCharacter(name, ragdollParams); limbPairEditing = false; @@ -2678,23 +2677,33 @@ namespace Barotrauma.CharacterEditor { Stretch = true }; - // Character selection var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUIStyle.LargeFont); - var characterDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.2f), content.RectTransform) { RelativeOffset = new Vector2(0, 0.2f) }, elementCount: 8, style: null); characterDropDown.ListBox.Color = new Color(characterDropDown.ListBox.Color.R, characterDropDown.ListBox.Color.G, characterDropDown.ListBox.Color.B, byte.MaxValue); - foreach (var file in AllSpecies) + foreach (CharacterPrefab prefab in CharacterPrefab.Prefabs.OrderByDescending(p => p.Identifier)) { - characterDropDown.AddItem(file.Value.CapitaliseFirstInvariant(), file); + Identifier speciesName = prefab.Identifier; + if (ShowCreature(prefab)) + { + characterDropDown.AddItem(speciesName.Value.CapitaliseFirstInvariant(), speciesName).SetAsFirstChild(); + } + else if (!CreatureMetrics.Encountered.Contains(speciesName)) + { + // Using a matching placeholder string here ("hidden"). + var element = characterDropDown.AddItem(TextManager.Get("hiddensubmarines"), Identifier.Empty, textColor: Color.Gray * 0.75f); + element.SetAsLastChild(); + element.Enabled = false; + } } characterDropDown.SelectItem(currentCharacterIdentifier); characterDropDown.OnSelected = (component, data) => { Identifier characterIdentifier = (Identifier)data; + if (characterIdentifier.IsEmpty) { return true; } try { SpawnCharacter(characterIdentifier); @@ -2795,7 +2804,7 @@ namespace Barotrauma.CharacterEditor saveAllButton.OnClicked += (button, userData) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; @@ -2835,7 +2844,7 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); @@ -2973,7 +2982,7 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); @@ -3212,7 +3221,7 @@ namespace Barotrauma.CharacterEditor Wizard.Instance.CopyExisting(CharacterParams, RagdollParams, AnimParams); } - #region ToggleButtons +#region ToggleButtons private enum Direction { Left, @@ -4235,7 +4244,7 @@ namespace Barotrauma.CharacterEditor int points = 1000; float GetAmplitude() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveAmplitude) * Cam.Zoom / amplitudeMultiplier; float GetWaveLength() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveLength) * Cam.Zoom / lengthMultiplier; - Vector2 GetRefPoint() => SimToScreen(collider.SimPosition) - GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(collider.radius) * 3 * Cam.Zoom; + Vector2 GetRefPoint() => SimToScreen(collider.SimPosition) - GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(collider.Radius) * 3 * Cam.Zoom; Vector2 GetDrawPos() => GetRefPoint() - GetScreenSpaceForward() * GetWaveLength(); Vector2 GetDir() => GetRefPoint() - GetDrawPos(); Vector2 GetStartPoint() => GetDrawPos() + GetDir() / 2; @@ -5008,9 +5017,9 @@ namespace Barotrauma.CharacterEditor // We want the collider to be slightly smaller than the source rect, because the source rect is usually a bit bigger than the graphic. float multiplier = 0.9f; l.body.SetSize(new Vector2(size.X, size.Y) * l.Scale * RagdollParams.TextureScale * multiplier); - TryUpdateLimbParam(l, "radius", ConvertUnits.ToDisplayUnits(l.body.radius / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); - TryUpdateLimbParam(l, "width", ConvertUnits.ToDisplayUnits(l.body.width / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); - TryUpdateLimbParam(l, "height", ConvertUnits.ToDisplayUnits(l.body.height / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + TryUpdateLimbParam(l, "radius", ConvertUnits.ToDisplayUnits(l.body.Radius / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + TryUpdateLimbParam(l, "width", ConvertUnits.ToDisplayUnits(l.body.Width / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + TryUpdateLimbParam(l, "height", ConvertUnits.ToDisplayUnits(l.body.Height / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); } private void RecalculateOrigin(Limb l, Vector2? newOrigin = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs index e191d3e54..43968f02e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs @@ -1,6 +1,4 @@ using Microsoft.Xna.Framework; -using System; -using System.Xml.Linq; namespace Barotrauma { @@ -8,7 +6,7 @@ namespace Barotrauma { private GUIListBox listBox; - private ContentXElement configElement; + private readonly ContentXElement configElement; private float scrollSpeed; @@ -37,6 +35,8 @@ namespace Barotrauma set { listBox.BarScroll = value; } } + public readonly GUIButton CloseButton; + public CreditsPlayer(RectTransform rectT, string configFile) : base(null, rectT) { @@ -51,6 +51,10 @@ namespace Barotrauma configElement = doc.Root.FromPackage(ContentPackageManager.VanillaCorePackage); Load(); + + CloseButton = new GUIButton(new RectTransform(new Vector2(0.1f), RectTransform, Anchor.BottomRight, maxSize: new Point(GUI.IntScale(300), GUI.IntScale(50))) + { AbsoluteOffset = new Point(GUI.IntScale(20), GUI.IntScale(20) + (Rect.Bottom - GameMain.GraphicsHeight)) }, + TextManager.Get("close")); } private void Load() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index ac0be6534..337cdb366 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Lights; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -14,9 +15,9 @@ namespace Barotrauma private RenderTarget2D renderTargetWater; private RenderTarget2D renderTargetFinal; - private Effect damageEffect; - private Texture2D damageStencil; - private Texture2D distortTexture; + public readonly Effect DamageEffect; + private readonly Texture2D damageStencil; + private readonly Texture2D distortTexture; private float fadeToBlackState; @@ -38,7 +39,7 @@ namespace Barotrauma }; //var blurEffect = LoadEffect("Effects/blurshader"); - damageEffect = EffectLoader.Load("Effects/damageshader"); + DamageEffect = EffectLoader.Load("Effects/damageshader"); PostProcessEffect = EffectLoader.Load("Effects/postprocess"); GradientEffect = EffectLoader.Load("Effects/gradientshader"); GrainEffect = EffectLoader.Load("Effects/grainshader"); @@ -46,9 +47,9 @@ namespace Barotrauma BlueprintEffect = EffectLoader.Load("Effects/blueprintshader"); damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png"); - damageEffect.Parameters["xStencil"].SetValue(damageStencil); - damageEffect.Parameters["aMultiplier"].SetValue(50.0f); - damageEffect.Parameters["cMultiplier"].SetValue(200.0f); + DamageEffect.Parameters["xStencil"].SetValue(damageStencil); + DamageEffect.Parameters["aMultiplier"].SetValue(50.0f); + DamageEffect.Parameters["cMultiplier"].SetValue(200.0f); distortTexture = TextureLoader.FromFile("Content/Effects/distortnormals.png"); PostProcessEffect.Parameters["xDistortTexture"].SetValue(distortTexture); @@ -105,13 +106,13 @@ namespace Barotrauma c.DoVisibilityCheck(cam); if (c.IsVisible != wasVisible) { - c.AnimController.Limbs.ForEach(l => + foreach (var limb in c.AnimController.Limbs) { - if (l.LightSource != null) + if (limb.LightSource is LightSource light) { - l.LightSource.Enabled = c.IsVisible; + light.Enabled = c.IsVisible; } - }); + } } } @@ -187,6 +188,10 @@ namespace Barotrauma GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:LOS", sw.ElapsedTicks); sw.Restart(); + + static bool IsFromOutpostDrawnBehindSubs(Entity e) + => e.Submarine is { Info.OutpostGenerationParams.DrawBehindSubs: true }; + //------------------------------------------------------------------------ graphics.SetRenderTarget(renderTarget); graphics.Clear(Color.Transparent); @@ -194,7 +199,7 @@ namespace Barotrauma //(= the background texture that's revealed when a wall is destroyed) into the background render target //These will be visible through the LOS effect. spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); - Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); + Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null) && !IsFromOutpostDrawnBehindSubs(e)); Submarine.DrawPaintedColors(spriteBatch, false); spriteBatch.End(); @@ -221,7 +226,11 @@ namespace Barotrauma Level.Loaded.DrawBack(graphics, spriteBatch, cam); } - //draw alpha blended particles that are in water and behind subs + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); + Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null) && IsFromOutpostDrawnBehindSubs(e)); + spriteBatch.End(); + + //draw alpha blended particles that are in water and behind subs #if LINUX || OSX spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); #else @@ -336,12 +345,13 @@ namespace Barotrauma GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontParticles", sw.ElapsedTicks); sw.Restart(); + DamageEffect.CurrentTechnique = DamageEffect.Techniques["StencilShader"]; spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, null, null, - damageEffect, + DamageEffect, cam.Transform); - Submarine.DrawDamageable(spriteBatch, damageEffect, false); + Submarine.DrawDamageable(spriteBatch, DamageEffect, false); spriteBatch.End(); sw.Stop(); @@ -368,7 +378,7 @@ namespace Barotrauma { graphics.DepthStencilState = DepthStencilState.None; graphics.SamplerStates[0] = SamplerState.LinearWrap; - graphics.BlendState = Lights.CustomBlendStates.Multiplicative; + graphics.BlendState = CustomBlendStates.Multiplicative; Quad.UseBasicEffect(GameMain.LightManager.LightMap); Quad.Render(); } @@ -399,12 +409,13 @@ namespace Barotrauma { GameMain.LightManager.LosEffect.CurrentTechnique = GameMain.LightManager.LosEffect.Techniques["LosShader"]; + GameMain.LightManager.LosEffect.Parameters["blurDistance"].SetValue(0.005f); GameMain.LightManager.LosEffect.Parameters["xTexture"].SetValue(renderTargetBackground); GameMain.LightManager.LosEffect.Parameters["xLosTexture"].SetValue(GameMain.LightManager.LosTexture); GameMain.LightManager.LosEffect.Parameters["xLosAlpha"].SetValue(GameMain.LightManager.LosAlpha); Color losColor; - if (GameMain.LightManager.LosMode == LosMode.Transparent) + if (GameMain.LightManager.LosMode is LosMode.Transparent or LosMode.BlockOutsideView) { //convert the los color to HLS and make sure the luminance of the color is always the same //as the luminance of the ambient light color @@ -447,6 +458,11 @@ namespace Barotrauma Vector3 chromaticAberrationStrength = GameSettings.CurrentConfig.Graphics.ChromaticAberration ? new Vector3(-0.02f, -0.01f, 0.0f) : Vector3.Zero; + if (Level.Loaded?.Renderer != null) + { + chromaticAberrationStrength += new Vector3(-0.03f, -0.015f, 0.0f) * Level.Loaded.Renderer.ChromaticAberrationStrength; + } + if (Character.Controlled != null) { BlurStrength = Character.Controlled.BlurStrength * 0.005f; @@ -504,6 +520,11 @@ namespace Barotrauma spriteBatch.End(); } + if (GameMain.LightManager.DebugLos) + { + GameMain.LightManager.DebugDrawLos(spriteBatch, cam); + } + sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:PostProcess", sw.ElapsedTicks); sw.Restart(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 4537be4ac..24e67f496 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -219,7 +219,7 @@ namespace Barotrauma currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; - var dummyLocations = GameSession.CreateDummyLocations(seed: currentLevelData.Seed); + var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition); GameMain.LightManager.AddLight(pointerLightSource); @@ -343,7 +343,7 @@ namespace Barotrauma editorContainer.ClearChildren(); paramsList.Content.ClearChildren(); - foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams) + foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paramsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Identifier.Value) @@ -359,7 +359,7 @@ namespace Barotrauma editorContainer.ClearChildren(); caveParamsList.Content.ClearChildren(); - foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams) + foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), caveParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) @@ -375,7 +375,7 @@ namespace Barotrauma editorContainer.ClearChildren(); ruinParamsList.Content.ClearChildren(); - foreach (RuinGenerationParams genParams in RuinGenerationParams.RuinParams) + 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) @@ -391,7 +391,7 @@ namespace Barotrauma editorContainer.ClearChildren(); outpostParamsList.Content.ClearChildren(); - foreach (OutpostGenerationParams genParams in OutpostGenerationParams.OutpostParams) + foreach (OutpostGenerationParams genParams in OutpostGenerationParams.OutpostParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), outpostParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 107c22358..9f1a60b02 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -46,7 +46,7 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; - private GUIDropDown serverExecutableDropdown; + private GUIDropDown languageDropdown, serverExecutableDropdown; private readonly GUIButton joinServerButton, hostServerButton; private readonly GUIFrame modsButtonContainer; @@ -82,6 +82,7 @@ namespace Barotrauma { GameMain.Instance.ResolutionChanged += () => { + SetMenuTabPositioning(); CreateHostServerFields(); CreateCampaignSetupUI(); SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); @@ -426,31 +427,33 @@ namespace Barotrauma var relativeSize = new Vector2(0.6f, 0.65f); var minSize = new Point(600, 400); var maxSize = new Point(2000, 1500); - var anchor = Anchor.CenterRight; - var pivot = Pivot.CenterRight; - Vector2 relativeSpacing = new Vector2(0.05f, 0.0f); - - menuTabs = new Dictionary(); + var anchor = Anchor.Center; + var pivot = Pivot.Center; + Vector2 relativeOffset = new Vector2(0.05f, 0.0f); - menuTabs[Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }, - style: null); - menuTabs[Tab.Settings].CanBeFocused = false; - - menuTabs[Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); - menuTabs[Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs = new Dictionary + { + [Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }, + style: null) + { + CanBeFocused = false + }, + [Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }), + [Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }) + }; CreateCampaignSetupUI(); var hostServerScale = new Vector2(0.7f, 1.2f); menuTabs[Tab.HostServer] = new GUIFrame(new RectTransform( Vector2.Multiply(relativeSize, hostServerScale), GUI.Canvas, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale)) - { RelativeOffset = relativeSpacing }); + { RelativeOffset = relativeOffset }); CreateHostServerFields(); //---------------------------------------------------------------------- - menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }); CreateTutorialTab(); this.game = game; @@ -466,6 +469,25 @@ namespace Barotrauma var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); + creditsPlayer.CloseButton.OnClicked = (btn, userdata) => + { + SelectTab(Tab.Empty); + return true; + }; + + SetMenuTabPositioning(); + } + + private void SetMenuTabPositioning() + { + foreach (GUIFrame menuTab in menuTabs.Values) + { + var anchor = GUI.IsUltrawide ? Anchor.Center : Anchor.CenterRight; + var pivot = GUI.IsUltrawide ? Pivot.Center : Pivot.CenterRight; + Vector2 relativeOffset = GUI.IsUltrawide ? Vector2.Zero : new Vector2(0.05f, 0.0f); + menuTab.RectTransform.SetPosition(anchor, pivot); + menuTab.RectTransform.RelativeOffset = relativeOffset; + } } private void CreateTutorialTab() @@ -893,12 +915,14 @@ namespace Barotrauma #endif } - string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + - " -public " + isPublicBox.Selected.ToString() + - " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + - " -banafterwrongpassword " + wrongPasswordBanBox.Selected.ToString() + - " -karmaenabled " + (!karmaBox.Selected).ToString() + - " -maxplayers " + maxPlayersBox.Text; + string arguments = + "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + + " -public " + isPublicBox.Selected.ToString() + + " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + + " -banafterwrongpassword " + wrongPasswordBanBox.Selected.ToString() + + " -karmaenabled " + (!karmaBox.Selected).ToString() + + " -maxplayers " + maxPlayersBox.Text + + $" -language \"{(LanguageIdentifier)languageDropdown.SelectedData}\""; if (!string.IsNullOrWhiteSpace(passwordBox.Text)) { @@ -1039,22 +1063,29 @@ namespace Barotrauma #if UNSTABLE backgroundSprite = new Sprite("Content/UnstableBackground.png", sourceRectangle: null); #endif - backgroundSprite ??= (LocationType.Prefabs.Where(l => l.UseInMainMenu).GetRandomUnsynced())?.GetPortrait(0); - } - - if (backgroundSprite != null) - { - GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, - aberrationStrength: 0.0f); + if (GUIStyle.GetComponentStyle("MainMenuBackground") is { } mainMenuStyle && + mainMenuStyle.Sprites.TryGetValue(GUIComponent.ComponentState.None, out var sprites)) + { + backgroundSprite = sprites.GetRandomUnsynced()?.Sprite; + } + backgroundSprite ??= LocationType.Prefabs.GetRandomUnsynced()?.GetPortrait(0); } var vignette = GUIStyle.GetComponentStyle("mainmenuvignette")?.GetDefaultSprite(); + float vignetteScale = Math.Min(GameMain.GraphicsWidth / vignette.size.X, GameMain.GraphicsHeight / vignette.size.Y); + + Rectangle drawArea = new Rectangle( + (int)(vignette.size.X * vignetteScale / 2), 0, + (int)(GameMain.GraphicsWidth - vignette.size.X * vignetteScale / 2), GameMain.GraphicsHeight); + + if (backgroundSprite?.Texture != null) + { + GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White, drawArea); + } + if (vignette != null) { - spriteBatch.Begin(blendState: BlendState.NonPremultiplied); - vignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, - new Vector2(GameMain.GraphicsWidth / vignette.size.X, GameMain.GraphicsHeight / vignette.size.Y)); - spriteBatch.End(); + vignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, vignetteScale); } } @@ -1067,10 +1098,10 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { - DrawBackground(graphics, spriteBatch); - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); + DrawBackground(graphics, spriteBatch); + GUI.Draw(Cam, spriteBatch); if (selectedTab != Tab.Credits) @@ -1100,7 +1131,7 @@ namespace Barotrauma if (i == 0) { GUI.DrawLine(spriteBatch, textPos, textPos - Vector2.UnitX * textSize.X, mouseOn ? Color.White : Color.White * 0.7f); - if (mouseOn && PlayerInput.PrimaryMouseButtonClicked()) + if (mouseOn && PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null) { GameMain.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com"); } @@ -1205,45 +1236,28 @@ namespace Barotrauma { menuTabs[Tab.HostServer].ClearChildren(); - string name = ""; - string password = ""; - int maxPlayers = 8; - bool isPublic = true; - bool banAfterWrongPassword = false; - bool karmaEnabled = true; - string selectedKarmaPreset = ""; - PlayStyle selectedPlayStyle = PlayStyle.Casual; - if (File.Exists(ServerSettings.SettingsFile)) + var serverSettings = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile, out _)?.Root ?? new XElement("serversettings"); + + var name = serverSettings.GetAttributeString("name", ""); + var password = serverSettings.GetAttributeString("password", ""); + var isPublic = serverSettings.GetAttributeBool("IsPublic", true); + var banAfterWrongPassword = serverSettings.GetAttributeBool("banafterwrongpassword", false); + + int maxPlayersElement = serverSettings.GetAttributeInt("maxplayers", 8); + if (maxPlayersElement > NetConfig.MaxPlayers) { - XDocument settingsDoc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); - if (settingsDoc != null) - { - name = settingsDoc.Root.GetAttributeString("name", name); - password = settingsDoc.Root.GetAttributeString("password", password); - isPublic = settingsDoc.Root.GetAttributeBool("public", isPublic); - banAfterWrongPassword = settingsDoc.Root.GetAttributeBool("banafterwrongpassword", banAfterWrongPassword); - - int maxPlayersElement = settingsDoc.Root.GetAttributeInt("maxplayers", maxPlayers); - if (maxPlayersElement > NetConfig.MaxPlayers) - { - DebugConsole.IsOpen = true; - DebugConsole.NewMessage($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", Color.Red); - maxPlayersElement = NetConfig.MaxPlayers; - } - - maxPlayers = maxPlayersElement; - karmaEnabled = settingsDoc.Root.GetAttributeBool("karmaenabled", true); - selectedKarmaPreset = settingsDoc.Root.GetAttributeString("karmapreset", "default"); - string playStyleStr = settingsDoc.Root.GetAttributeString("playstyle", "Casual"); - Enum.TryParse(playStyleStr, out selectedPlayStyle); - } + DebugConsole.AddWarning($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead."); } + int maxPlayers = Math.Clamp(maxPlayersElement, min: 1, max: NetConfig.MaxPlayers); + + var karmaEnabled = serverSettings.GetAttributeBool("karmaenabled", true); + var selectedPlayStyle = serverSettings.GetAttributeEnum("playstyle", PlayStyle.Casual); Vector2 textLabelSize = new Vector2(1.0f, 0.05f); Alignment textAlignment = Alignment.CenterLeft; Vector2 textFieldSize = new Vector2(0.5f, 1.0f); Vector2 tickBoxSize = new Vector2(0.4f, 0.04f); - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.9f), menuTabs[Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.95f), menuTabs[Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.01f, Stretch = true @@ -1321,7 +1335,7 @@ namespace Barotrauma //other settings ----------------------------------------------------- //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); var label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerName"), textAlignment: textAlignment); serverNameBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), text: name, textAlignment: textAlignment) @@ -1373,6 +1387,21 @@ namespace Barotrauma }; label.RectTransform.IsFixedSize = true; + var languageLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), + TextManager.Get("Language"), textAlignment: textAlignment); + languageDropdown = new GUIDropDown(new RectTransform(textFieldSize, languageLabel.RectTransform, Anchor.CenterRight)); + foreach (var language in ServerLanguageOptions.Options) + { + languageDropdown.AddItem(language.Label, language.Identifier); + } + var defaultLanguage = ServerLanguageOptions.PickLanguage(GameSettings.CurrentConfig.Language); + var settingsLanguage = serverSettings.GetAttributeIdentifier("language", defaultLanguage.Value).ToLanguageIdentifier(); + if (!ServerLanguageOptions.Options.Any(o => o.Identifier == settingsLanguage)) + { + settingsLanguage = defaultLanguage; + } + languageDropdown.Select(ServerLanguageOptions.Options.FindIndex(o => o.Identifier == settingsLanguage)); + var serverExecutableLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerExecutable"), textAlignment: textAlignment); const string vanillaServerOption = "Vanilla"; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index df051fb9c..8536cf80a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -398,12 +398,9 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { - GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); //wtf - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); - + GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); GUI.Draw(Cam, spriteBatch); - spriteBatch.End(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 59514c304..74ad6abde 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -301,7 +301,7 @@ namespace Barotrauma levelSeed = value; int intSeed = ToolBox.StringToInt(levelSeed); - backgroundSprite = LocationType.Random(new MTRandom(intSeed))?.GetPortrait(intSeed); + backgroundSprite = LocationType.Random(new MTRandom(intSeed), predicate: lt => lt.UsePortraitInRandomLoadingScreens)?.GetPortrait(intSeed); SeedBox.Text = levelSeed; } } @@ -1934,7 +1934,7 @@ namespace Barotrauma var selectedSub = component.UserData as SubmarineInfo; if (SelectedMode == GameModePreset.MultiPlayerCampaign && CampaignSetupUI != null) { - if (selectedSub.Price > CampaignSetupUI.CurrentSettings.InitialMoney) + if (selectedSub.Price > CampaignSettings.CurrentSettings.InitialMoney) { new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("campaignsubtooexpensive")); } @@ -2244,9 +2244,9 @@ namespace Barotrauma List rankOptions = new List(); foreach (PermissionPreset rank in PermissionPreset.List) { - rankOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => + rankOptions.Add(new ContextMenuOption(rank.DisplayName, isEnabled: true, onSelected: () => { - LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.Name)); + LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.DisplayName)); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked = delegate @@ -2350,7 +2350,7 @@ namespace Barotrauma }; foreach (PermissionPreset permissionPreset in PermissionPreset.List) { - rankDropDown.AddItem(permissionPreset.Name, permissionPreset, permissionPreset.Description); + rankDropDown.AddItem(permissionPreset.DisplayName, permissionPreset, permissionPreset.Description); } rankDropDown.AddItem(TextManager.Get("CustomRank"), null); @@ -2758,12 +2758,10 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { + if (backgroundSprite?.Texture == null) { return; } graphics.Clear(Color.Black); - - GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite); - spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - + GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White); GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } @@ -3315,7 +3313,7 @@ namespace Barotrauma foreach (var subElement in SubList.Content.Children) { var sub = subElement.UserData as SubmarineInfo; - bool tooExpensive = sub.Price > CampaignSetupUI.CurrentSettings.InitialMoney; + bool tooExpensive = sub.Price > CampaignSettings.CurrentSettings.InitialMoney; if (tooExpensive || !sub.IsCampaignCompatible) { foreach (var textBlock in subElement.GetAllChildren()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index c48aa0ac5..56c74dcc3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -166,7 +166,7 @@ namespace Barotrauma { prefabList.ClearChildren(); - var particlePrefabs = GameMain.ParticleManager.GetPrefabList(); + var particlePrefabs = ParticleManager.GetPrefabList(); foreach (ParticlePrefab particlePrefab in particlePrefabs) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), prefabList.Content.RectTransform) { MinSize = new Point(0, 20) }, @@ -204,7 +204,7 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } - var prefabList = GameMain.ParticleManager.GetPrefabList(); + var prefabList = ParticleManager.GetPrefabList(); foreach (ParticlePrefab prefab in prefabList) { foreach (XElement element in doc.Root.Elements()) @@ -273,7 +273,7 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } - var prefabList = GameMain.ParticleManager.GetPrefabList(); + var prefabList = ParticleManager.GetPrefabList(); foreach (ParticlePrefab otherPrefab in prefabList) { foreach (var subElement in doc.Root.Elements()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index f86553cb7..5d1831408 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -7,8 +7,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Xml.Linq; namespace Barotrauma @@ -241,6 +239,7 @@ namespace Barotrauma private GUITickBox filterPassword; private GUITickBox filterFull; private GUITickBox filterEmpty; + private GUIDropDown languageDropdown; private Dictionary ternaryFilters; private Dictionary filterTickBoxes; private Dictionary playStyleTickBoxes; @@ -255,6 +254,7 @@ namespace Barotrauma private TernaryOption filterModdedValue = TernaryOption.Any; private ColumnLabel sortedBy; + private bool sortedAscending = true; private const float sidebarWidth = 0.2f; public ServerListScreen() @@ -425,10 +425,13 @@ namespace Barotrauma ternaryFilters = new Dictionary(); filterTickBoxes = new Dictionary(); + RectTransform createFilterRectT() + => new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform); + GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false) { text ??= TextManager.Get(key); - var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), text) + var tickBox = new GUITickBox(createFilterRectT(), text) { UserData = text, Selected = defaultState, @@ -450,6 +453,109 @@ namespace Barotrauma filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); + // Language filter + if (ServerLanguageOptions.Options.Any()) + { + var languageKey = "Language".ToIdentifier(); + var allLanguagesKey = "AllLanguages".ToIdentifier(); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get(languageKey), font: GUIStyle.SubHeadingFont) + { + CanBeFocused = false + }; + + languageDropdown = new GUIDropDown(createFilterRectT(), selectMultiple: true); + + languageDropdown.AddItem(TextManager.Get(allLanguagesKey), allLanguagesKey); + var allTickbox = languageDropdown.ListBox.Content.FindChild(allLanguagesKey)?.GetChild(); + + // Spacer between "All" and the individual languages + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), languageDropdown.ListBox.Content.RectTransform) + { + MinSize = new Point(0, GUI.IntScaleCeiling(2)) + }, style: null) + { + Color = Color.DarkGray, + CanBeFocused = false + }; + + var selectedLanguages + = ServerListFilters.Instance.GetAttributeLanguageIdentifierArray( + languageKey, + Array.Empty()); + foreach (var (label, identifier, _) in ServerLanguageOptions.Options) + { + languageDropdown.AddItem(label, identifier); + } + + if (!selectedLanguages.Any()) + { + selectedLanguages = ServerLanguageOptions.Options.Select(o => o.Identifier).ToArray(); + } + + foreach (var lang in selectedLanguages) + { + languageDropdown.SelectItem(lang); + } + + if (ServerLanguageOptions.Options.All(o => selectedLanguages.Any(l => o.Identifier == l))) + { + languageDropdown.SelectItem(allLanguagesKey); + languageDropdown.Text = TextManager.Get(allLanguagesKey); + } + + var langTickboxes = languageDropdown.ListBox.Content.Children + .Where(c => c.UserData is LanguageIdentifier) + .Select(c => c.GetChild()) + .ToArray(); + + bool inSelectedCall = false; + languageDropdown.OnSelected = (_, userData) => + { + if (inSelectedCall) { return true; } + try + { + inSelectedCall = true; + + if (Equals(allLanguagesKey, userData)) + { + foreach (var tb in langTickboxes) + { + tb.Selected = allTickbox.Selected; + } + } + + bool noneSelected = langTickboxes.All(tb => !tb.Selected); + bool allSelected = langTickboxes.All(tb => tb.Selected); + + if (allSelected != allTickbox.Selected) + { + allTickbox.Selected = allSelected; + } + + if (allSelected) + { + languageDropdown.Text = TextManager.Get(allLanguagesKey); + } + else if (noneSelected) + { + languageDropdown.Text = TextManager.Get("None"); + } + + var languages = languageDropdown.SelectedDataMultiple.OfType(); + + ServerListFilters.Instance.SetAttribute(languageKey, string.Join(", ", languages)); + GameSettings.SaveCurrentConfig(); + return true; + } + finally + { + inSelectedCall = false; + FilterServers(); + } + }; + } + // Filter Tags new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont) { @@ -713,7 +819,7 @@ namespace Barotrauma private void SortList(ColumnLabel sortBy, bool toggle) { - if (!(labelHolder.GetChildByUserData(sortBy) is GUIButton button)) { return; } + if (labelHolder.GetChildByUserData(sortBy) is not GUIButton button) { return; } sortedBy = sortBy; @@ -730,51 +836,74 @@ namespace Barotrauma } } - bool ascending = arrowUp.Visible; + sortedAscending = arrowUp.Visible; if (toggle) { - ascending = !ascending; + sortedAscending = !sortedAscending; } - arrowUp.Visible = ascending; - arrowDown.Visible = !ascending; + arrowUp.Visible = sortedAscending; + arrowDown.Visible = !sortedAscending; serverList.Content.RectTransform.SortChildren((c1, c2) => { - if (!(c1.GUIComponent.UserData is ServerInfo s1)) { return 0; } - if (!(c2.GUIComponent.UserData is ServerInfo s2)) { return 0; } - - switch (sortBy) - { - case ColumnLabel.ServerListCompatible: - bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion); - bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); - - if (s1Compatible == s2Compatible) { return 0; } - return (s1Compatible ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListHasPassword: - if (s1.HasPassword == s2.HasPassword) { return 0; } - return (s1.HasPassword ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListName: - // I think we actually want culture-specific sorting here? - return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * (ascending ? 1 : -1); - case ColumnLabel.ServerListRoundStarted: - if (s1.GameStarted == s2.GameStarted) { return 0; } - return (s1.GameStarted ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListPlayers: - return s2.PlayerCount.CompareTo(s1.PlayerCount) * (ascending ? 1 : -1); - case ColumnLabel.ServerListPing: - return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch - { - (false, false) => 0, - (true, true) => s2Ping.CompareTo(s1Ping) * (ascending ? 1 : -1), - (false, true) => 1, - (true, false) => -1 - }; - default: - return 0; - } + if (c1.GUIComponent.UserData is not ServerInfo s1) { return 0; } + if (c2.GUIComponent.UserData is not ServerInfo s2) { return 0; } + int comparison = sortedAscending ? 1 : -1; + return CompareServer(sortBy, s1, s2) * comparison; }); } + + private void InsertServer(ServerInfo serverInfo, GUIComponent component) + { + var children = serverList.Content.RectTransform.Children.Reverse().ToList(); + + int comparison = sortedAscending ? 1 : -1; + foreach (var child in children) + { + if (child.GUIComponent.UserData is not ServerInfo serverInfo2 || serverInfo.Equals(serverInfo2)) { continue; } + if (CompareServer(sortedBy, serverInfo, serverInfo2) * comparison >= 0) + { + var index = serverList.Content.RectTransform.GetChildIndex(child); + component.RectTransform.RepositionChildInHierarchy(index + 1); + return; + } + } + component.RectTransform.SetAsFirstChild(); + } + + private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2) + { + switch (sortBy) + { + case ColumnLabel.ServerListCompatible: + bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion); + bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); + + if (s1Compatible == s2Compatible) { return 0; } + return s1Compatible ? -1 : 1; + case ColumnLabel.ServerListHasPassword: + if (s1.HasPassword == s2.HasPassword) { return 0; } + return s1.HasPassword ? 1 : -1; + case ColumnLabel.ServerListName: + // I think we actually want culture-specific sorting here? + return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture); + case ColumnLabel.ServerListRoundStarted: + if (s1.GameStarted == s2.GameStarted) { return 0; } + return s1.GameStarted ? 1 : -1; + case ColumnLabel.ServerListPlayers: + return s2.PlayerCount.CompareTo(s1.PlayerCount); + case ColumnLabel.ServerListPing: + return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch + { + (false, false) => 0, + (true, true) => s2Ping.CompareTo(s1Ping), + (false, true) => 1, + (true, false) => -1 + }; + default: + return 0; + } + } public override void Select() { @@ -821,6 +950,7 @@ namespace Barotrauma UpdateFriendsList(); panelAnimator?.Update(); + scanServersButton.Enabled = (DateTime.Now - lastRefreshTime) >= AllowedRefreshInterval; if (PlayerInput.PrimaryMouseButtonClicked()) @@ -840,7 +970,7 @@ namespace Barotrauma RemoveMsgFromServerList(MsgUserData.NoMatchingServers); foreach (GUIComponent child in serverList.Content.Children) { - if (!(child.UserData is ServerInfo serverInfo)) { continue; } + if (child.UserData is not ServerInfo serverInfo) { continue; } child.Visible = ShouldShowServer(serverInfo); } @@ -851,6 +981,20 @@ namespace Barotrauma serverList.UpdateScrollBarSize(); } + private bool AllLanguagesVisible + { + get + { + if (languageDropdown is null) { return true; } + + // CountChildren-1 because there's a separator element in there that can't be selected + int tickBoxCount = languageDropdown.ListBox.Content.CountChildren - 1; + int selectedCount = languageDropdown.SelectedIndexMultiple.Count(); + + return selectedCount >= tickBoxCount; + } + } + private bool ShouldShowServer(ServerInfo serverInfo) { #if !DEBUG @@ -918,6 +1062,14 @@ namespace Barotrauma } } + if (!AllLanguagesVisible) + { + if (!languageDropdown.SelectedDataMultiple.OfType().Contains(serverInfo.Language)) + { + return false; + } + } + foreach (GUITickBox tickBox in gameModeTickBoxes.Values) { var gameMode = (Identifier)tickBox.UserData; @@ -1031,8 +1183,8 @@ namespace Barotrauma if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } if (info.IsInServer - && info.ConnectCommand is Some { Value: { EndpointOrLobby: var endpointOrLobby } } - && endpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) + && info.ConnectCommand.TryUnwrap(out var command) + && command.EndpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) { const int framePadding = 5; @@ -1270,7 +1422,7 @@ namespace Barotrauma serverPreview.Content.ClearChildren(); panelAnimator.RightEnabled = false; joinButton.Enabled = false; - selectedServer = null; + selectedServer = Option.None; if (selectedTab == TabEnum.All) { @@ -1370,8 +1522,7 @@ namespace Barotrauma UpdateServerInfoUI(serverInfo); if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } - SortList(sortedBy, toggle: false); - FilterServers(); + InsertServer(serverInfo, serverFrame); } private void UpdateServerInfoUI(ServerInfo serverInfo) @@ -1629,7 +1780,7 @@ namespace Barotrauma #endif } - private Color GetPingTextColor(int ping) + private static Color GetPingTextColor(int ping) { if (ping < 0) { return Color.DarkRed; } return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); @@ -1640,12 +1791,10 @@ namespace Barotrauma graphics.Clear(Color.CornflowerBlue); GameMain.TitleScreen.DrawLoadingText = false; - GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); - + GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); GUI.Draw(Cam, spriteBatch); - spriteBatch.End(); } @@ -1666,6 +1815,7 @@ namespace Barotrauma { ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString()); } + GameSettings.SaveCurrentConfig(); } public void LoadServerFilters() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs new file mode 100644 index 000000000..458614977 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs @@ -0,0 +1,173 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Linq; + +namespace Barotrauma +{ + class SlideshowPlayer : GUIComponent + { + private readonly SlideshowPrefab slideshowPrefab; + private readonly LocalizedString pressAnyKeyText; + + private int state; + + private Color overlayColor, textColor; + + private float timer; + + private LocalizedString currentText; + + public bool LastTextShown => state >= slideshowPrefab.Slides.Length; + public bool Finished => state > slideshowPrefab.Slides.Length; + + public SlideshowPlayer(RectTransform rectT, SlideshowPrefab prefab) : base(null, rectT) + { + slideshowPrefab = prefab; + overlayColor = Color.Black; + textColor = Color.Transparent; + pressAnyKeyText = TextManager.Get("pressanykey"); + RefreshText(); + } + + public void Restart() + { + state = 0; + } + + public void Finish() + { + state = slideshowPrefab.Slides.Length + 1; + } + + protected override void Update(float deltaTime) + { + var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)]; + if (!Visible || (Finished && timer > slide.FadeOutDuration)) { return; } + + timer += deltaTime; + + if (state == 0) + { + overlayColor = Color.Lerp(Color.Black, Color.White, Math.Min((timer - slide.FadeInDelay) / slide.FadeInDuration, 1.0f)); + } + else + { + overlayColor = Color.Lerp(Color.Transparent, Color.White, Math.Min((timer - slide.FadeInDelay) / slide.FadeInDuration, 1.0f)); + } + + if (timer > slide.TextFadeInDelay) + { + textColor = Color.Lerp(Color.Transparent, Color.White, Math.Min((timer - slide.TextFadeInDelay) / slide.TextFadeInDuration, 1.0f)); + if (AnyKeyHit()) + { + if (timer > slide.TextFadeInDelay + slide.FadeInDuration) + { + overlayColor = textColor = Color.Transparent; + timer = 0.0f; + state++; + RefreshText(); + } + else + { + timer = slide.TextFadeInDelay + slide.TextFadeInDuration; + } + } + } + else + { + textColor = Color.Transparent; + if (AnyKeyHit()) + { + timer = slide.TextFadeInDelay + slide.TextFadeInDuration; + } + } + + if (state >= slideshowPrefab.Slides.Length) + { + overlayColor = Color.Lerp(Color.White, Color.Transparent, Math.Min(timer / slide.FadeOutDuration, 1.0f)); + textColor = Color.Lerp(Color.White, Color.Transparent, Math.Min(timer / slide.FadeOutDuration, 1.0f)); + if (timer >= slide.FadeOutDuration) + { + state++; + RefreshText(); + } + } + + static bool AnyKeyHit() + { + return + PlayerInput.GetKeyboardState.GetPressedKeys().Any(k => PlayerInput.KeyHit(k)) || + PlayerInput.PrimaryMouseButtonClicked(); + } + } + + private void RefreshText() + { + var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)]; + currentText = slide.Text + .Replace("[submarine]", Submarine.MainSub?.Info.Name ?? GameMain.GameSession?.SubmarineInfo?.Name ?? "Unknown") + .Replace("[location]", Level.Loaded?.StartOutpost?.Info.Name ?? "Unknown"); + } + + protected override void Draw(SpriteBatch spriteBatch) + { + if (slideshowPrefab.Slides.IsEmpty) { return; } + + var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)]; + if ((Finished && timer > slide.FadeOutDuration)) { return; } + + var overlaySprite = slide.Portrait; + + if (overlaySprite != null) + { + Sprite prevPortrait = null; + if (state > 0 && state < slideshowPrefab.Slides.Length) + { + prevPortrait = slideshowPrefab.Slides[state - 1].Portrait; + DrawOverlay(prevPortrait, Color.White); + } + if (prevPortrait?.Texture != overlaySprite.Texture) + { + DrawOverlay(overlaySprite, overlayColor); + } + } + else + { + GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), overlayColor, isFilled: true); + } + + if (!currentText.IsNullOrEmpty() && textColor.A > 0) + { + var backgroundSprite = GUIStyle.GetComponentStyle("CommandBackground").GetDefaultSprite(); + Vector2 centerPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2; + LocalizedString wrappedText = ToolBox.WrapText(currentText, GameMain.GraphicsWidth / 3, GUIStyle.Font); + Vector2 textSize = GUIStyle.Font.MeasureString(wrappedText); + Vector2 textPos = centerPos - textSize / 2; + backgroundSprite.Draw(spriteBatch, + centerPos, + Color.White * (textColor.A / 255.0f), + origin: backgroundSprite.size / 2, + rotate: 0.0f, + scale: new Vector2(GameMain.GraphicsWidth / 2 / backgroundSprite.size.X, textSize.Y / backgroundSprite.size.Y * 2.0f)); + + GUI.DrawString(spriteBatch, textPos + Vector2.One, wrappedText, Color.Black * (textColor.A / 255.0f)); + GUI.DrawString(spriteBatch, textPos, wrappedText, textColor); + + if (timer > slide.TextFadeInDelay * 2) + { + float alpha = Math.Min(timer - slide.TextFadeInDelay * 2, 1.0f); + Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUIStyle.Font.MeasureString(pressAnyKeyText) / 2; + GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, pressAnyKeyText, Color.Black * (textColor.A / 255.0f) * alpha); + GUI.DrawString(spriteBatch, bottomTextPos, pressAnyKeyText, textColor * alpha); + } + } + + void DrawOverlay(Sprite sprite, Color color) + { + if (sprite.Texture == null) { return; } + GUI.DrawBackgroundSprite(spriteBatch, sprite, color); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 3e95da567..3ad662591 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1158,7 +1158,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in entityLists[categoryKey]) { #if !DEBUG - if (ep.HideInMenus) { continue; } + if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); } @@ -1177,7 +1177,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in MapEntityPrefab.List) { #if !DEBUG - if (ep.HideInMenus) { continue; } + if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, allEntityList.Content); } @@ -1306,7 +1306,6 @@ namespace Barotrauma try { assemblyPrefab.Delete(); - UpdateEntityList(); OpenEntityMenu(MapEntityCategory.ItemAssembly); } catch (Exception e) @@ -2416,6 +2415,17 @@ namespace Barotrauma return true; } }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("beaconstationplacement")) + { + Selected = MainSub.Info.BeaconStationInfo is { Placement: Level.PlacementType.Top }, + OnSelected = (tb) => + { + MainSub.Info.BeaconStationInfo.Placement = tb.Selected ? + Level.PlacementType.Top : + Level.PlacementType.Bottom; + return true; + } + }; beaconSettingsContainer.RectTransform.MinSize = new Point(0, beaconSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); //------------------------------------------------------------------ @@ -3079,9 +3089,17 @@ namespace Barotrauma string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject); existingContentPackage = ContentPackageManager.LocalPackages.GetRegularModByPath(newPackagePath); } - + XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); - doc.SaveSafe(filePath); + try + { + doc.SaveSafe(filePath); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to save the item assembly to \"{filePath}\".", e); + return; + } var result = ContentPackageManager.ReloadContentPackage(existingContentPackage); if (!result.TryUnwrapSuccess(out var resultPackage)) @@ -3635,6 +3653,8 @@ namespace Barotrauma private void OpenEntityMenu(MapEntityCategory? entityCategory) { + UpdateEntityList(); + foreach (GUIButton categoryButton in entityCategoryButtons) { categoryButton.Selected = entityCategory.HasValue ? @@ -3762,14 +3782,14 @@ namespace Barotrauma { if (GUIContextMenu.CurrentContextMenu != null) { return; } - List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? - MapEntity.mapEntityList.Where(me => me.IsHighlighted).ToList() : + List targets = MapEntity.HighlightedEntities.Any(me => !MapEntity.SelectedList.Contains(me)) ? + MapEntity.HighlightedEntities.ToList() : new List(MapEntity.SelectedList); Item target = null; var single = targets.Count == 1 ? targets.Single() : null; - if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) + if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && ic is not Repairable && ic.GuiFrame != null)) { // Do not offer the ability to open the inventory if the inventory should never be drawn var container = item.GetComponent(); @@ -4014,7 +4034,7 @@ namespace Barotrauma pickerMutex = new object(), hexMutex = new object(); - Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.4f : 0.3f, 0.3f); + Vector2 relativeSize = new Vector2(0.4f * GUI.AspectRatioAdjustment, 0.3f); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty(), relativeSize, type: GUIMessageBox.Type.Vote) { @@ -4053,24 +4073,31 @@ namespace Barotrauma GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - colorPicker.RectTransform.RelativeSize.X, 1f), colorLayout.RectTransform), childAnchor: Anchor.TopRight); float currentHue = colorPicker.SelectedHue / 360f; - GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), hueSliderLayout.RectTransform), text: "H:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Hue" }; GUIScrollBar hueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), hueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = currentHue }; - GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + inputType: NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; - GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), satSliderLayout.RectTransform), text: "S:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Saturation"}; GUIScrollBar satScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), satSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedSaturation }; - GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + inputType: NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; - GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), valueSliderLayout.RectTransform), text: "V:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Value"}; GUIScrollBar valueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), valueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedValue }; - GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + inputType: NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; - GUILayoutGroup colorInfoLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), sliderLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.15f }; + GUILayoutGroup colorInfoLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), sliderLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.1f + }; - new GUICustomComponent(new RectTransform(new Vector2(0.4f, 0.8f), colorInfoLayout.RectTransform), (batch, component) => + new GUICustomComponent(new RectTransform(Vector2.One, colorInfoLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), (batch, component) => { Rectangle rect = component.Rect; Point areaSize = new Point(rect.Width, rect.Height / 2); @@ -5236,15 +5263,15 @@ namespace Barotrauma } } - if (PlayerInput.KeyHit(Keys.E) && mode == Mode.Default) + if (PlayerInput.KeyHit(InputType.Use) && mode == Mode.Default) { if (dummyCharacter != null) { if (dummyCharacter.SelectedItem == null) { - foreach (var entity in MapEntity.mapEntityList) + foreach (var entity in MapEntity.HighlightedEntities) { - if (entity is Item item && entity.IsHighlighted && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) + if (entity is Item item && item.Components.Any(ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) { var container = item.GetComponents().ToList(); if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false)) @@ -5327,6 +5354,16 @@ namespace Barotrauma else { var selectables = MapEntity.mapEntityList.Where(entity => entity.SelectableInEditor).ToList(); + foreach (var item in Item.ItemList) + { + //attached wires are not normally selectable (by clicking), + //but let's select them manually when selecting all + var wire = item.GetComponent(); + if (wire != null && wire.Connections.None(c => c == null) && !selectables.Contains(item)) + { + selectables.Add(item); + } + } lock (selectables) { selectables.ForEach(MapEntity.AddSelection); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index a5e37dcdb..7cf2b716b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -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 == "assistant")); + dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.FirstOrDefault(static jp => jp.Identifier == "captain")); dummyCharacter.Info.Name = "Galldren"; dummyCharacter.Inventory.CreateSlots(); dummyCharacter.Info.GiveExperience(999999); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs index 48bde97dc..d96f57b72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs @@ -1,20 +1,18 @@ #nullable enable using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma { - #warning TODO: implement properly public class ServerListFilters { private readonly Dictionary attributes = new Dictionary(); - private ServerListFilters() { } - - private ServerListFilters(XElement elem) + private ServerListFilters(XElement? elem) { - if (elem == null) { return; } + if (elem is null) { return; } foreach (var attr in elem.Attributes()) { attributes.Add(attr.NameAsIdentifier(), attr.Value); @@ -23,8 +21,6 @@ namespace Barotrauma public static void Init(XElement? elem) { - if (elem is null) { return; } - Instance = new ServerListFilters(elem); } @@ -50,17 +46,27 @@ namespace Barotrauma { if (attributes.TryGetValue(key, out string? val)) { - if (Enum.TryParse(val, out T result)) { return result; } + if (Enum.TryParse(val, ignoreCase: true, out T result)) { return result; } } return def; } + public LanguageIdentifier[] GetAttributeLanguageIdentifierArray(Identifier key, LanguageIdentifier[] def) + { + return attributes.TryGetValue(key, out string? val) + ? val.Split(",") + .Select(static s => s.Trim()) + .Where(static s => !s.IsNullOrWhiteSpace()) + .Select(static s => s.ToLanguageIdentifier()).ToArray() + : def; + } + public void SetAttribute(Identifier key, string val) { attributes[key] = val; } - public static ServerListFilters Instance { get; private set; } = new ServerListFilters(); + public static ServerListFilters Instance { get; private set; } = new ServerListFilters(null); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 64ba580ef..f53f49be0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -176,6 +176,10 @@ namespace Barotrauma void updateWaterAmbience(Sound sound, float volume) { SoundChannel chn = waterAmbienceChannels.FirstOrDefault(c => c.Sound == sound); + if (Level.Loaded != null) + { + volume *= Level.Loaded.GenerationParams.WaterAmbienceVolume; + } if (chn is null || !chn.IsPlaying) { if (volume < 0.01f) { return; } @@ -513,6 +517,11 @@ namespace Barotrauma if (musicDisposed) { Thread.Sleep(60); } } + + public static void ForceMusicUpdate() + { + updateMusicTimer = 0.0f; + } private static void UpdateMusic(float deltaTime) { @@ -540,7 +549,7 @@ namespace Barotrauma IEnumerable suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity); int mainTrackIndex = 0; - if (suitableMusic.Count() == 0) + if (suitableMusic.None()) { targetMusic[mainTrackIndex] = null; } @@ -564,11 +573,21 @@ namespace Barotrauma } } - if (Level.Loaded?.Type == LevelData.LevelType.LocationConnection) + if (Level.Loaded != null && (Level.Loaded.Type == LevelData.LevelType.LocationConnection || Level.Loaded.GenerationParams.PlayNoiseLoopInOutpostLevel)) { + Identifier biome = Level.Loaded.LevelData.Biome.Identifier; + if (Level.Loaded.IsEndBiome && GameMain.GameSession?.Campaign is CampaignMode campaign) + { + //don't play end biome music in the path leading up to the end level(s) + if (!campaign.Map.EndLocations.Contains(Level.Loaded.StartLocation)) + { + biome = Level.Loaded.StartLocation.Biome.Identifier; + } + } + // Find background noise loop for the current biome IEnumerable suitableNoiseLoops = Screen.Selected == GameMain.GameScreen ? - GetSuitableMusicClips(Level.Loaded.LevelData.Biome.Identifier, currentIntensity) : + GetSuitableMusicClips(biome, currentIntensity) : Enumerable.Empty(); if (suitableNoiseLoops.Count() == 0) { @@ -597,10 +616,17 @@ namespace Barotrauma targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandomUnsynced(); } + IEnumerable suitableIntensityMusic = Enumerable.Empty(); + if (targetMusic[mainTrackIndex] is { MuteIntensityTracks: false } mainTrack && Screen.Selected == GameMain.GameScreen) + { + float intensity = currentIntensity; + if (mainTrack?.ForceIntensityTrack != null) + { + intensity = mainTrack.ForceIntensityTrack.Value; + } + suitableIntensityMusic = GetSuitableMusicClips("intensity".ToIdentifier(), intensity); + } //get the appropriate intensity layers for current situation - IEnumerable suitableIntensityMusic = Screen.Selected == GameMain.GameScreen ? - GetSuitableMusicClips("intensity".ToIdentifier(), currentIntensity) : - Enumerable.Empty(); int intensityTrackStartIndex = 3; for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { @@ -729,6 +755,14 @@ namespace Barotrauma firstTimeInMainMenu = false; + if (GameMain.GameSession != null) + { + foreach (var mission in GameMain.GameSession.Missions) + { + var missionMusic = mission.GetOverrideMusicType(); + if (!missionMusic.IsEmpty) { return missionMusic; } + } + } if (Character.Controlled != null) { @@ -754,6 +788,11 @@ namespace Barotrauma } } + if (Level.Loaded is { IsEndBiome: true }) + { + return "endlevel".ToIdentifier(); + } + Submarine targetSubmarine = Character.Controlled?.Submarine; if (targetSubmarine != null && targetSubmarine.AtDamageDepth) { @@ -765,8 +804,8 @@ namespace Barotrauma return "deep".ToIdentifier(); } - if (targetSubmarine != null) - { + if (targetSubmarine != null) + { float floodedArea = 0.0f; float totalArea = 0.0f; foreach (Hull hull in Hull.HullList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs index 14b0b68e2..bb3af5eee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -238,17 +238,24 @@ namespace Barotrauma public readonly float Volume; public readonly Vector2 IntensityRange; + public readonly bool MuteIntensityTracks; + public readonly float? ForceIntensityTrack; public readonly bool ContinueFromPreviousTime; public int PreviousTime; public BackgroundMusic(ContentXElement element, SoundsFile file) : base(element, file, stream: true) { - Type = element.GetAttributeIdentifier("type", ""); - IntensityRange = element.GetAttributeVector2("intensityrange", new Vector2(0.0f, 100.0f)); - DuckVolume = element.GetAttributeBool("duckvolume", false); - this.Volume = element.GetAttributeFloat("volume", 1.0f); - ContinueFromPreviousTime = element.GetAttributeBool("continuefromprevioustime", false); + Type = element.GetAttributeIdentifier(nameof(Type), ""); + IntensityRange = element.GetAttributeVector2(nameof(IntensityRange), new Vector2(0.0f, 100.0f)); + DuckVolume = element.GetAttributeBool(nameof(DuckVolume), false); + MuteIntensityTracks = element.GetAttributeBool(nameof(MuteIntensityTracks), false); + if (element.GetAttribute(nameof(ForceIntensityTrack)) != null) + { + ForceIntensityTrack = element.GetAttributeFloat(nameof(ForceIntensityTrack), 0.0f); + } + Volume = element.GetAttributeFloat(nameof(Volume), 1.0f); + ContinueFromPreviousTime = element.GetAttributeBool(nameof(ContinueFromPreviousTime), false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/ConditionalSprite.cs index 6b3d92896..d6ef57ebc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/ConditionalSprite.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using System.Xml.Linq; -using System.Linq; - -namespace Barotrauma +namespace Barotrauma { partial class ConditionalSprite { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index 765fa93ca..c1c8efe3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -144,13 +144,7 @@ namespace Barotrauma default: continue; } - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionalList.Add(new PropertyConditional(attribute)); - } - } + conditionalList.AddRange(PropertyConditional.FromXElement(subElement)); } } @@ -188,7 +182,6 @@ namespace Barotrauma public float GetRotation(ref float rotationState, float randomRotationFactor) { - RotationSpeed = -Math.Abs(RotationSpeed); switch (RotationAnim) { case AnimationType.Sine: diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 9927c01b8..41be59e49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -72,8 +72,9 @@ namespace Barotrauma float angle = 0.0f; float particleRotation = 0.0f; bool mirrorAngle = false; - if (emitter.Prefab.Properties.CopyEntityAngle) + if (emitter.Prefab.Properties.CopyEntityAngle || emitter.Prefab.Properties.CopyTargetAngle) { + bool entityAngleAssigned = false; Limb targetLimb = null; if (entity is Item item && item.body != null) { @@ -84,19 +85,23 @@ namespace Barotrauma particleRotation += MathHelper.Pi; mirrorAngle = true; } + entityAngleAssigned = true; } - else if (entity is Character c && !c.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) + if (emitter.Prefab.Properties.CopyTargetAngle || !entityAngleAssigned) { - targetLimb = c.AnimController.GetLimb(l); - } - else - { - for (int i = 0; i < targets.Count; i++) + if (entity is Character c && !c.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) { - if (targets[i] is Limb limb) + targetLimb = c.AnimController.GetLimb(l); + } + else + { + for (int i = 0; i < targets.Count; i++) { - targetLimb = limb; - break; + if (targets[i] is Limb limb) + { + targetLimb = limb; + break; + } } } } @@ -108,14 +113,19 @@ namespace Barotrauma particleRotation += offset; if (emitter.Prefab.Properties.CopyEntityDir && targetLimb.body.Dir < 0.0f) { - particleRotation += MathHelper.Pi; - mirrorAngle = true; + angle = targetLimb.body.Rotation + ((targetLimb.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + particleRotation = -targetLimb.body.Rotation; + if (targetLimb.body.Dir < 0.0f) + { + particleRotation += MathHelper.Pi; + mirrorAngle = true; + } } } } emitter.Emit(deltaTime, worldPosition, hull, angle: angle, particleRotation: particleRotation, mirrorAngle: mirrorAngle); - } + } } private bool ignoreMuffling; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index 27c008170..264cf243f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -94,7 +94,7 @@ namespace Barotrauma.Steam CanBeFocused = false }; var itemTitle = new GUITextBlock(new RectTransform(Vector2.One, itemFrame.RectTransform), - text: item.Title); + text: item.Title ?? ""); var itemDownloadProgress = new GUIProgressBar(new RectTransform((0.5f, 0.75f), itemFrame.RectTransform, Anchor.CenterRight), 0.0f) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 258594f3d..831abe5b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -105,7 +105,8 @@ namespace Barotrauma.Steam currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); - currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.UgcId))); + currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp + => cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : ""))); currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); @@ -117,6 +118,7 @@ namespace Barotrauma.Steam currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? ""); + currentLobby?.SetData("language", serverSettings.Language.ToString()); DebugConsole.Log("Lobby updated!"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs index 26b32f26c..918437918 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -16,6 +16,9 @@ namespace Barotrauma.Steam private static readonly List initializationErrors = new List(); public static IReadOnlyList InitializationErrors => initializationErrors; + private static bool IsInitializedProjectSpecific + => Steamworks.SteamClient.IsValid && Steamworks.SteamClient.IsLoggedOn; + private static void InitializeProjectSpecific() { if (IsInitialized) { return; } @@ -23,7 +26,6 @@ namespace Barotrauma.Steam try { Steamworks.SteamClient.Init(AppID, false); - IsInitialized = Steamworks.SteamClient.IsLoggedOn && Steamworks.SteamClient.IsValid; if (IsInitialized) { @@ -43,13 +45,11 @@ namespace Barotrauma.Steam } catch (DllNotFoundException) { - IsInitialized = false; initializationErrors.Add("SteamDllNotFound".ToIdentifier()); } catch (Exception e) { DebugConsole.ThrowError("SteamManager initialization threw an exception", e); - IsInitialized = false; initializationErrors.Add("SteamClientInitFailed".ToIdentifier()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 124e77d1d..ed270c344 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -111,7 +111,7 @@ namespace Barotrauma.Steam { await Task.Yield(); - string thumbnailUrl = item.PreviewImageUrl; + string? thumbnailUrl = item.PreviewImageUrl; if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; } var client = new RestClient(thumbnailUrl); var request = new RestRequest(".", Method.GET); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 415bda37b..779101b67 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Steam private string ExtractTitle(ItemOrPackage itemOrPackage) => itemOrPackage.TryGet(out ContentPackage package) ? package.Name - : ((Steamworks.Ugc.Item)itemOrPackage).Title; + : (((Steamworks.Ugc.Item)itemOrPackage).Title ?? ""); private void CreateWorkshopItemDetailContainer( GUIFrame parent, @@ -340,6 +340,8 @@ namespace Barotrauma.Steam subscribeButton.OnClicked = (button, o) => { + if (!SteamManager.IsInitialized) { return false; } + if (!workshopItem.IsSubscribed) { workshopItem.Subscribe(); @@ -360,6 +362,8 @@ namespace Barotrauma.Steam new RectTransform(Vector2.Zero, subscribeButton.RectTransform), onUpdate: (deltaTime, component) => { + if (!SteamManager.IsInitialized) { return; } + if (subscribeButtonSprite.Style is { Identifier: { } styleId }) { if (workshopItem.IsSubscribed && styleId != minusButton) @@ -380,6 +384,8 @@ namespace Barotrauma.Steam new RectTransform((1.22f, 1.22f), subscribeButtonSprite.RectTransform, Anchor.Center), onDraw: (spriteBatch, component) => { + if (!SteamManager.IsInitialized) { return; } + bool visible = workshopItem.IsSubscribed && (workshopItem.IsDownloading || workshopItem.IsDownloadPending @@ -407,6 +413,8 @@ namespace Barotrauma.Steam }, onUpdate: (deltaTime, component) => { + if (!SteamManager.IsInitialized) { return; } + displayedDownloadAmount = Math.Min( workshopItem.DownloadAmount, MathHelper.Lerp(displayedDownloadAmount, workshopItem.DownloadAmount, 0.05f)); @@ -450,7 +458,7 @@ namespace Barotrauma.Steam var title = new GUITextBlock( new RectTransform(Vector2.One, itemLayout.RectTransform), - workshopItem.Title, font: GUIStyle.Font) + workshopItem.Title ?? "", font: GUIStyle.Font) { CanBeFocused = false }; @@ -570,7 +578,7 @@ namespace Barotrauma.Steam var titleAndAuthorLayout = new GUILayoutGroup(new RectTransform(Vector2.One, headerLayout.RectTransform)); var selectedTitle = - new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title, + new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title ?? "", font: GUIStyle.LargeFont); var author = workshopItem.Owner; @@ -682,9 +690,9 @@ namespace Barotrauma.Steam TaskPool.Add($"Request username for {author.Id}", author.RequestInfoAsync(), (t) => { - authorButton.Text = author.Name; + authorButton.Text = author.Name ?? ""; authorButton.RectTransform.NonScaledSize = - ((int)(authorButton.Font.MeasureString(author.Name).X + authorPadding.X + authorPadding.Z), + ((int)(authorButton.Font.MeasureString(author.Name ?? "").X + authorPadding.X + authorPadding.Z), authorButton.RectTransform.NonScaledSize.Y); }); @@ -769,7 +777,7 @@ namespace Barotrauma.Steam var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), TextManager.Get("WorkshopItemTags"), font: GUIStyle.SubHeadingFont); - CreateTagsList(workshopItem.Tags.ToIdentifiers(), new RectTransform((0.97f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); + CreateTagsList((workshopItem.Tags ?? Array.Empty()).ToIdentifiers(), new RectTransform((0.97f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); #endregion var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs index 7a3a9b85d..12b4b2876 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs @@ -113,7 +113,7 @@ namespace Barotrauma { case ModType.Workshop: pkgElem.SetAttributeValue("name", pkg.Name); - if (pkg.UgcId.TryUnwrap(out ContentPackageId ugcId)) + if (pkg.UgcId.TryUnwrap(out var ugcId)) { pkgElem.SetAttributeValue("id", ugcId.ToString()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index 272f9e4c9..c5342a37c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -142,11 +142,17 @@ namespace Barotrauma GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform); - Submarine.Draw(spriteBatch); - Submarine.DrawFront(spriteBatch); - Submarine.DrawDamageable(spriteBatch, null); - spriteBatch.End(); + DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null))); + DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => (e is not Structure || e.SpriteDepth < 0.9f))); + DrawBatch(() => Submarine.DrawDamageable(spriteBatch, null, editing: true)); + DrawBatch(() => Submarine.DrawFront(spriteBatch, editing: true)); + + void DrawBatch(Action drawAction) + { + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform); + drawAction.Invoke(); + spriteBatch.End(); + } GameMain.Instance.GraphicsDevice.SetRenderTarget(null); GameMain.Instance.GraphicsDevice.Viewport = prevViewport; diff --git a/Barotrauma/BarotraumaClient/Content/Effects/damageshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/damageshader.xnb index 37e87a0173a8f7f5c73ad61cd294b75ae7814cff..4b95d883feb67d25dc3adad5278be84be922ef21 100644 GIT binary patch delta 717 zcmYLH&ubG=5dP-9-Q;B<7%C#fpo^4RZNU~rv_fr?n(Dz=x=PbyvBefb1BpL+GD|%b zK?th{MKFR?dK0{Q5f2JpdXkDKOa21MMK8~Jn|-)2JM-rI=9_u5`^|ehq-xdDLzjj# z9rgIia|PU(tJDyIwc&JGgyqv5_Xe>tJ$s?nIF8`j?7}N% z%mBE{wgfnpr#?rl%3E3FeS~7HJ%n7Gn@YSF`XLV(Kdg>P?r_p#iKm$Xei2Z%-b=82 z=*OU`&BBrSB!3mi{N|WPLM6wa()dTY=#0#^jgIYM<}jS8M90MEcm~_Y>e+s#cFP~! z!RBl&m*N7DY9@GP%Qva`ndVJj59X?MAUcrm4*&lZ6iKhusIRX?FPvl1Gv_65>a1HB iIU3ws-MF(}57w4fR-1uglpFO%6Xeb>w_20XX4ZeKk9Pq8 delta 164 zcmZ1?befAJ!q2IkmBE{JB1f~$9u@`$Uw5|%4#wq&f$TV4-X+9(47NNiAYvWDQ6x ME;eNN544*B0O(&OB>(^b diff --git a/Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/damageshader_opengl.xnb index f51787721cdf3843813eb6172ccb69f127305f90..5cffd1d1a6f5697268c8d718c6bff009fdf87944 100644 GIT binary patch delta 405 zcmX@l^+=E-!p|v%mGKeBM2=?uU=9WbUw5|%4hH?JXHu9Z&QewsC@79E&C5*7FUl>B zFH21}abRF!Wnf?sU|>{WU@~A}+PLo&vmhG-BO?PVP-(FtJ0nn*d*Xp?Mw7`pjM|e^ z7<(oQGaa%tP_R`1Dkw70D+dAvZ3SH*FWE@10tl>mxi~>`hI-`)RSH@l^~uJQ`Iw~{ z4JKM&xGZX+SKi&;i>DRG|q|0(1e?1f$7}EFwGxdKHyb z5Rv4`aV%mg20)dyyj;9oOhB(dyk&smLq-OU$un3gI2f6^it}?aQzp-4k)ABTs^rYX t8(fl_mz8*WRN(DTrfzZbAC=fk}6Qp0RXKZYu^9> delta 72 zcmV-O0Js0*5zh(;SWZG@1prS4kqCnizytsQO-Dvp2msJVJR1R#nj--LvEIr95Cs4M e0RRPXb6;~Y1_1y72b1Clc$0kyA^`!Dpa>kWq!LsB diff --git a/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb index 7f09d5fe633c65a2edafa7e0eeb973a1442cc946..02d69944238fdc31774804ee34bcae17bac0813b 100644 GIT binary patch delta 842 zcmZuvJ!=$E6g~I7nasEfyBMp)Vo?wa5e*31Y|Lge3ZkI0xDg9Kq9DqO`5=%Y`@l{@ zNEj=z*c53}1yY7sSojAN`~!=XHiC`KcG$@2?cMh3#h;7?)~Lph z9l%!-0l@?AsaNeQABc6sWZ9f-{y1||UHu|+8wf4RUBK12C)4wCu4ACd;#joHdzalt za_m$yu+)00ZRMIF-}D+wsWlJ#Y>u7QS$eR?)#)K9lj}K&dtJpKrl;bLCkzQQP{1jj z9l$}}unQW{)CP!Y03XoD40QoA<-E=mkM+5m4ywNHBCignD))h~n;x9?10*-_cSM`|9m;sLbqON?> zuUpMA9u_BSJ{8)?i9-)Jx&k$BVGiz&UL2xdfF&+?G} zW}Nc<6Tol?`}_cEpqj3+;?(l=z8RO=UfURn__y#;g1u$?MI=mbgqKEPk3D|5G~al> Qu=uF` zhV;Uz8HekGr}NjtkCpo1{_PLX55K~I-8tk1!q8Zvz9E9slhsdp7Enczq4Pak;kTWp8?65HPc0z+YEsU;3q)d6`f~ROQ}K42dFjoxZ<3#H1j$U6xJ)Xmq>b~rl+bb z)S%rW_SrMKt#M+X^iJ(HZ2b*j><4D(M#jK;(dCG7P%=N5EHmRUNia+Uy}(i8DF`oB RdRy+hh?CBE%jvOK{|EBSK79ZH diff --git a/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb b/Barotrauma/BarotraumaClient/Content/Effects/wearableclip.xnb index 1f290fc5c1ed740460bf482de9f1bb57fc3dbc8f..9951e2186b56992ef5c477c02a0f176eb5820b01 100644 GIT binary patch literal 2416 zcmbtWO>Y}j6ur-5dnOjeZY2<~fDvM8StONcimfjK{yQCqe7$CER+Y$d%;0CxLYWOy;0B`71rAApf%bGnx6-QyuT6l!gaV@ zIOhHBU@+{2z2&QgYlX$a;`JMOzc}uW#)DwF7mP=PW;gF|j(5AA)(63ZT6j0;E%(OV zu5hV8Jdus{a>G+==a0Wr(w7v~IZ14aQ$CsD(|M}*} zy_*W2v?X!AjqN!i|D{Fx(A^GuH-1lua7$u3{T^qDqU7;OsRa2gDok+>qsgaWrI-X!7nFk($ zO(sd*7I7W8J`tHl)G1NW9YZ(OIul2oL?E_t#C2fCiH%D@cMKilv@h~ynJaRne#D{@ z8rOl3HFk{sVG=yFCyiZW){Y6mSB{LVqLlSZqek)qvnD4EKlA`>@}^Axz-QX-T7Pg& z$mLl~*|o9c@gTe4q2DSno<~Fu<(+Aq22RglrR%-0d%)g|-34|n&S*Y`_$g|gM#P-8 z3z>Bjv#*r48%(#r}lxJ2{PGSphX(k)-sEzFxn;v=Q&- z#5vF9ShSXxAYAg!TW*r%}h!{^m36jKOl5hpRku(zYm)nYSfYV3=E%B9359 ze})gPke(a+n&-KS*i6+G#^09b zT~4lL7;C{{(5-g92rg19=Y=8u)3iF>`u4p}4+c}Q&iRzFdHA%^alJzj@;{9!kcz6| YG75Fc!sX_u(Z}9h>#-7)}mnPcK#SBQpk$H51hl(X2n?XIYRX^ovj@Pi}?uCtHGCZ)G z(?6-=c$~3?y2U3?2{(a?)J)h(BmYN zo;y|cknV$OsH8Ks>d+|7%v4IRC z9z^WW^SBdz;fdPk1i(=<&(z;d!+(Qrr<(1RZi$jE5uY`O=QwuCs^$^?N*Ug;%XqD@i(v z+30fm&z&299T|jC1~?tskA<4a%sS2gSj^l9It`@EqF@z;Mp0-Mg;r5$7llqy=oW<@ z2z$^C5pdjqW3Fz)yjeu7Rm56C8mmZS32Ck(%_U?_OIu4wdo@ZMkaRAgJskO5H4YF9 zv8U@Taf59&7s0(A)#OOQ86HSqs>KFPOj@wPz!tnN_-r7haxqh(s}VS5rUi=}nDpMi zg;(c3PKJkSB85wbwg_tb)yns3Wd~|kCY#OS=)hxyj}SjmKTNgS>9B`ssOMw6V{%H5W(X%3cimxAUAx7mc~NnvQly(A;F%$MmMPLs5vV#taL9d{ zBE1v=&l~|j_r7?T8`)tN9I7=F`w9~D}Pps3R=f}$Sxkxj)SQU}r1E>fYbP@%2dwYJx#gOgB@lAj>> z1KdPV+#LiR{5Se0N0M_(p3gULJ{F!zcJ1__X+U7j>|5`_1UNaW)@-QW{j^B=I<618 z?aN-&iR(A52CYCI0Eh!Q4+u<++56QZJ>MzLH+tQ;?{JBOo(5$VQfFUe%HDEI>?QX) zNt``rn^U1XLVW>`k>`#O6?%6pLEjy5Euwwo6Ah1tT0E3+AR?N*{yGozw?vw^p=q9l z^d!ssipR1v6lY1CP412`Qrdk4#VBG_NvoOoe45Yrl>#4|3eI?JT9DlUm3+1}RIP!l zlBfbXEk@x%?9*ChWz!-ado%(PuwP@jgjI=pA{7u8X(;5-8oD)2wN~HvyS|azyp3P(SZC#Y}KnNK45wSl<01S3$-?L(2 F{|~?T#h3s9 diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 5c27ba425..0e2ac8220 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index aec4b86f1..fd6943fa0 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx index 6f0781293..3ec964651 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx @@ -5,6 +5,8 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = ; } Texture2D xStencil; sampler StencilSampler = sampler_state { Texture = ; }; +float4 solidColor; + float4 inColor; float aCutoff; @@ -16,7 +18,6 @@ float cMultiplier; float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor; - float4 stencilColor = xStencil.Sample(StencilSampler, texCoord); float aDiff = stencilColor.a - aCutoff; @@ -30,6 +31,18 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord min(aDiff * aMultiplier, c.a)); } +float4 solidColorStencil(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor; + float4 stencilColor = xStencil.Sample(StencilSampler, texCoord); + + float aDiff = stencilColor.a - aCutoff; + + clip(aDiff); + + return float4(solidColor.rgb, solidColor.a * min(aDiff * aMultiplier, c.a)); +} + technique StencilShader { pass Pass1 @@ -37,3 +50,11 @@ technique StencilShader PixelShader = compile ps_4_0_level_9_1 main(); } } + +technique StencilShaderSolidColor +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 solidColorStencil(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx index 3a4242a3a..69370113c 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx @@ -5,6 +5,8 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = ; } Texture xStencil; sampler StencilSampler = sampler_state { Texture = ; }; +float4 solidColor; + float4 inColor; float aCutoff; @@ -16,7 +18,6 @@ float cMultiplier; float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = tex2D(TextureSampler, texCoord) * inColor; - float4 stencilColor = tex2D(StencilSampler, texCoord); float aDiff = stencilColor.a - aCutoff; @@ -30,6 +31,18 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord min(aDiff * aMultiplier, c.a)); } +float4 solidColorStencil(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 c = tex2D(TextureSampler, texCoord) * inColor; + float4 stencilColor = tex2D(StencilSampler, texCoord); + + float aDiff = stencilColor.a - aCutoff; + + clip(aDiff); + + return float4(solidColor.rgb, solidColor.a * min(aDiff * aMultiplier, c.a)); +} + technique StencilShader { pass Pass1 @@ -37,3 +50,11 @@ technique StencilShader PixelShader = compile ps_2_0 main(); } } + +technique StencilShaderSolidColor +{ + pass Pass1 + { + PixelShader = compile ps_2_0 solidColorStencil(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Shaders/losshader.fx b/Barotrauma/BarotraumaClient/Shaders/losshader.fx index f761aa1bc..1e93bd9fb 100644 --- a/Barotrauma/BarotraumaClient/Shaders/losshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/losshader.fx @@ -30,11 +30,18 @@ float xLosAlpha; float4 xColor; +float blurDistance; + float4 mainPS(VertexShaderOutput input) : COLOR0 { float4 sampleColor = xTexture.Sample(TextureSampler, input.TexCoords); - float4 losColor = xLosTexture.Sample(LosSampler, input.TexCoords); - + + float4 losColor = xLosTexture.Sample(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y + blurDistance)); + losColor += xLosTexture.Sample(LosSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y - blurDistance)); + losColor += xLosTexture.Sample(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y - blurDistance)); + losColor += xLosTexture.Sample(LosSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y + blurDistance)); + losColor = losColor * 0.25f; + float obscureAmount = 1.0f - losColor.r; float4 outColor = float4( @@ -53,4 +60,4 @@ technique LosShader VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 mainPS(); } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx index 4ea589cf4..6d08d4460 100644 --- a/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip.fx @@ -9,6 +9,8 @@ float aCutoff; float4x4 wearableUvToClipperUv; float clipperTexelSize; +float2 stencilUVmin, stencilUVmax; + float stencilSample(float2 texCoord, float2 offset) { return xStencil.Sample( @@ -20,6 +22,12 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord { float4 c = xTexture.Sample(TextureSampler, texCoord) * color; + float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; + clip(stencilUV.x - stencilUVmin.x); + clip(stencilUV.y - stencilUVmin.y); + clip(stencilUVmax.y - stencilUV.x); + clip(stencilUVmax.y - stencilUV.y); + float minStencil = stencilSample(texCoord, float2(0,0)); minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); minStencil = min(minStencil, stencilSample(texCoord, float2(clipperTexelSize,0))); diff --git a/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx index 25dd7f3d3..d845e79d3 100644 --- a/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/wearableclip_opengl.fx @@ -9,6 +9,8 @@ float aCutoff; float4x4 wearableUvToClipperUv; float clipperTexelSize; +float2 stencilUVmin, stencilUVmax; + float stencilSample(float2 texCoord, float2 offset) { return tex2D( @@ -19,6 +21,12 @@ float stencilSample(float2 texCoord, float2 offset) float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = tex2D(TextureSampler, texCoord) * color; + + float2 stencilUV = mul(float4(texCoord.x, texCoord.y, 0, 1), wearableUvToClipperUv).xy; + clip(stencilUV.x - stencilUVmin.x); + clip(stencilUV.y - stencilUVmin.y); + clip(stencilUVmax.y - stencilUV.x); + clip(stencilUVmax.y - stencilUV.y); float minStencil = stencilSample(texCoord, float2(0,0)); minStencil = min(minStencil, stencilSample(texCoord, float2(-clipperTexelSize,0))); diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index adc6b7153..0e53d121f 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -14,7 +14,7 @@ Debug;Release;Unstable true app.manifest - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 5ee5b2bc2..f271ea211 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 6692e0d12..d884d6e0d 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index a8b32422a..0f0014017 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -71,6 +71,11 @@ namespace Barotrauma msg.WriteString(ragdollFileName); msg.WriteIdentifier(HumanPrefabIds.NpcIdentifier); + msg.WriteIdentifier(MinReputationToHire.factionId); + if (MinReputationToHire.factionId != default) + { + msg.WriteSingle(MinReputationToHire.reputation); + } if (Job != null) { msg.WriteUInt32(Job.Prefab.UintIdentifier); @@ -86,7 +91,7 @@ namespace Barotrauma msg.WriteByte((byte)0); } - msg.WriteUInt16((ushort)ExperiencePoints); + msg.WriteInt32(ExperiencePoints); msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index b960f2f7d..5eff578d3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -692,6 +692,7 @@ namespace Barotrauma { msg.WriteIdentifier(MerchantIdentifier); } + msg.WriteIdentifier(Faction); int msgLengthBeforeOrders = msg.LengthBytes; // Current orders diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 9379178b2..a4c7e8509 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -13,6 +13,15 @@ namespace Barotrauma { static partial class DebugConsole { + private static readonly RateLimiter rateLimiter = new( + maxRequests: 10, + expiryInSeconds: 5, + punishmentRules: new[] + { + (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce), + (RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick) + }); + public partial class Command { /// @@ -608,12 +617,12 @@ namespace Barotrauma NewMessage("Valid ranks are:", Color.White); foreach (PermissionPreset permissionPreset in PermissionPreset.List) { - NewMessage(" - " + permissionPreset.Name, Color.White); + NewMessage(" - " + permissionPreset.DisplayName, Color.White); } ShowQuestionPrompt("Rank to grant to \"" + client.Name + "\"?", (rank) => { - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); + PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { ThrowError("Rank \"" + rank + "\" not found."); @@ -622,7 +631,7 @@ namespace Barotrauma client.SetPermissions(preset.Permissions, preset.PermittedCommands); GameMain.Server.UpdateClientPermissions(client); - NewMessage("Assigned the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White); + NewMessage("Assigned the rank \"" + preset.DisplayName + "\" to " + client.Name + ".", Color.White); }, args, 1); }); @@ -1401,6 +1410,44 @@ namespace Barotrauma })); + commands.Add(new Command("forcelocationtypechange", "", (string[] args) => + { + if (GameMain.Server == null || GameMain.GameSession?.Campaign == null) { return; } + + if (args.Length < 2) + { + ThrowError("Invalid parameters. The command should be formatted as \"forcelocationtypechange [locationname] [locationtype]\". If the names consist of multiple words, you should surround them with quotation marks."); + return; + } + + var location = GameMain.GameSession.Campaign.Map.Locations.FirstOrDefault(l => l.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (location == null) + { + ThrowError($"Could not find a location with the name {args[0]}."); + return; + } + + var locationType = LocationType.Prefabs.FirstOrDefault(lt => + lt.Name.Equals(args[1], StringComparison.OrdinalIgnoreCase) || lt.Identifier == args[1]); + if (location == null) + { + ThrowError($"Could not find the location type {args[1]}."); + return; + } + + location.ChangeType(GameMain.GameSession.Campaign, locationType); + }, + () => + { + if (GameMain.GameSession?.Campaign == null) { return null; } + + return new string[][] + { + GameMain.GameSession.Campaign.Map.Locations.Select(l => l.Name).ToArray(), + LocationType.Prefabs.Select(lt => lt.Name.Value).ToArray() + }; + })); + AssignOnExecute("resetcharacternetstate", (string[] args) => { if (GameMain.Server == null) { return; } @@ -1672,13 +1719,7 @@ namespace Barotrauma "teleportsub", (Client client, Vector2 cursorWorldPos, string[] args) => { - if (Submarine.MainSub == null || Level.Loaded == null) return; - if (Level.Loaded.Type == LevelData.LevelType.Outpost) - { - GameMain.Server.SendConsoleMessage("The teleportsub command is unavailable in outpost levels!", client, Color.Red); - return; - } - + if (Submarine.MainSub == null || Level.Loaded == null) { return; } if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) { Submarine.MainSub.SetPosition(cursorWorldPos); @@ -1934,7 +1975,7 @@ namespace Barotrauma { GameMain.Server.SendConsoleMessage("Could not find the specified character.", client, Color.Red); } - killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); + killedCharacter?.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null); } ); @@ -1960,6 +2001,7 @@ namespace Barotrauma "freecam", (Client client, Vector2 cursorWorldPos, string[] args) => { + client.UsingFreeCam = true; GameMain.Server.SetClientCharacter(client, null); client.SpectateOnly = true; } @@ -2073,7 +2115,7 @@ namespace Barotrauma } string rank = string.Join("", args.Skip(1)); - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); + PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient, Color.Red); @@ -2082,8 +2124,8 @@ namespace Barotrauma client.SetPermissions(preset.Permissions, preset.PermittedCommands); GameMain.Server.UpdateClientPermissions(client); - GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.Name}\" to {client.Name}.", senderClient); - NewMessage(senderClient.Name + " granted the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White); + GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.DisplayName}\" to {client.Name}.", senderClient); + NewMessage(senderClient.Name + " granted the rank \"" + preset.DisplayName + "\" to " + client.Name + ".", Color.White); } ); @@ -2477,7 +2519,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { item.TryCreateServerEventSpam(); - item.CreateStatusEvent(); + item.CreateStatusEvent(loadingRound: false); } foreach (Structure wall in Structure.WallList) { @@ -2497,6 +2539,17 @@ namespace Barotrauma #endif } + public static void ServerRead(IReadMessage inc, Client sender) + { + string consoleCommand = inc.ReadString(); + float cursorX = inc.ReadSingle(); + float cursorY = inc.ReadSingle(); + + if (rateLimiter.IsLimitReached(sender)) { return; } + + ExecuteClientCommand(sender, new Vector2(cursorX, cursorY), consoleCommand); + } + public static void ExecuteClientCommand(Client client, Vector2 cursorWorldPos, string command) { if (GameMain.Server == null) return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index c4a043bbd..353ef08d8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -41,7 +41,7 @@ namespace Barotrauma clientsToRemove.Add(k); } } - if (!(clientsToRemove is null)) + if (clientsToRemove is not null) { foreach (var k in clientsToRemove) { @@ -62,7 +62,7 @@ namespace Barotrauma { foreach (Entity e in targets) { - if (!(e is Character character) || !character.IsRemotePlayer) { continue; } + if (e is not Character character || !character.IsRemotePlayer) { continue; } Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); if (targetClient != null) { @@ -85,7 +85,7 @@ namespace Barotrauma IEnumerable entities = ParentEvent.GetTargets(TargetTag); foreach (Entity e in entities) { - if (!(e is Character character) || !character.IsRemotePlayer) { continue; } + if (e is not Character character || !character.IsRemotePlayer) { continue; } Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); if (targetClient != null) { @@ -149,5 +149,15 @@ namespace Barotrauma } GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); } + + public void ServerWriteSelectedOption(Client client) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.CONVERSATION_SELECTED_OPTION); + outmsg.WriteUInt16(Identifier); + outmsg.WriteByte((byte)(selectedOption + 1)); + GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs new file mode 100644 index 000000000..5369ec7e1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs @@ -0,0 +1,43 @@ +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma +{ + partial class MissionAction : EventAction + { + private static readonly HashSet missionsUnlockedThisRound = new HashSet(); + + public static void ResetMissionsUnlockedThisRound() + { + missionsUnlockedThisRound.Clear(); + } + + public static void NotifyMissionsUnlockedThisRound(Client client) + { + foreach (Mission mission in missionsUnlockedThisRound) + { + NotifyMissionUnlock(mission, client); + } + } + + private static void NotifyMissionUnlock(Mission mission) + { + foreach (Client client in GameMain.Server.ConnectedClients) + { + NotifyMissionUnlock(mission, client); + } + } + + private static void NotifyMissionUnlock(Mission mission, Client client) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION); + outmsg.WriteIdentifier(mission.Prefab.Identifier); + outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(mission.Locations[0]) ?? -1); + outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(mission.Locations[1]) ?? -1); + outmsg.WriteString(mission.Name.Value); + GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 5d9dde87c..62b7f9882 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -14,12 +14,12 @@ namespace Barotrauma foreach (Event ev in activeEvents) { - if (!(ev is ScriptedEvent scriptedEvent)) { continue; } + if (ev is not ScriptedEvent scriptedEvent) { continue; } var actions = FindActions(scriptedEvent); foreach (EventAction action in actions.Select(a => a.Item2)) { - if (!(action is ConversationAction convAction) || convAction.Identifier != actionId) { continue; } + if (action is not ConversationAction convAction || convAction.Identifier != actionId) { continue; } if (!convAction.TargetClients.Contains(sender)) { #if DEBUG || UNSTABLE @@ -42,6 +42,14 @@ namespace Barotrauma else { convAction.SelectedOption = selectedOption; + if (convAction.Options.Any() && !convAction.GetEndingOptions().Contains(selectedOption)) + { + foreach (Client c in convAction.TargetClients) + { + if (c == sender) { continue; } + convAction.ServerWriteSelectedOption(c); + } + } } } return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EndMission.cs new file mode 100644 index 000000000..6284dd232 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EndMission.cs @@ -0,0 +1,19 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class EndMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + base.ServerWriteInitial(msg, c); + + boss.WriteSpawnData(msg, boss.ID, restrictMessageSize: false); + msg.WriteByte((byte)minions.Length); + foreach (Character minion in minions) + { + minion.WriteSpawnData(msg, minion.ID, restrictMessageSize: false); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs index 65a5653e8..55e94e312 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using System.Linq; namespace Barotrauma { @@ -16,11 +17,10 @@ namespace Barotrauma foreach (var kvp in spawnedResources) { msg.WriteByte((byte)kvp.Value.Count); - var rotation = resourceClusters[kvp.Key].Rotation; - msg.WriteSingle(rotation); - foreach (var r in kvp.Value) + msg.WriteSingle(kvp.Value.FirstOrDefault()?.Rotation ?? 0.0f); + foreach (var item in kvp.Value) { - r.WriteSpawnData(msg, r.ID, Entity.NullEntityID, 0, -1); + item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0, -1); } } @@ -28,9 +28,9 @@ namespace Barotrauma { msg.WriteIdentifier(kvp.Key); msg.WriteByte((byte)kvp.Value.Length); - foreach (var i in kvp.Value) + foreach (var item in kvp.Value) { - msg.WriteUInt16(i.ID); + msg.WriteUInt16(item.ID); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 52a944a4e..9653e372e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -6,33 +6,66 @@ namespace Barotrauma { partial class SalvageMission : Mission { - private bool usedExistingItem; + struct SpawnInfo + { + public readonly bool UsedExistingItem; + public readonly UInt16 OriginalInventoryID; + public readonly byte OriginalItemContainerIndex; + public readonly int OriginalSlotIndex; + public readonly List<(int listIndex, int effectIndex)> ExecutedEffectIndices; - private UInt16 originalInventoryID; - private byte originalItemContainerIndex; - private int originalSlotIndex; + public SpawnInfo(bool usedExistingItem, UInt16 originalInventoryID, byte originalItemContainerIndex, int originalSlotIndex, List<(int listIndex, int effectIndex)> executedEffectIndices) + { + UsedExistingItem = usedExistingItem; + OriginalInventoryID = originalInventoryID; + OriginalItemContainerIndex = originalItemContainerIndex; + OriginalSlotIndex = originalSlotIndex; + ExecutedEffectIndices = executedEffectIndices; + } + } - private readonly List> executedEffectIndices = new List>(); + private readonly Dictionary spawnInfo = new Dictionary(); public override void ServerWriteInitial(IWriteMessage msg, Client c) { base.ServerWriteInitial(msg, c); - msg.WriteBoolean(usedExistingItem); - if (usedExistingItem) + foreach (var target in targets) { - msg.WriteUInt16(item.ID); - } - else - { - item.WriteSpawnData(msg, item.ID, originalInventoryID, originalItemContainerIndex, originalSlotIndex); - } + bool targetFound = spawnInfo.ContainsKey(target) && target.Item != null; + msg.WriteBoolean(targetFound); + if (!targetFound) { continue; } - msg.WriteByte((byte)executedEffectIndices.Count); - foreach (Pair effectIndex in executedEffectIndices) + msg.WriteBoolean(spawnInfo[target].UsedExistingItem); + if (spawnInfo[target].UsedExistingItem) + { + msg.WriteUInt16(target.Item.ID); + } + else + { + target.Item.WriteSpawnData(msg, + target.Item.ID, + spawnInfo[target].OriginalInventoryID, + spawnInfo[target].OriginalItemContainerIndex, + spawnInfo[target].OriginalSlotIndex); + } + + msg.WriteByte((byte)spawnInfo[target].ExecutedEffectIndices.Count); + foreach ((int listIndex, int effectIndex) in spawnInfo[target].ExecutedEffectIndices) + { + msg.WriteByte((byte)listIndex); + msg.WriteByte((byte)effectIndex); + } + } + } + + public override void ServerWrite(IWriteMessage msg) + { + base.ServerWrite(msg); + msg.WriteByte((byte)targets.Count); + for (int i = 0; i < targets.Count; i++) { - msg.WriteByte((byte)effectIndex.First); - msg.WriteByte((byte)effectIndex.Second); + msg.WriteByte((byte)targets[i].State); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index ea3362d68..f41f62988 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -84,9 +84,6 @@ namespace Barotrauma Console.WriteLine("Loading game settings"); GameSettings.Init(); - Console.WriteLine("Loading MD5 hash cache"); - Md5Hash.Cache.Load(); - Console.WriteLine("Initializing SteamManager"); SteamManager.Initialize(); @@ -182,7 +179,7 @@ namespace Barotrauma for (int i = 0; i < CommandLineArgs.Length; i++) { - switch (CommandLineArgs[i].Trim()) + switch (CommandLineArgs[i].Trim().ToLowerInvariant()) { case "-name": name = CommandLineArgs[i + 1]; @@ -248,7 +245,7 @@ namespace Barotrauma for (int i = 0; i < CommandLineArgs.Length; i++) { - switch (CommandLineArgs[i].Trim()) + switch (CommandLineArgs[i].Trim().ToLowerInvariant()) { case "-playstyle": Enum.TryParse(CommandLineArgs[i + 1], out PlayStyle playStyle); @@ -270,6 +267,14 @@ namespace Barotrauma Server.ServerSettings.KarmaPreset = karmaPresetName; i++; break; + case "-language": + LanguageIdentifier language = CommandLineArgs[i + 1].ToLanguageIdentifier(); + if (ServerLanguageOptions.Options.Any(o => o.Identifier == language)) + { + Server.ServerSettings.Language = language; + } + i++; + break; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 82810135f..7629dd08f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Steam; namespace Barotrauma { @@ -85,6 +84,7 @@ namespace Barotrauma { if (purchasedHullRepairs == value) { return; } purchasedHullRepairs = value; + PurchasedHullRepairsInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -95,6 +95,7 @@ namespace Barotrauma { if (purchasedLostShuttles == value) { return; } purchasedLostShuttles = value; + PurchasedLostShuttlesInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -105,6 +106,7 @@ namespace Barotrauma { if (purchasedItemRepairs == value) { return; } purchasedItemRepairs = value; + PurchasedItemRepairsInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -337,11 +339,12 @@ namespace Barotrauma IsFirstRound = true; break; case TransitionType.ProgressToNextEmptyLocation: + Map.Visit(Map.CurrentLocation); TotalPassedLevels++; break; } - Map.ProgressWorld(transitionType, GameMain.GameSession.RoundDuration); + Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration); bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); if (success) @@ -391,17 +394,14 @@ namespace Barotrauma NextLevel = newLevel; MirrorLevel = mirror; - //give clients time to play the end cinematic before starting the next round - if (transitionType == TransitionType.End) - { - yield return new WaitForSeconds(EndCinematicDuration); - } - else - { - yield return new WaitForSeconds(EndTransitionDuration * 0.5f); - } - GameMain.Server.TryStartGame(); + yield return new WaitForSeconds(EndTransitionDuration * 0.5f); + + //don't start the next round automatically if we just finished the campaign + if (transitionType != TransitionType.End) + { + GameMain.Server.TryStartGame(); + } yield return CoroutineStatus.Success; } @@ -424,7 +424,7 @@ namespace Barotrauma } public bool CanPurchaseSub(SubmarineInfo info, Client client) - => CanAfford(info.Price, client) && GetCampaignSubs().Contains(info); + => CanAfford(info.GetPrice(), client) && GetCampaignSubs().Contains(info); private readonly List discardedCharacters = new List(); public void DiscardClientCharacterData(Client client) @@ -493,7 +493,8 @@ namespace Barotrauma if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) { var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); - if (transitionType == TransitionType.End) + if (transitionType == TransitionType.End || + (Level.Loaded.IsEndBiome && transitionType == TransitionType.ProgressToNextLocation)) { LoadNewLevel(); } @@ -509,6 +510,14 @@ namespace Barotrauma } } } + else if (Level.Loaded.IsEndBiome) + { + var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); + if (transitionType == TransitionType.ProgressToNextLocation) + { + LoadNewLevel(); + } + } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) { KeepCharactersCloseToOutpost(deltaTime); @@ -704,10 +713,6 @@ namespace Barotrauma if (requiredFlags.HasFlag(NetFlags.Reputation)) { msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.Reputation)); - Reputation reputation = Map?.CurrentLocation?.Reputation; - msg.WriteBoolean(reputation != null); - if (reputation != null) { msg.WriteSingle(reputation.Value); } - // hopefully we'll never have more than 128 factions msg.WriteByte((byte)Factions.Count); foreach (Faction faction in Factions) @@ -823,54 +828,37 @@ namespace Barotrauma Bank.ForceUpdate(); } - if (purchasedHullRepairs != PurchasedHullRepairs) + if (purchasedHullRepairs && !PurchasedHullRepairs) { - switch (purchasedHullRepairs) + if (GetBalance(sender) >= hullRepairCost) { - case true when GetBalance(sender) >= hullRepairCost: - TryPurchase(sender, hullRepairCost); - PurchasedHullRepairs = true; - GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); - break; - case false: - PurchasedHullRepairs = false; - personalWallet.Refund(hullRepairCost); - break; + TryPurchase(sender, hullRepairCost); + PurchasedHullRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); } } - if (purchasedItemRepairs != PurchasedItemRepairs) + if (purchasedItemRepairs && !PurchasedItemRepairs) { - switch (purchasedItemRepairs) + if (GetBalance(sender) >= itemRepairCost) { - case true when GetBalance(sender) >= itemRepairCost: - TryPurchase(sender, itemRepairCost); - PurchasedItemRepairs = true; - GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); - break; - case false: - PurchasedItemRepairs = false; - personalWallet.Refund(itemRepairCost); - break; + TryPurchase(sender, itemRepairCost); + PurchasedItemRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); } } - if (purchasedLostShuttles != PurchasedLostShuttles) + if (purchasedLostShuttles && !PurchasedLostShuttles) { if (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); } - else if (purchasedLostShuttles && TryPurchase(sender, shuttleRetrieveCost)) + else if (TryPurchase(sender, shuttleRetrieveCost)) { PurchasedLostShuttles = true; GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); } - else if (!purchasedItemRepairs) - { - PurchasedLostShuttles = false; - personalWallet.Refund(shuttleRetrieveCost); - } } if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) @@ -1021,12 +1009,13 @@ namespace Barotrauma bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character); } + var characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both); foreach (var (prefab, category, _) in purchasedUpgrades) { UpgradeManager.PurchaseUpgrade(prefab, category, client: sender); // unstable logging - int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList); int level = UpgradeManager.GetUpgradeLevel(prefab, category); GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); } @@ -1057,49 +1046,47 @@ namespace Barotrauma if (GameMain.Server is null) { return; } - switch (transfer.Sender) + if (transfer.Sender.TryUnwrap(out var id)) { - case Some { Value: var id }: - if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; } + if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; } - Wallet wallet = GetWalletByID(id); - if (wallet is InvalidWallet) { return; } + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } - TransferMoney(wallet); - break; - case None _: - if (!AllowedToManageWallets(sender)) + TransferMoney(wallet); + } + else + { + if (!AllowedToManageWallets(sender)) + { + if (transfer.Receiver.TryUnwrap(out var receiverId) && receiverId == sender.CharacterID) { - if (transfer.Receiver is Some { Value: var receiverId } && receiverId == sender.CharacterID) - { - if (transfer.Amount > GameMain.Server.ServerSettings.MaximumMoneyTransferRequest) { return; } - GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender); - GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money); - } - return; + if (transfer.Amount > GameMain.Server.ServerSettings.MaximumMoneyTransferRequest) { return; } + GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender); + GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money); } + return; + } - TransferMoney(Bank); - break; + TransferMoney(Bank); } void TransferMoney(Wallet from) { if (!from.TryDeduct(transfer.Amount)) { return; } - switch (transfer.Receiver) + if (transfer.Receiver.TryUnwrap(out var id)) { - case Some { Value: var id }: - Wallet wallet = GetWalletByID(id); - if (wallet is InvalidWallet) { return; } + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } - wallet.Give(transfer.Amount); - GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); - break; - case None _: - Bank.Give(transfer.Amount); - GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); - break; + wallet.Give(transfer.Amount); + GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); + } + else + { + Bank.Give(transfer.Amount); + GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); } } @@ -1322,6 +1309,10 @@ namespace Barotrauma public override bool TryPurchase(Client client, int price) { + //disconnected clients can never purchase anything + //(can happen e.g. if someone starts a vote to buy something and then disconnects) + if (client != null && !GameMain.Server.ConnectedClients.Contains(client)) { return false; } + Wallet wallet = GetWallet(client); if (!AllowedToManageWallets(client)) { @@ -1371,6 +1362,12 @@ namespace Barotrauma modeElement.Add(Settings.Save()); modeElement.Add(SaveStats()); modeElement.Add(Bank.Save()); + + if (GameMain.GameSession?.EventManager != null) + { + modeElement.Add(GameMain.GameSession?.EventManager.Save()); + } + CampaignMetadata?.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index e46134ca6..d2a706459 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -11,20 +11,15 @@ namespace Barotrauma { internal partial class MedicalClinic { - private enum RateLimitResult - { - OK, - LimitReached - } + // allow 10 requests per 5 seconds, announce to chat if the limit is reached + private readonly RateLimiter rateLimiter = new( + maxRequests: 10, + expiryInSeconds: 5, + punishmentRules: (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce)); - private struct RateLimitInfo - { - public int Requests; - public const int MaxRequests = 10; - public DateTimeOffset Expiry; - } + private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry); - private readonly Dictionary rateLimits = new Dictionary(); + private readonly List afflictionSubscribers = new(); public void ServerRead(IReadMessage inc, Client sender) { @@ -35,6 +30,9 @@ namespace Barotrauma case NetworkHeader.ADD_EVERYTHING_TO_PENDING: ProcessAddEverything(sender); break; + case NetworkHeader.UNSUBSCRIBE_ME: + RemoveClientSubscription(sender); + break; case NetworkHeader.REQUEST_AFFLICTIONS: ProcessRequestedAfflictions(inc, sender); break; @@ -58,7 +56,7 @@ namespace Barotrauma private void ProcessNewAddition(IReadMessage inc, Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } NetCrewMember newCrewMember = INetSerializableStruct.Read(inc); InsertPendingCrewMember(newCrewMember); @@ -67,14 +65,25 @@ namespace Barotrauma private void ProcessAddEverything(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } AddEverythingToPending(); ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); } + private void RemoveClientSubscription(Client client) + { + foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) + { + if (sub.Subscriber == client || sub.Expiry < DateTimeOffset.Now) + { + afflictionSubscribers.Remove(sub); + } + } + } + private void ProcessNewRemoval(IReadMessage inc, Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } NetRemovedAffliction removed = INetSerializableStruct.Read(inc); RemovePendingAffliction(removed.CrewMember, removed.Affliction); @@ -83,14 +92,14 @@ namespace Barotrauma private void ProcessRequestedPending(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); } private void ProcessHealing(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } HealRequestResult result = HealAllPending(client: client); ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client); @@ -98,7 +107,7 @@ namespace Barotrauma private void ProcessClearing(Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } if (!PendingHeals.Any()) { return; } @@ -108,7 +117,7 @@ namespace Barotrauma private void ProcessRequestedAfflictions(IReadMessage inc, Client client) { - if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + if (rateLimiter.IsLimitReached(client)) { return; } NetCrewMember crewMember = INetSerializableStruct.Read(inc); @@ -129,35 +138,17 @@ namespace Barotrauma Afflictions = pendingAfflictions }; + if (foundInfo is not null) + { + RemoveClientSubscription(client); + + // the client subscribes to the afflictions of the crew member for the next minute + afflictionSubscribers.Add(new AfflictionSubscriber(client, foundInfo, DateTimeOffset.Now.AddMinutes(1))); + } + ServerSend(writeCrewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable, client); } - private RateLimitResult CheckRateLimit(Client client) - { - if (rateLimits.TryGetValue(client, out RateLimitInfo rateLimitInfo)) - { - if (rateLimitInfo.Expiry < DateTimeOffset.Now) - { - rateLimitInfo.Expiry = DateTimeOffset.Now.AddSeconds(5); - rateLimitInfo.Requests = 1; - } - else - { - if (rateLimitInfo.Requests > RateLimitInfo.MaxRequests) { return RateLimitResult.LimitReached; } - - rateLimitInfo.Requests++; - } - - rateLimits[client] = rateLimitInfo; - } - else - { - rateLimits.Add(client, new RateLimitInfo { Requests = 1, Expiry = DateTimeOffset.Now.AddSeconds(5) }); - } - - return RateLimitResult.OK; - } - private IWriteMessage StartSending() { IWriteMessage msg = new WriteOnlyMessage(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index de8517914..188721aa1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -8,10 +8,12 @@ namespace Barotrauma.Items.Components private readonly struct EventData : IEventData { public readonly bool Launch; + public readonly byte SpreadCounter; - public EventData(bool launch) + public EventData(bool launch, byte spreadCounter = 0) { Launch = launch; + SpreadCounter = spreadCounter; } } @@ -32,6 +34,7 @@ namespace Barotrauma.Items.Components msg.WriteSingle(launchPos.X); msg.WriteSingle(launchPos.Y); msg.WriteSingle(launchRot); + msg.WriteByte(eventData.SpreadCounter); } bool stuck = StickTarget != null && !item.Removed && !StickTargetRemoved(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index 7ba476df9..9403732da 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -185,7 +185,7 @@ namespace Barotrauma.Items.Components //already connected, no need to do anything if (Connections[i].Wires.Contains(newWire)) { continue; } - newWire.Connect(Connections[i], true, true); + newWire.TryConnect(Connections[i], true, true); Connections[i].TryAddLink(newWire); var otherConnection = newWire.OtherConnection(Connections[i]); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index 8beb42942..fd03a0bfd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components { string newOutputValue = msg.ReadString(); - if (item.CanClientAccess(c)) + if (item.CanClientAccess(c) && !Readonly) { if (newOutputValue.Length > MaxMessageLength) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 65b176cff..50e6146bb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -4,6 +4,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -135,6 +136,8 @@ namespace Barotrauma } } + EnsureItemsInBothHands(c.Character); + CreateNetworkEvent(); foreach (Inventory prevInventory in prevItemInventories.Distinct()) { @@ -174,6 +177,29 @@ namespace Barotrauma } } + private void EnsureItemsInBothHands(Character character) + { + if (this is not CharacterInventory charInv) { return; } + + int leftHandSlot = charInv.FindLimbSlot(InvSlotType.LeftHand), + rightHandSlot = charInv.FindLimbSlot(InvSlotType.RightHand); + + TryPutInOppositeHandSlot(rightHandSlot, leftHandSlot); + TryPutInOppositeHandSlot(leftHandSlot, rightHandSlot); + + void TryPutInOppositeHandSlot(int originalSlot, int otherHandSlot) + { + const InvSlotType bothHandSlot = InvSlotType.LeftHand | InvSlotType.RightHand; + + foreach (Item it in slots[originalSlot].Items) + { + if (it.AllowedSlots.None(static s => s.HasFlag(bothHandSlot)) || slots[otherHandSlot].Contains(it)) { continue; } + + TryPutItem(it, otherHandSlot, true, true, character, false); + } + } + } + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { SharedWrite(msg, extraData); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 3508a03ae..a48f13895 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -65,7 +65,8 @@ namespace Barotrauma msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); itemContainer.Inventory.ServerEventWrite(msg, c); break; - case ItemStatusEventData _: + case ItemStatusEventData statusEvent: + msg.WriteBoolean(statusEvent.LoadingRound); msg.WriteSingle(condition); break; case AssignCampaignInteractionEventData _: @@ -253,6 +254,7 @@ namespace Barotrauma msg.WriteBoolean(hasIdCard); if (hasIdCard) { + msg.WriteInt32(idCardComponent.SubmarineSpecificID); msg.WriteString(idCardComponent.OwnerName); msg.WriteString(idCardComponent.OwnerTags); msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex+1)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index c2a1f8cc6..8742d17eb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -140,8 +140,7 @@ namespace Barotrauma.Networking return Option.Some(new BannedPlayer(name, addressOrAccountId, reason, expirationTime)); } - bannedPlayers.AddRange(doc.Root.Elements().Select(loadFromElement) - .OfType>().Select(o => o.Value)); + bannedPlayers.AddRange(doc.Root.Elements().Select(loadFromElement).NotNone()); } private void RemoveExpired() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index c285765e3..e9216ab7b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -102,9 +102,9 @@ namespace Barotrauma.Networking similarity *= 0.25f; } - bool isOwner = GameMain.Server.OwnerConnection != null && c.Connection == GameMain.Server.OwnerConnection; + bool isSpamExempt = RateLimiter.IsExempt(c); - if (similarity + c.ChatSpamSpeed > 5.0f && !isOwner) + if (similarity + c.ChatSpamSpeed > 5.0f && !isSpamExempt) { GameMain.Server.KarmaManager.OnSpamFilterTriggered(c); @@ -125,7 +125,7 @@ namespace Barotrauma.Networking c.ChatSpamSpeed += similarity + 0.5f; - if (c.ChatSpamTimer > 0.0f && !isOwner) + if (c.ChatSpamTimer > 0.0f && !isSpamExempt) { ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); c.ChatSpamTimer = 10.0f; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 8c9a90c95..cb1f3d105 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -303,7 +303,7 @@ namespace Barotrauma.Networking } else { - var defaultPerms = PermissionPreset.List.Find(p => p.Name == "None"); + var defaultPerms = PermissionPreset.List.Find(p => p.Identifier == "None"); if (defaultPerms != null) { newClient.SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands); @@ -332,9 +332,8 @@ namespace Barotrauma.Networking public void Update(float deltaTime) { -#if CLIENT - if (ShowNetStats) { netStats.Update(deltaTime); } -#endif + dosProtection.Update(deltaTime); + if (!started) { return; } if (ChildServerRelay.HasShutDown) @@ -387,10 +386,10 @@ namespace Barotrauma.Networking Voting.Update(deltaTime); bool isCrewDead = - connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated); + connectedClients.All(c => !c.UsingFreeCam && (c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated)); bool subAtLevelEnd = false; - if (Submarine.MainSub != null && !(GameMain.GameSession.GameMode is PvPMode)) + if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode) { if (Level.Loaded?.EndOutpost != null) { @@ -692,10 +691,14 @@ namespace Barotrauma.Networking } } + private readonly DoSProtection dosProtection = new(); + private void ReadDataMessage(NetworkConnection sender, IReadMessage inc) { var connectedClient = connectedClients.Find(c => c.Connection == sender); + using var _ = dosProtection.Start(connectedClient); + ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte(); switch (header) { @@ -784,7 +787,7 @@ namespace Barotrauma.Networking if (GameStarted) { SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); - return; + break; } if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) { @@ -1085,7 +1088,7 @@ namespace Barotrauma.Networking ChatMessage.ServerRead(inc, c); break; case ClientNetSegment.Vote: - Voting.ServerRead(inc, c); + Voting.ServerRead(inc, c, dosProtection); break; default: return SegmentTableReader.BreakSegmentReading.Yes; @@ -1116,6 +1119,7 @@ namespace Barotrauma.Networking { //check if midround syncing is needed due to missed unique events if (!midroundSyncingDone) { entityEventManager.InitClientMidRoundSync(c); } + MissionAction.NotifyMissionsUnlockedThisRound(c); c.InGame = true; } } @@ -1245,7 +1249,7 @@ namespace Barotrauma.Networking entityEventManager.Read(inc, c); break; case ClientNetSegment.Vote: - Voting.ServerRead(inc, c); + Voting.ServerRead(inc, c, dosProtection); break; case ClientNetSegment.SpectatingPos: c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); @@ -1405,19 +1409,23 @@ namespace Barotrauma.Networking bool quitCampaign = inc.ReadBoolean(); if (GameStarted) { - Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage); - if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) + using (dosProtection.Pause(sender)) { - mpCampaign.SavePlayers(); - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - mpCampaign.UpdateStoreStock(); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage); + if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save) + { + mpCampaign.SavePlayers(); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + mpCampaign.UpdateStoreStock(); + GameMain.GameSession?.EventManager?.RegisterEventHistory(registerFinishedOnly: true); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + } + else + { + save = false; + } + EndGame(wasSaved: save); } - else - { - save = false; - } - EndGame(wasSaved: save); } else if (mpCampaign != null) { @@ -1441,45 +1449,54 @@ namespace Barotrauma.Networking } else if (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + using (dosProtection.Pause(sender)) + { + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + } } } else if (!GameStarted && !initiatedStartGame) { - Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); - TryStartGame(); + using (dosProtection.Pause(sender)) + { + Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); + TryStartGame(); + } } else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))) { - var availableTransition = mpCampaign.GetAvailableTransition(out _, out _); - //don't force location if we've teleported - bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation; - switch (availableTransition) + using (dosProtection.Pause(sender)) { - case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: - if (forceLocation) - { - mpCampaign.Map.SelectLocation( - mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation)); - } - mpCampaign.LoadNewLevel(); - break; - case CampaignMode.TransitionType.ProgressToNextEmptyLocation: - if (forceLocation) - { - mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation)); - } - mpCampaign.LoadNewLevel(); - break; - case CampaignMode.TransitionType.None: -#if DEBUG || UNSTABLE - DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available."); -#endif - return; - default: - Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); - mpCampaign.LoadNewLevel(); - break; + var availableTransition = mpCampaign.GetAvailableTransition(out _, out _); + //don't force location if we've teleported + bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation; + switch (availableTransition) + { + case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: + if (forceLocation) + { + mpCampaign.Map.SelectLocation( + mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation)); + } + mpCampaign.LoadNewLevel(); + break; + case CampaignMode.TransitionType.ProgressToNextEmptyLocation: + if (forceLocation) + { + mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation)); + } + mpCampaign.LoadNewLevel(); + break; + case CampaignMode.TransitionType.None: + #if DEBUG || UNSTABLE + DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available."); + #endif + break; + default: + Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); + mpCampaign.LoadNewLevel(); + break; + } } } } @@ -1519,11 +1536,7 @@ namespace Barotrauma.Networking mpCampaign?.ServerRead(inc, sender); break; case ClientPermissions.ConsoleCommands: - { - string consoleCommand = inc.ReadString(); - Vector2 clientCursorPos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); - DebugConsole.ExecuteClientCommand(sender, clientCursorPos, consoleCommand); - } + DebugConsole.ServerRead(inc, sender); break; case ClientPermissions.ManagePermissions: byte targetClientID = inc.ReadByte(); @@ -2366,10 +2379,7 @@ namespace Barotrauma.Networking List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[n]).ToList(); - if (Level.Loaded?.StartOutpost != null && - Level.Loaded.Type == LevelData.LevelType.Outpost && - (Level.Loaded.StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) && - Level.Loaded.StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) + if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost()) { spawnWaypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && @@ -2444,7 +2454,6 @@ namespace Barotrauma.Networking spawnedCharacter.Info.InventoryData = new XElement("inventory"); spawnedCharacter.Info.StartItemsGiven = true; spawnedCharacter.SaveInventory(); - // talents are only avilable for players in online sessions, but modders or someone else might want to have them loaded anyway spawnedCharacter.LoadTalents(); } } @@ -2979,7 +2988,7 @@ namespace Barotrauma.Networking client.WaitForNextRoundRespawn = null; client.InGame = false; - if (client.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } + if (client.AccountId.TryUnwrap(out var steamId)) { SteamManager.StopAuthSession(steamId); } var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); if (previousPlayer == null) @@ -3313,12 +3322,13 @@ namespace Barotrauma.Networking if (checkActiveVote && Voting.ActiveVote != null) { +#warning TODO: this is mostly the same as Voting.Update, deduplicate (if/when refactoring the Voting class?) var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); - if (inGameClients.Count() == 1) + if (inGameClients.Count() == 1 && inGameClients.First() == Voting.ActiveVote.VoteStarter) { Voting.ActiveVote.Finish(Voting, passed: true); } - else + else if (inGameClients.Any()) { var eligibleClients = inGameClients.Where(c => c != Voting.ActiveVote.VoteStarter); int yes = eligibleClients.Count(c => c.GetVote(Voting.ActiveVote.VoteType) == 2); @@ -3388,12 +3398,11 @@ namespace Barotrauma.Networking public void SwitchSubmarine() { - if (!(Voting.ActiveVote is Voting.SubmarineVote subVote)) { return; } + if (Voting.ActiveVote is not Voting.SubmarineVote subVote) { return; } SubmarineInfo targetSubmarine = subVote.Sub; VoteType voteType = Voting.ActiveVote.VoteType; Client starter = Voting.ActiveVote.VoteStarter; - int deliveryFee = 0; switch (voteType) { @@ -3403,7 +3412,6 @@ namespace Barotrauma.Networking GameMain.GameSession.PurchaseSubmarine(targetSubmarine, starter); break; case VoteType.SwitchSub: - deliveryFee = subVote.DeliveryFee; break; default: return; @@ -3411,7 +3419,7 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { - GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, deliveryFee, starter); + GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, starter); } Voting.StopSubmarineVote(true); @@ -3592,15 +3600,28 @@ namespace Barotrauma.Networking } } + private readonly RateLimiter charInfoRateLimiter = new( + maxRequests: 5, + expiryInSeconds: 10, + punishmentRules: new[] + { + (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce), + (RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick) + }); + private void UpdateCharacterInfo(IReadMessage message, Client sender) { - sender.SpectateOnly = message.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); - if (sender.SpectateOnly) - { - return; - } + bool spectateOnly = message.ReadBoolean(); + message.ReadPadBits(); - string newName = message.ReadString(); + sender.SpectateOnly = spectateOnly && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); + if (sender.SpectateOnly) { return; } + + var netInfo = INetSerializableStruct.Read(message); + + if (charInfoRateLimiter.IsLimitReached(sender)) { return; } + + string newName = netInfo.NewName; if (string.IsNullOrEmpty(newName)) { newName = sender.Name; @@ -3618,42 +3639,31 @@ namespace Barotrauma.Networking } } - int tagCount = message.ReadByte(); - HashSet tagSet = new HashSet(); - for (int i = 0; i < tagCount; i++) - { - tagSet.Add(message.ReadIdentifier()); - } - int hairIndex = message.ReadByte(); - int beardIndex = message.ReadByte(); - int moustacheIndex = message.ReadByte(); - int faceAttachmentIndex = message.ReadByte(); - Color skinColor = message.ReadColorR8G8B8(); - Color hairColor = message.ReadColorR8G8B8(); - Color facialHairColor = message.ReadColorR8G8B8(); - - List jobPreferences = new List(); - int count = message.ReadByte(); - for (int i = 0; i < Math.Min(count, 3); i++) - { - string jobIdentifier = message.ReadString(); - int variant = message.ReadByte(); - if (JobPrefab.Prefabs.TryGet(jobIdentifier, out JobPrefab jobPrefab)) - { - if (jobPrefab.HiddenJob) { continue; } - jobPreferences.Add(new JobVariant(jobPrefab, variant)); - } - } - sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName); - sender.CharacterInfo.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); - sender.CharacterInfo.Head.SkinColor = skinColor; - sender.CharacterInfo.Head.HairColor = hairColor; - sender.CharacterInfo.Head.FacialHairColor = facialHairColor; - if (jobPreferences.Count > 0) + sender.CharacterInfo.RecreateHead( + tags: netInfo.Tags.ToImmutableHashSet(), + hairIndex: netInfo.HairIndex, + beardIndex: netInfo.BeardIndex, + moustacheIndex: netInfo.MoustacheIndex, + faceAttachmentIndex: netInfo.FaceAttachmentIndex); + + sender.CharacterInfo.Head.SkinColor = netInfo.SkinColor; + sender.CharacterInfo.Head.HairColor = netInfo.HairColor; + sender.CharacterInfo.Head.FacialHairColor = netInfo.FacialHairColor; + + if (netInfo.JobVariants.Length > 0) { - sender.JobPreferences = jobPreferences; + List variants = new List(); + foreach (NetJobVariant jv in netInfo.JobVariants) + { + if (jv.ToJobVariant() is { } variant) + { + variants.Add(variant); + } + } + + sender.JobPreferences = variants; } } @@ -3973,8 +3983,7 @@ namespace Barotrauma.Networking } public void Quit() - { - + { if (started) { started = false; @@ -3986,7 +3995,7 @@ namespace Barotrauma.Networking ServerSettings.SaveSettings(); - ModSender.Dispose(); + ModSender?.Dispose(); if (ServerSettings.SaveServerLogs) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index e732ce117..737e9555d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -158,7 +158,7 @@ namespace Barotrauma else if (client.Karma < 40.0f) herpesStrength = 30.0f; - var existingAffliction = client.Character.CharacterHealth.GetAffliction("spaceherpes"); + var existingAffliction = client.Character.CharacterHealth.GetAffliction(AfflictionPrefab.SpaceHerpesType); if (existingAffliction == null && herpesStrength > 0.0f) { client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength)); @@ -170,7 +170,7 @@ namespace Barotrauma existingAffliction.Strength = herpesStrength; if (herpesStrength <= 0.0f) { - client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs("invertcontrols".ToIdentifier(), 100.0f); + client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.InvertControlsType, 100.0f); } } @@ -358,8 +358,8 @@ namespace Barotrauma } } - bool targetIsHusk = target.CharacterHealth?.GetAffliction("huskinfection")?.State == AfflictionHusk.InfectionState.Active; - bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction("huskinfection")?.State == AfflictionHusk.InfectionState.Active; + bool targetIsHusk = target.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; + bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; //huskified characters count as enemies to healthy characters and vice versa if (targetIsHusk != attackerIsHusk) { isEnemy = true; } @@ -614,7 +614,7 @@ namespace Barotrauma if (amount < 0.0f) { - float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); var clientMemory = GetClientMemory(client); clientMemory.KarmaDecreasesInPastMinute.RemoveAll(ta => ta.Time + 60.0f < Timing.TotalTime); float aggregate = clientMemory.KarmaDecreasesInPastMinute.Select(ta => ta.Amount).DefaultIfEmpty().Aggregate((a, b) => a + b); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index b231a8672..b60d34661 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -298,7 +298,7 @@ namespace Barotrauma.Networking { if (netServer == null) { return; } - PendingClient? pendingClient = pendingClients.Find(c => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId); + PendingClient? pendingClient = pendingClients.Find(c => c.AccountInfo.AccountId.TryUnwrap(out var id) && id.Value == steamId); DebugConsole.Log($"{steamId} validation: {status}, {(pendingClient != null)}"); if (pendingClient is null) @@ -306,7 +306,7 @@ namespace Barotrauma.Networking if (status == Steamworks.AuthResponse.OK) { return; } if (connectedClients.Find(c - => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId) + => c.AccountInfo.AccountId.TryUnwrap(out var id) && id.Value == steamId) is LidgrenConnection connection) { Disconnect(connection, PeerDisconnectPacket.SteamAuthError(status)); @@ -380,7 +380,7 @@ namespace Barotrauma.Networking lidgrenConn.Status = NetworkConnectionStatus.Disconnected; connectedClients.Remove(lidgrenConn); callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket); - if (conn.AccountInfo.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } + if (conn.AccountInfo.AccountId.TryUnwrap(out var steamId)) { SteamManager.StopAuthSession(steamId); } } lidgrenConn.NetConnection.Disconnect(peerDisconnectPacket.ToLidgrenStringRepresentation()); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 4fd2c36a1..b8e4393bf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -71,7 +71,7 @@ namespace Barotrauma.Networking protected List connectedClients = null!; protected List pendingClients = null!; protected ServerSettings serverSettings = null!; - protected Option ownerKey = null!; + protected Option ownerKey = Option.None; protected NetworkConnection? OwnerConnection; protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc, ConnectionInitialization initializationStep) @@ -290,7 +290,7 @@ namespace Barotrauma.Networking pendingClients.Remove(pendingClient); - if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId is Some { Value: SteamId steamId }) + if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId.TryUnwrap(out var steamId)) { Steam.SteamManager.StopAuthSession(steamId); pendingClient.Connection.SetAccountInfo(AccountInfo.None); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 17d19a55f..bc268f9dc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -218,7 +218,10 @@ namespace Barotrauma.Networking foreach (Door door in shuttleDoors) { - if (door.IsOpen) door.TrySetState(false, false, true); + if (door.IsOpen) + { + door.TrySetState(open: false, isNetworkMessage: false, sendNetworkMessage: true); + } } var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == RespawnShuttle && g.ConnectedWall != null); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 7e348523a..55a116898 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Networking => LastUpdateIdForFlag.Keys .Where(k => IsFlagRequired(c, k)) .Aggregate(NetFlags.None, (f1, f2) => f1 | f2); - + partial void InitProjSpecific() { LoadSettings(); @@ -176,7 +176,11 @@ namespace Barotrauma.Networking netProperties[key].Read(incMsg); if (!netProperties[key].PropEquals(prevValue, netProperties[key])) { - GameServer.Log(GameServer.ClientLogName(c) + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage); + GameServer.Log( + NetworkMember.ClientLogName(c) + + $" changed {netProperties[key].Name}" + + $" to {netProperties[key].Value}", + ServerLog.MessageType.ServerMessage); } propertiesChanged = true; } @@ -330,6 +334,10 @@ namespace Barotrauma.Networking { LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } + if (string.IsNullOrEmpty(doc.Root.GetAttributeString("language", ""))) + { + Language = ServerLanguageOptions.PickLanguage(GameSettings.CurrentConfig.Language); + } AutoRestart = doc.Root.GetAttributeBool("autorestart", false); @@ -512,7 +520,7 @@ namespace Barotrauma.Networking else { string presetName = clientElement.GetAttributeString("preset", ""); - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name == presetName); + PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName == presetName); if (preset == null) { DebugConsole.ThrowError("Failed to restore saved permissions to the client \"" + clientName + "\". Permission preset \"" + presetName + "\" not found."); @@ -577,8 +585,7 @@ namespace Barotrauma.Networking foreach (SavedClientPermission clientPermission in ClientPermissions) { var matchingPreset = PermissionPreset.List.Find(p => p.MatchesPermissions(clientPermission.Permissions, clientPermission.PermittedCommands)); - #warning TODO: this is broken because of localization - if (matchingPreset != null && matchingPreset.Name == "None") + if (matchingPreset != null && matchingPreset.Identifier == "None") { continue; } @@ -592,7 +599,7 @@ namespace Barotrauma.Networking clientElement.Add(matchingPreset == null ? new XAttribute("permissions", clientPermission.Permissions.ToString()) - : new XAttribute("preset", matchingPreset.Name)); + : new XAttribute("preset", matchingPreset.DisplayName)); if (clientPermission.Permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands)) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 3eaee94aa..4af593e86 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -28,13 +28,11 @@ namespace Barotrauma public SubmarineInfo Sub; public bool TransferItems; - public int DeliveryFee; - public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, int deliveryFee, VoteType voteType) + public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, VoteType voteType) { Sub = subInfo; TransferItems = transferItems; - DeliveryFee = deliveryFee; VoteType = voteType; State = VoteState.Started; VoteStarter = starter; @@ -81,10 +79,10 @@ namespace Barotrauma if (passed) { Wallet fromWallet = From == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : From.Character?.Wallet; - if (fromWallet.TryDeduct(TransferAmount)) + if (fromWallet != null && fromWallet.TryDeduct(TransferAmount)) { Wallet toWallet = To == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : To.Character?.Wallet; - toWallet.Give(TransferAmount); + toWallet?.Give(TransferAmount); } } else @@ -109,7 +107,6 @@ namespace Barotrauma sender, subInfo, transferItems, - voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0, voteType); StartOrEnqueueVote(subVote); GameMain.Server.UpdateVoteStatus(checkActiveVote: false); @@ -206,12 +203,16 @@ namespace Barotrauma // Do not take unanswered into account for total int yes = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 2); int no = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 1); - int total = Math.Max(yes + no, 1); - - bool passed = - yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio || - inGameClients.Count() == 1; + int total = yes + no; + bool passed = false; + //total can be zero if the client who initiated the vote has left + if (total > 0) + { + passed = + yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio || + inGameClients.Count() == 1; + } ActiveVote.Finish(this, passed); } } @@ -224,7 +225,7 @@ namespace Barotrauma } } - public void ServerRead(IReadMessage inc, Client sender) + public void ServerRead(IReadMessage inc, Client sender, DoSProtection dosProtection) { if (GameMain.Server == null || sender == null) { return; } @@ -336,7 +337,10 @@ namespace Barotrauma inc.ReadPadBits(); - GameMain.Server.UpdateVoteStatus(); + using (dosProtection.Pause(sender)) + { + GameMain.Server.UpdateVoteStatus(); + } } public void ServerWrite(IWriteMessage msg) @@ -436,7 +440,6 @@ namespace Barotrauma var subVote = ActiveVote as SubmarineVote; msg.WriteString(subVote.Sub.Name); msg.WriteBoolean(subVote.TransferItems); - msg.WriteInt16((short)subVote.DeliveryFee); break; } break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 993323acb..2a127c1ba 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -158,7 +158,10 @@ namespace Barotrauma sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); if (ContentPackageManager.EnabledPackages.All != null) { - sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); + sb.AppendLine("Selected content packages: " + + (!ContentPackageManager.EnabledPackages.All.Any() ? + "None" : + string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => $"{c.Name} ({c.Hash?.ShortRepresentation ?? "unknown"})")))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index eb1a00ee9..ff3f08bbe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -204,20 +204,24 @@ namespace Barotrauma public void RandomizeSettings() { - if (GameMain.Server.ServerSettings.RandomizeSeed) LevelSeed = ToolBox.RandomSeed(8); + if (GameMain.Server.ServerSettings.RandomizeSeed) { LevelSeed = ToolBox.RandomSeed(8); } - if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random) + //don't touch any of these settings if a campaign is running! + if (GameMain.GameSession?.Campaign == null) { - var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus) && c.IsPlayer).ToList(); - SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)]; - } - if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random) - { - var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign); - SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier; - } + if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random) + { + var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus) && c.IsPlayer).ToList(); + SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)]; + } + if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random) + { + var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign); + SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier; + } - GameMain.Server.ServerSettings.SelectNonHiddenSubmarine(); + GameMain.Server.ServerSettings.SelectNonHiddenSubmarine(); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index 18e6c142d..56b57530c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -5,12 +5,13 @@ namespace Barotrauma.Steam { partial class SteamManager { - private static void InitializeProjectSpecific() { IsInitialized = true; } + private static void InitializeProjectSpecific() { } + + private static bool IsInitializedProjectSpecific + => Steamworks.SteamServer.IsValid; public static bool CreateServer(Networking.GameServer server, bool isPublic) { - IsInitialized = true; - Steamworks.SteamServerInit options = new Steamworks.SteamServerInit("Barotrauma", "Barotrauma") { GamePort = (ushort)server.Port, @@ -56,10 +57,15 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText); Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString()); Steamworks.SteamServer.SetKey("playercount", server.ConnectedClients.Count.ToString()); - Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); - Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); - Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp - => cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : ""))); + int index = 0; + foreach (var contentPackage in contentPackages) + { + string ugcIdStr = contentPackage.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : string.Empty; + Steamworks.SteamServer.SetKey( + $"contentpackage{index}", + contentPackage.Name+","+ contentPackage.Hash.StringRepresentation + "," + ugcIdStr); + index++; + } Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); @@ -71,6 +77,7 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString()); Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier.Value); Steamworks.SteamServer.SetKey("playstyle", server.ServerSettings.PlayStyle.ToString()); + Steamworks.SteamServer.SetKey("language", server.ServerSettings.Language.ToString()); Steamworks.SteamServer.DedicatedServer = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs b/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs new file mode 100644 index 000000000..faf50c98a --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs @@ -0,0 +1,232 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal sealed class DoSProtection + { + /// + /// A struct that executes an action when it's created and another one when it's disposed. + /// + public readonly ref struct DoSAction + { + private readonly Client sender; + private readonly Action end; + + public DoSAction(Client sender, Action start, Action end) + { + this.sender = sender; + this.end = end; + start(sender); + } + + public void Dispose() + { + end(sender); + } + } + + private sealed class OffenseData + { + /// + /// Timer that keeps track of how long it takes to process a packet. + /// + public readonly Stopwatch Stopwatch = new(); + + /// + /// Amount of strikes the client has received for causing the server to slow down. + /// + public int Strikes; + + /// + /// How many packets have been sent in the last minute. + /// + public int PacketCount; + + /// + /// Resets the strikes and packet count. + /// + public void ResetStrikes() + { + Strikes = 0; + PacketCount = 0; + } + + /// + /// Resets the timer. + /// + public void ResetTimer() => Stopwatch.Reset(); + } + + private readonly Dictionary clients = new(); + + private float stopwatchResetTimer, + strikesResetTimer; + + private const int StopwatchResetInterval = 1, + StrikesResetInterval = 60, + StrikeThreshold = 6; + + /// + /// Called when the server receives a packet to start logging how much time it takes to process. + /// + /// The client to start a timer for. + /// Nothing useful. Required for the "using" keyword. + /// + /// Calling stop is not required, the timer will be stopped automatically when the function it was started in returns. + /// + /// + /// + /// public void ServerRead(IReadMessage msg, Client c) + /// { + /// // start the timer + /// using var _ = dosProtection.Start(connectedClient); + /// + /// if (condition) + /// { + /// // the timer will be stopped here. + /// return; + /// } + /// + /// ProcessMessage(msg); + /// // the timer will be stopped here. + /// } + /// + /// + public DoSAction Start(Client client) => new DoSAction(client, StartFor, EndFor); + + /// + /// Temporary pauses the timer for the client. + /// Used when we know a packet is going to slow down the server but we don't want to count it as a strike. + /// For example when a client is starting a round. + /// + /// The client to pause the timer for. + /// Nothing useful. Required for the "using" keyword. + /// + /// Calling resume is not required, the timer will be resumed automatically when the using block ends. + /// + /// + /// + /// using (dos.Pause(client)) + /// { + /// // do something that will slow down the server + /// } + /// // the timer will be resumed here + /// + /// + public DoSAction Pause(Client client) => new DoSAction(client, PauseFor, ResumeFor); + + private void StartFor(Client client) + { + if (!clients.ContainsKey(client)) + { + clients.Add(client, new OffenseData()); + } + + clients[client].Stopwatch.Start(); + } + + private void EndFor(Client client) + { + if (GetData(client) is not { } data) { return; } + + data.PacketCount++; + data.Stopwatch.Stop(); + UpdateOffense(client, data); + } + + // stops the clock but doesn't update offenses + private void PauseFor(Client client) => GetData(client)?.Stopwatch.Stop(); + + private void ResumeFor(Client client) => GetData(client)?.Stopwatch.Start(); + + private void UpdateOffense(Client client, OffenseData data) + { + if (GameMain.Server?.ServerSettings is not { } settings) { return; } + + // client is sending too many packets, kick them + if (data.PacketCount > settings.MaxPacketAmount && settings.MaxPacketAmount > ServerSettings.PacketLimitMin) + { + AttemptKickClient(client, TextManager.Get("PacketLimitKicked")); + clients.Remove(client); + return; + } + + // if the stopwatch has been running for an entire second without the Update() method resetting it (which it does every second) then something is wrong + if (data.Stopwatch.ElapsedMilliseconds < 100) { return; } + + data.Strikes++; + data.ResetTimer(); + + GameServer.Log($"{NetworkMember.ClientLogName(client)} is causing the server to slow down.", ServerLog.MessageType.DoSProtection); + + // too many strikes, get them out of here + if (data.Strikes < StrikeThreshold) { return; } + + if (settings.EnableDoSProtection) + { + AttemptKickClient(client, TextManager.Get("DoSProtectionKicked")); + } + + clients.Remove(client); + + static void AttemptKickClient(Client client, LocalizedString reason) + { + // ReSharper disable once ConvertToConstant.Local + bool doesRateLimitAffectClient = +#if DEBUG + true; // for testing +#else + !RateLimiter.IsExempt(client); +#endif + + if (!doesRateLimitAffectClient) + { + return; + } + + GameMain.Server?.KickClient(client, reason.Value); + } + } + + public void Update(float deltaTime) + { + stopwatchResetTimer += deltaTime; + strikesResetTimer += deltaTime; + + // reset the stopwatch every second + if (stopwatchResetTimer > StopwatchResetInterval) + { + stopwatchResetTimer = 0; + foreach (OffenseData data in clients.Values) + { + data.ResetTimer(); + } + } + + // reset the strikes every minute + if (strikesResetTimer > StrikesResetInterval) + { + strikesResetTimer = 0; + foreach (var (client, data) in clients) + { + if (GameMain.Server?.ServerSettings is { MaxPacketAmount: > ServerSettings.PacketLimitMin } settings) + { + if (data.PacketCount > settings.MaxPacketAmount * 0.9f) + { + GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending a lot of packets and almost got kicked! ({data.PacketCount}).", ServerLog.MessageType.DoSProtection); + } + } + + data.ResetStrikes(); + } + } + } + + private OffenseData? GetData(Client client) => clients.TryGetValue(client, out OffenseData? data) ? data : null; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs b/Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs new file mode 100644 index 000000000..4c6f141c9 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs @@ -0,0 +1,135 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Networking; + +namespace Barotrauma +{ + public enum RateLimitAction + { + Invalid, + OnLimitReached, + OnLimitDoubled, + } + + public enum RateLimitPunishment + { + None, // just ignore + Announce, // announce to the server + Kick, // kick the player + Ban // ban the player + } + + internal sealed class RateLimiter + { + private sealed record RateLimit(DateTimeOffset Expiry) + { + public int RequestAmount; + } + + private readonly Dictionary rateLimits = new(); + private readonly HashSet expiredRateLimits = new(); + private readonly Dictionary recentlyAnnouncedOffenders = new(); + + private readonly int maxRequests, expiryInSeconds; + + private readonly ImmutableDictionary punishments; + + public RateLimiter(int maxRequests, int expiryInSeconds, params (RateLimitAction Action, RateLimitPunishment Punishment)[] punishmentRules) + { + this.maxRequests = maxRequests; + this.expiryInSeconds = expiryInSeconds; + + punishments = punishmentRules.ToImmutableDictionary( + static pair => pair.Action, + static pair => pair.Punishment); + } + + public bool IsLimitReached(Client client) + { +#if !DEBUG + if (IsExempt(client)) { return false; } +#endif + expiredRateLimits.Clear(); + + foreach (var (c, limit) in rateLimits) + { + if (limit.Expiry < DateTimeOffset.Now) + { + expiredRateLimits.Add(c); + } + } + + foreach (Client c in expiredRateLimits) + { + rateLimits.Remove(c); + } + + if (!rateLimits.TryGetValue(client, out RateLimit? rateLimit)) + { + rateLimit = new RateLimit(DateTimeOffset.Now.AddSeconds(expiryInSeconds)); + rateLimits.Add(client, rateLimit); + } + + rateLimit.RequestAmount++; + + if (rateLimit.RequestAmount > maxRequests) + { + ProcessPunishment(client, rateLimit.RequestAmount); + return true; + } + + return false; + } + + private void ProcessPunishment(Client client, int requests) + { + bool isDosProtectionEnabled = GameMain.Server is { ServerSettings.EnableDoSProtection: true }; + + foreach (var (action, punishment) in punishments) + { + switch (action) + { + case RateLimitAction.Invalid: + continue; + case RateLimitAction.OnLimitReached when requests >= maxRequests: + case RateLimitAction.OnLimitDoubled when requests >= maxRequests * 2: + switch (punishment) + { + case RateLimitPunishment.None: + continue; + case RateLimitPunishment.Announce: + AnnounceOffender(client); + break; + case RateLimitPunishment.Ban when isDosProtectionEnabled: + GameMain.Server?.BanClient(client, TextManager.Get("SpamFilterKicked").Value); + break; + case RateLimitPunishment.Kick when isDosProtectionEnabled: + GameMain.Server?.KickClient(client, TextManager.Get("SpamFilterKicked").Value); + break; + } + break; + } + } + } + + private void AnnounceOffender(Client client) + { + if (recentlyAnnouncedOffenders.TryGetValue(client, out DateTimeOffset expiry)) + { + if (expiry > DateTimeOffset.Now) { return; } + + recentlyAnnouncedOffenders.Remove(client); + } + + GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending too many packets!", ServerLog.MessageType.DoSProtection); + recentlyAnnouncedOffenders.Add(client, DateTimeOffset.Now.AddSeconds(expiryInSeconds)); + } + + public static bool IsExempt(Client client) => + (GameMain.Server.OwnerConnection != null && client.Connection == GameMain.Server.OwnerConnection) + || client.HasPermission(ClientPermissions.SpamImmunity); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 8e904d0d8..2370283c3 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,14 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.6.0 + 1.1.4.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico Debug;Release;Unstable true - ;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Barotrauma/BarotraumaShared/Data/languageoptions.xml b/Barotrauma/BarotraumaShared/Data/languageoptions.xml new file mode 100644 index 000000000..c104ddc2e --- /dev/null +++ b/Barotrauma/BarotraumaShared/Data/languageoptions.xml @@ -0,0 +1,22 @@ + + diff --git a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml index 961def545..e42ed985a 100644 --- a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml +++ b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml @@ -8,7 +8,7 @@ + permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney,ManageBotTalents,SpamImmunity"> diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index f0eae6be8..131be5ec0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -168,6 +168,7 @@ namespace Barotrauma public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left; public bool IsSteeringThroughGap { get; protected set; } + public bool IsTryingToSteerThroughGap { get; protected set; } public virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime) { @@ -444,7 +445,7 @@ namespace Barotrauma if (EscapeTarget != null) { var door = EscapeTarget.ConnectedDoor; - bool isClosedDoor = door != null && !door.IsOpen; + bool isClosedDoor = door != null && door.IsClosed; Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition; float sqrDist = diff.LengthSquared(); bool isClose = sqrDist < MathUtils.Pow2(100); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index bbe7863ec..24eebf8fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -245,11 +245,6 @@ namespace Barotrauma { throw new Exception($"Tried to create an enemy ai controller for human!"); } - if (Character.Params.Group == "human") - { - // Pet - Character.TeamID = CharacterTeamType.FriendlyNPC; - } var mainElement = c.Params.OriginalElement.IsOverride() ? c.Params.OriginalElement.FirstElement() : c.Params.OriginalElement; targetMemories = new Dictionary(); steeringManager = outsideSteering; @@ -309,17 +304,20 @@ namespace Barotrauma break; } } - + //pets are friendly! + if (PetBehavior != null || Character.Group == "human") + { + Character.TeamID = CharacterTeamType.FriendlyNPC; + } ReevaluateAttacks(); outsideSteering = new SteeringManager(this); insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors); steeringManager = outsideSteering; State = AIState.Idle; - 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); + CreatureMetrics.UnlockInEditor(Character.SpeciesName); } private CharacterParams.AIParams _aiParams; @@ -452,6 +450,7 @@ namespace Barotrauma base.Update(deltaTime); UpdateTriggers(deltaTime); Character.ClearInputs(); + IsTryingToSteerThroughGap = false; Reverse = false; bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); @@ -558,8 +557,9 @@ namespace Barotrauma } } - if (AIParams.CanOpenDoors) + if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors) { + // Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk). bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2); if (Character.Submarine != null || HasValidPath() && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer)) @@ -584,6 +584,7 @@ namespace Barotrauma } else { + // Normally the monsters only use pathing inside submarines, not outside. if (Character.Submarine != null && Character.Params.UsePathFinding) { if (steeringManager != insideSteering) @@ -848,7 +849,7 @@ namespace Barotrauma IsSteeringThroughGap = false; if (SwarmBehavior != null) { - SwarmBehavior.IsActive = State == AIState.Idle && Character.CurrentHull == null; + SwarmBehavior.IsActive = SwarmBehavior.ForceActive || State == AIState.Idle && Character.CurrentHull == null; SwarmBehavior.Refresh(); SwarmBehavior.UpdateSteering(deltaTime); } @@ -876,7 +877,7 @@ namespace Barotrauma var pathSteering = SteeringManager as IndoorsSteeringManager; if (pathSteering == null) { - if (SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f)) + if (Level.Loaded != null && Level.Loaded.GetRealWorldDepth(WorldPosition.Y) > Character.CharacterHealth.CrushDepth * 0.75f) { // Steer straight up if very deep SteeringManager.SteeringManual(deltaTime, Vector2.UnitY); @@ -1144,7 +1145,6 @@ namespace Barotrauma return; } } - attackLimbSelectionTimer -= deltaTime; if (AttackLimb == null || attackLimbSelectionTimer <= 0) { @@ -1154,7 +1154,8 @@ namespace Barotrauma AttackLimb = GetAttackLimb(attackWorldPos); } } - + Character targetCharacter = SelectedAiTarget.Entity as Character; + IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; bool canAttack = true; bool pursue = false; if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0)) @@ -1379,7 +1380,6 @@ namespace Barotrauma float distance = 0; Limb attackTargetLimb = null; - Character targetCharacter = SelectedAiTarget.Entity as Character; if (canAttack) { if (!Character.AnimController.SimplePhysicsEnabled) @@ -1400,29 +1400,29 @@ namespace Barotrauma attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb); } } - Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; + Vector2 toTargetOffset = toTarget; // Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute if (wallTarget != null && Character.Submarine == null) { if (wallTarget.Structure.Submarine != null) { Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity); - toTarget += margin; + toTargetOffset += margin; } } else if (targetCharacter != null) { Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity); - toTarget += margin; + toTargetOffset += margin; } else if (SelectedAiTarget.Entity is MapEntity e) { if (e.Submarine != null) { Vector2 margin = CalculateMargin(e.Submarine.Velocity); - toTarget += margin; + toTargetOffset += margin; } } @@ -1430,7 +1430,7 @@ namespace Barotrauma { if (targetVelocity == Vector2.Zero) { return Vector2.Zero; } float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange; - if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; } + if (diff <= 0 || toTargetOffset.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; } float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity)); if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; } float distanceOffset = diff * AttackLimb.attack.Duration; @@ -1439,7 +1439,7 @@ namespace Barotrauma } // Check that we can reach the target - distance = toTarget.Length(); + distance = toTargetOffset.Length(); canAttack = distance < AttackLimb.attack.Range; if (canAttack) { @@ -1523,20 +1523,18 @@ namespace Barotrauma } } Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null; + bool updateSteering = true; if (steeringLimb == null) { // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. steeringLimb = Character.AnimController.GetLimb(LimbType.Head) ?? Character.AnimController.GetLimb(LimbType.Torso); } - if (steeringLimb == null) { State = AIState.Idle; return; } - var pathSteering = SteeringManager as IndoorsSteeringManager; - if (AttackLimb != null && AttackLimb.attack.Retreat) { UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); @@ -1603,7 +1601,7 @@ namespace Barotrauma } } } - else + else if (!IsTryingToSteerThroughGap) { if (AttackLimb.attack.Ranged) { @@ -1624,6 +1622,10 @@ namespace Barotrauma SteeringManager.Reset(); } } + else + { + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition)); + } } else { @@ -1662,40 +1664,60 @@ namespace Barotrauma if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; } if (selectedTargetingParams == null) { break; } var targetSub = SelectedAiTarget.Entity?.Submarine; - if (targetSub == null) { break; } - float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2; - float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition); + ISpatialEntity spatialTarget = targetSub ?? SelectedAiTarget.Entity; + float targetSize = 0; + if (!selectedTargetingParams.IgnoreTargetSize) + { + targetSize = + targetSub != null ? Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2 : + targetCharacter != null ? ConvertUnits.ToDisplayUnits(targetCharacter.AnimController.Collider.GetSize().X) : 100; + } + float sqrDistToTarget = Vector2.DistanceSquared(WorldPosition, spatialTarget.WorldPosition); + bool isProgressive = AIParams.MaxAggression - AIParams.StartAggression > 0; switch (CirclePhase) { case CirclePhase.Start: - currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f)); + currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, ClampIntensity(aggressionIntensity)); inverseDir = false; circleDir = GetDirFromHeadingInRadius(); circleRotation = 0; strikeTimer = 0; blockCheckTimer = 0; breakCircling = false; - float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; - float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f; float maxFallBackDistance = selectedTargetingParams.CircleStartDistance; + float maxRandomOffset = selectedTargetingParams.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. - circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); - circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); - circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f))); - canAttack = false; + 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; + circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, intensity); + circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, intensity); + circleOffset = Rand.Vector(MathHelper.Lerp(maxRandomOffset, 0, intensity)); + } + else + { + circleRotationSpeed = selectedTargetingParams.CircleRotationSpeed; + circleFallbackDistance = maxFallBackDistance; + circleOffset = Rand.Vector(maxRandomOffset); + } + circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor); aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); - if (targetSub.Borders.Width < 1000) + DisableAttacksIfLimbNotRanged(); + if (targetSub != null && targetSub.Borders.Width < 1000 && AttackLimb?.attack is { Ranged: false }) { breakCircling = true; CirclePhase = CirclePhase.CloseIn; } - else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance)) + else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance)) { CirclePhase = CirclePhase.CloseIn; } - else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + else if (sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance)) { CirclePhase = CirclePhase.FallBack; } @@ -1705,52 +1727,76 @@ namespace Barotrauma } break; case CirclePhase.CloseIn: - if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) + Vector2 targetVelocity = GetTargetVelocity(); + float targetDistance = selectedTargetingParams.IgnoreTargetSize ? selectedTargetingParams.CircleStartDistance * 0.9f: + targetSize + selectedTargetingParams.CircleStartDistance / 2; + if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetVelocity)) { strikeTimer = AttackLimb.attack.CoolDown; CirclePhase = CirclePhase.Strike; } - else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) + else if (!breakCircling && sqrDistToTarget <= MathUtils.Pow2(targetDistance) && targetVelocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) { CirclePhase = CirclePhase.Advance; } - canAttack = false; + DisableAttacksIfLimbNotRanged(); break; case CirclePhase.FallBack: + updateSteering = false; bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true); - if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance)) + if (isBlocked || sqrDistToTarget > MathUtils.Pow2(targetSize + circleFallbackDistance)) { CirclePhase = CirclePhase.Advance; break; } - return; + DisableAttacksIfLimbNotRanged(); + break; case CirclePhase.Advance: - Vector2 subSpeed = targetSub.Velocity; - float requiredDistMultiplier = 1; - // If the target sub is moving fast, just steer towards the target until close enough to strike - if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f)) + Vector2 targetVel = GetTargetVelocity(); + // If the target is moving fast, just steer towards the target + if (breakCircling || targetVel.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed())) { CirclePhase = CirclePhase.CloseIn; } + else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance * 1.2f)) + { + if (selectedTargetingParams.DynamicCircleRotationSpeed && circleRotationSpeed < 100) + { + circleRotationSpeed *= 1 + deltaTime; + } + else + { + CirclePhase = CirclePhase.CloseIn; + } + } else { - circleRotation += deltaTime * circleRotationSpeed * circleDir; - if (circleRotation < -360) + float rotationStep = circleRotationSpeed * deltaTime * circleDir; + if (isProgressive) { - circleRotation += 360; + circleRotation += rotationStep; } - else if (circleRotation > 360) + else { - circleRotation -= 360; + circleRotation = rotationStep; } Vector2 targetPos = attackSimPos + circleOffset; - if (Vector2.DistanceSquared(SimPosition, targetPos) < 100) + float targetDist = targetSize; + if (targetDist <= 0) + { + targetDist = circleFallbackDistance; + } + if (targetSub != null && AttackLimb?.attack is { Ranged: true }) + { + targetDist += circleFallbackDistance / 2; + } + if (Vector2.DistanceSquared(SimPosition, targetPos) < ConvertUnits.ToSimUnits(targetDist)) { // Too close to the target point // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, // which makes it continue circling around the point (as supposed) // But when there is some offset and the offset is too near, this is not what we want. - if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + if (canAttack && AttackLimb?.attack is { Ranged: false } && sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance)) { CirclePhase = CirclePhase.Strike; strikeTimer = AttackLimb.attack.CoolDown; @@ -1762,7 +1808,6 @@ namespace Barotrauma break; } steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation); - requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed); if (IsBlocked(deltaTime, steerPos)) { if (!inverseDir) @@ -1774,7 +1819,7 @@ namespace Barotrauma else if (circleRotationSpeed < 1) { // Then try increasing the rotation speed to change the movement curve - circleRotationSpeed *= 1.1f; + circleRotationSpeed *= 1 + deltaTime; } else if (circleOffset.LengthSquared() > 0.1f) { @@ -1784,16 +1829,24 @@ namespace Barotrauma else { // If we still fail, just steer towards the target - breakCircling = true; + breakCircling = AttackLimb?.attack is { Ranged: false }; + if (!breakCircling) + { + CirclePhase = CirclePhase.FallBack; + } } } } - if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + if (AttackLimb?.attack is { Ranged: false }) { - strikeTimer = AttackLimb.attack.CoolDown; - CirclePhase = CirclePhase.Strike; + canAttack = false; + float requiredDistMultiplier = GetStrikeDistanceMultiplier(targetVel); + if (distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + { + strikeTimer = AttackLimb.attack.CoolDown; + CirclePhase = CirclePhase.Strike; + } } - canAttack = false; break; case CirclePhase.Strike: strikeTimer -= deltaTime; @@ -1815,18 +1868,19 @@ namespace Barotrauma return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin; } - float GetStrikeDistanceMultiplier(Vector2 subSpeed) + float GetStrikeDistanceMultiplier(Vector2 targetVelocity) { + if (selectedTargetingParams.CircleStrikeDistanceMultiplier < 1) { return 0; } float requiredDistMultiplier = 2; - bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; + bool isHeading = Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; if (isHeading) { requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier; - float subSpeedHorizontal = Math.Abs(subSpeed.X); - if (subSpeedHorizontal > 1) + 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(subSpeedHorizontal / 10, 0, 1)); + requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(targetVelocityHorizontal / 10, 0, 1)); if (requiredDistMultiplier < 2) { requiredDistMultiplier = 2; @@ -1843,19 +1897,59 @@ namespace Barotrauma return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1; } - float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); + Vector2 GetTargetVelocity() + { + if (targetSub != null) + { + return targetSub.Velocity; + } + else if (targetCharacter != null) + { + return targetCharacter.AnimController.Collider.LinearVelocity; + } + return Vector2.Zero; + } + + float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.SwimFastParams.MovementSpeed * (targetSub != null ? 0.3f : 0.5f)); } } - if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) + if (updateSteering) { - bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f; - bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f); - if (fallBack) + if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) { - Reverse = true; - UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f; + bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f); + if (fallBack) + { + Reverse = true; + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + } + else if (advance) + { + if (pathSteering != null) + { + pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); + } + else + { + SteeringManager.SteeringSeek(steerPos, 10); + } + } + else + { + if (Character.CurrentHull == null && !canAttack) + { + SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); + } + else + { + SteeringManager.Reset(); + FaceTarget(SelectedAiTarget.Entity); + } + } } - else if (advance) + else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) { if (pathSteering != null) { @@ -1866,41 +1960,18 @@ namespace Barotrauma SteeringManager.SteeringSeek(steerPos, 10); } } - else + if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || + distance == 0 || + distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2) || + AttackLimb != null && AttackLimb.attack.Ranged)) { - if (Character.CurrentHull == null && !canAttack) - { - SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); - } - else - { - SteeringManager.Reset(); - FaceTarget(SelectedAiTarget.Entity); - } + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); } } - else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) - { - if (pathSteering != null) - { - pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); - } - else - { - SteeringManager.SteeringSeek(steerPos, 10); - } - } - - if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))) - { - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); - } } } Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity; - IDamageable damageTarget = targetEntity as IDamageable; - if (AttackLimb?.attack is Attack { Ranged: true} attack) + if (AttackLimb?.attack is Attack { Ranged: true } attack) { AimRangedAttack(attack, targetEntity); } @@ -1915,12 +1986,21 @@ namespace Barotrauma { AttackLimb.attack.ResetAttackTimer(); } + + void DisableAttacksIfLimbNotRanged() + { + if (AttackLimb?.attack is { Ranged: false }) + { + canAttack = false; + } + } } public void AimRangedAttack(Attack attack, Entity targetEntity) { if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; } Character.SetInput(InputType.Aim, false, true); + if (attack.AimRotationTorque <= 0) { return; } Limb limb = GetLimbToRotate(attack); if (limb != null) { @@ -2003,9 +2083,18 @@ namespace Barotrauma float prio = 1 + limb.attack.Priority; if (Character.AnimController.SimplePhysicsEnabled) { return prio; } float dist = Vector2.Distance(limb.WorldPosition, attackPos); - // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. - // We also need a max value that is more than the actual range. - float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); + float distanceFactor = 1; + if (limb.attack.Ranged) + { + float min = 100; + distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist)); + } + else + { + // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. + // We also need a max value that is more than the actual range. + distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); + } return prio * distanceFactor; } } @@ -2164,7 +2253,9 @@ namespace Barotrauma { if (SelectedAiTarget?.Entity == null) { return false; } if (AttackLimb?.attack == null) { return false; } - if (damageTarget == null) { return false; } + ISpatialEntity spatialTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as ISpatialEntity; + if (spatialTarget == null) { return false; } + ActiveAttack = AttackLimb.attack; if (wallTarget != null) { // If the selected target is not the wall target, make the wall target the selected target. @@ -2176,13 +2267,14 @@ namespace Barotrauma return true; } } + if (damageTarget == null) { return false; } ActiveAttack = AttackLimb.attack; if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0) { Limb referenceLimb = GetLimbToRotate(ActiveAttack); if (referenceLimb != null) { - Vector2 toTarget = damageTarget.WorldPosition - referenceLimb.WorldPosition; + Vector2 toTarget = spatialTarget.WorldPosition - referenceLimb.WorldPosition; float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir); float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget)); @@ -2200,16 +2292,20 @@ namespace Barotrauma { if (item.RequireAimToUse) { - if (!Aim(deltaTime, damageTarget as ISpatialEntity, item)) + if (!Aim(deltaTime, spatialTarget, item)) { // Valid target, but can't shoot -> return true so that it will not be ignored. return true; } } - Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); - item.Use(deltaTime, Character); + if (damageTarget != null) + { + Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); + item.Use(deltaTime, Character); + } } } + if (damageTarget == null) { return true; } //simulate attack input to get the character to attack client-side Character.SetInput(InputType.Attack, true, true); if (!ActiveAttack.IsRunning) @@ -2224,20 +2320,11 @@ namespace Barotrauma Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); #endif } - if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) { if (ActiveAttack.CoolDownTimer > 0) { SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f)); - // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon - float greed = AIParams.AggressionGreed; - if (damageTarget is not Barotrauma.Character) - { - // Halve the greed for attacking non-characters. - greed /= 2; - } - selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; } if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter) { @@ -2269,10 +2356,19 @@ namespace Barotrauma private float aimTimer; private float visibilityCheckTimer; private bool canSeeTarget; + private float sinTime; private bool Aim(float deltaTime, ISpatialEntity target, Item weapon) { if (target == null || weapon == null) { return false; } + if (AttackLimb == null) { return false; } + Vector2 toTarget = target.WorldPosition - weapon.WorldPosition; + float dist = toTarget.Length(); Character.CursorPosition = target.WorldPosition; + if (AttackLimb.attack.SwayAmount > 0) + { + sinTime += deltaTime * AttackLimb.attack.SwayFrequency; + Character.CursorPosition += VectorExtensions.Forward(weapon.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2 * AttackLimb.attack.SwayAmount); + } if (Character.Submarine != null) { Character.CursorPosition -= Character.Submarine.Position; @@ -2294,11 +2390,11 @@ namespace Barotrauma aimTimer -= deltaTime; return false; } - Vector2 toTarget = target.WorldPosition - weapon.WorldPosition; float angle = VectorExtensions.Angle(VectorExtensions.Forward(weapon.body.TransformedRotation), toTarget); - float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(100, 1000, toTarget.Length())); + float minDistance = 300; + float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(minDistance, 1000, dist)); float margin = MathHelper.PiOver4 * distanceFactor; - if (angle < margin) + if (angle < margin || dist < minDistance) { var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true); @@ -2591,13 +2687,19 @@ namespace Barotrauma { // Ignore all structures, items, and hulls inside these subs. if (aiTarget.Entity.Submarine != null) - { - if (aiTarget.Entity.Submarine.Info.IsWreck || - aiTarget.Entity.Submarine.Info.IsBeacon || + { + if (aiTarget.Entity.Submarine.Info.IsWreck || + aiTarget.Entity.Submarine.Info.IsBeacon || UnattackableSubmarines.Contains(aiTarget.Entity.Submarine)) { continue; } + //ignore the megaruin in end levels + if (aiTarget.Entity.Submarine.Info.OutpostGenerationParams != null && + aiTarget.Entity.Submarine.Info.OutpostGenerationParams.ForceToEndLocationIndex > -1) + { + continue; + } } if (aiTarget.Entity is Hull hull) { @@ -2698,7 +2800,7 @@ namespace Barotrauma } else if (CanPassThroughHole(s, i)) { - valueModifier *= isInnerWall ? 1 : 0; + valueModifier *= isInnerWall ? 0.5f : 0; } else if (!canAttackWalls) { @@ -2968,7 +3070,8 @@ namespace Barotrauma // In the attack state allow going into non-allowed zone only when chasing a target. if (State == targetParams.State && SelectedAiTarget == aiTarget) { break; } } - if (!IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _)) + bool insideSameSub = aiTarget?.Entity?.Submarine != null && aiTarget.Entity.Submarine == Character.Submarine; + if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _)) { // If we have recently been damaged by the target (or another player/bot in the same team) allow targeting it even when we are in the idle state. bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget); @@ -3401,10 +3504,10 @@ namespace Barotrauma private readonly float stateResetCooldown = 10; private float stateResetTimer; private bool isStateChanged; - private readonly Dictionary activeTriggers = new Dictionary(); - private readonly HashSet inactiveTriggers = new HashSet(); + private readonly Dictionary activeTriggers = new Dictionary(); + private readonly HashSet inactiveTriggers = new HashSet(); - public void LaunchTrigger(AITrigger trigger) + public void LaunchTrigger(StatusEffect.AITrigger trigger) { if (trigger.IsTriggered) { return; } if (activeTriggers.ContainsKey(trigger)) { return; } @@ -3424,7 +3527,7 @@ namespace Barotrauma { foreach (var triggerObject in activeTriggers) { - AITrigger trigger = triggerObject.Key; + StatusEffect.AITrigger trigger = triggerObject.Key; if (trigger.IsPermanent) { continue; } trigger.UpdateTimer(deltaTime); if (!trigger.IsActive) @@ -3434,7 +3537,7 @@ namespace Barotrauma inactiveTriggers.Add(trigger); } } - foreach (AITrigger trigger in inactiveTriggers) + foreach (StatusEffect.AITrigger trigger in inactiveTriggers) { activeTriggers.Remove(trigger); } @@ -3588,6 +3691,11 @@ namespace Barotrauma observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f); } reachTimer = 0; + sinTime = 0; + if (breakCircling && strikeTimer <= 0) + { + CirclePhase = CirclePhase.Start; + } } protected override void OnStateChanged(AIState from, AIState to) @@ -3609,6 +3717,11 @@ namespace Barotrauma } blockCheckTimer = 0; reachTimer = 0; + sinTime = 0; + if (breakCircling && strikeTimer <= 0) + { + CirclePhase = CirclePhase.Start; + } } private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f); @@ -3712,6 +3825,7 @@ namespace Barotrauma { targetDir = Vector2.Zero; if (Level.Loaded == null) { return true; } + if (Level.Loaded.LevelData.Biome.IsEndBiome) { return true; } if (AIParams.AvoidAbyss) { if (pos.Y < Level.Loaded.AbyssStart) @@ -3771,6 +3885,7 @@ namespace Barotrauma public override bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime) { + IsTryingToSteerThroughGap = true; wallTarget = null; LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2); Character.AnimController.ReleaseStuckLimbs(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 919db14e8..9330350dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -1,10 +1,10 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; -using Barotrauma.Items.Components; namespace Barotrauma { @@ -395,6 +395,8 @@ namespace Barotrauma } objectiveManager.UpdateObjectives(deltaTime); + UpdateDragged(deltaTime); + if (reportProblemsTimer > 0) { reportProblemsTimer -= deltaTime; @@ -430,7 +432,7 @@ namespace Barotrauma } if (reportProblemsTimer <= 0.0f) { - if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) + if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) { ReportProblems(); } @@ -444,7 +446,7 @@ namespace Barotrauma if (objectiveManager.CurrentObjective == null) { return; } objectiveManager.DoCurrentObjective(deltaTime); - bool run = objectiveManager.CurrentObjective.ForceRun || !objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority; + bool run = (objectiveManager.CurrentObjective.ForceRun && !objectiveManager.CurrentObjective.ForceWalk) || (!objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority); if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null) { if (Character.CurrentHull == null) @@ -546,12 +548,11 @@ namespace Barotrauma bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective) { - if (!Character.NeedsAir) { return false; } bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; Hull targetHull = gotoObjective.GetTargetHull(); - return gotoObjective.Target != null && targetHull == null || + return (gotoObjective.Target != null && targetHull == null && !Character.IsImmuneToPressure) || NeedsDivingGear(targetHull, out _) || - insideSteering && (PathSteering.CurrentPath.HasOutdoorsNodes || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _))); + (insideSteering && ((PathSteering.CurrentPath.HasOutdoorsNodes && !Character.IsImmuneToPressure) || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _)))); } if (isCarrying) @@ -584,7 +585,7 @@ namespace Barotrauma Character.AnimController.InWater || Character.AnimController.HeadInWater || Character.Submarine == null || - (Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) || + (!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) || ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 || @@ -621,9 +622,10 @@ namespace Barotrauma } else if (gotoObjective.Mimic) { + bool targetHasDivingGear = HasDivingGear(gotoObjective.Target as Character, requireOxygenTank: false); if (!removeSuit) { - removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character); + removeDivingSuit = !targetHasDivingGear; if (removeDivingSuit) { removeSuit = true; @@ -631,7 +633,7 @@ namespace Barotrauma } if (!removeMask) { - takeMaskOff = !HasDivingMask(gotoObjective.Target as Character); + takeMaskOff = !targetHasDivingGear; if (takeMaskOff) { removeMask = true; @@ -783,20 +785,23 @@ namespace Barotrauma private void HandleRelocation(Item item) { - if (item.Submarine?.TeamID == CharacterTeamType.FriendlyNPC) + if (item.SpawnedInCurrentOutpost) { return; } + if (item.Submarine == 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 + if (item.Submarine.TeamID == Character.TeamID) { return; } + if (itemsToRelocate.Contains(item)) { return; } + itemsToRelocate.Add(item); + if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort)) { - if (itemsToRelocate.Contains(item)) { return; } - itemsToRelocate.Add(item); - if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort)) - { - myPort.OnUnDocked += Relocate; - } - var campaign = GameMain.GameSession.Campaign; - if (campaign != null) - { - // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. - campaign.BeforeLevelLoading += Relocate; - } + myPort.OnUnDocked += Relocate; + } + var campaign = GameMain.GameSession.Campaign; + if (campaign != null) + { + // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. + campaign.BeforeLevelLoading += Relocate; } void Relocate() @@ -907,6 +912,35 @@ namespace Barotrauma return false; } + private float draggedTimer; + private float refuseDraggingTimer; + private const float RefuseDraggingAfter = 10.0f; + private const float RefuseDraggingDuration = 30.0f; + + private void UpdateDragged(float deltaTime) + { + if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; } + + //don't allow player characters who aren't in the same team to drag us for more than x seconds + if (Character.SelectedBy == null || + !Character.SelectedBy.IsPlayer || + Character.SelectedBy.TeamID == Character.TeamID) + { + refuseDraggingTimer -= deltaTime; + return; + } + + draggedTimer += deltaTime; + if (draggedTimer > RefuseDraggingAfter || + (draggedTimer > 0.5f && refuseDraggingTimer > 0.0f)) + { + draggedTimer = 0.0f; + refuseDraggingTimer = RefuseDraggingDuration; + Character.SelectedBy.DeselectCharacter(); + Character.Speak(TextManager.Get("dialogrefusedragging").Value, delay: 0.5f, identifier: "refusedragging".ToIdentifier(), minDurationBetweenSimilar: 5.0f); + } + } + protected void ReportProblems() { Order newOrder = null; @@ -989,8 +1023,8 @@ namespace Barotrauma targetHull = hull; } } - } - foreach (Item item in Item.ItemList) + } + foreach (Item item in Item.RepairableItems) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, Character)) @@ -1209,7 +1243,7 @@ namespace Barotrauma } else { - isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0; + isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.AlienInfectedType) > 0; // Inform other NPCs if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold) { @@ -1523,7 +1557,7 @@ namespace Barotrauma { margin *= 2; } - float minCeilingDist = mainCollider.height / 2 + mainCollider.radius + margin; + float minCeilingDist = mainCollider.Height / 2 + mainCollider.Radius + margin; shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null; } @@ -1546,23 +1580,19 @@ namespace Barotrauma public bool NeedsDivingGear(Hull hull, out bool needsSuit) { - if (!Character.NeedsAir) - { - needsSuit = false; - return false; - } needsSuit = false; + bool needsAir = Character.NeedsAir && Character.CharacterHealth.OxygenLowResistance < 1; if (hull == null || hull.WaterPercentage > 90 || hull.LethalPressure > 0 || hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f)) { - needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); - return true; + needsSuit = !Character.IsProtectedFromPressure; + return needsAir || needsSuit; } if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) { - return true; + return needsAir; } return false; } @@ -1641,7 +1671,7 @@ namespace Barotrauma { if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead || otherCharacter.Info?.Job == null || - !(otherCharacter.AIController is HumanAIController otherHumanAI) || + otherCharacter.AIController is not HumanAIController otherHumanAI || !otherHumanAI.VisibleHulls.Contains(character.CurrentHull)) { continue; @@ -1654,10 +1684,10 @@ namespace Barotrauma float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage); maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage); - if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) + if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null && character.IsPlayer) { var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage; - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage); } if (accumulatedDamage <= WarningThreshold) { return; } @@ -1745,12 +1775,14 @@ namespace Barotrauma } if (!someoneSpoke) { - if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) + if (!item.StolenDuringRound && + Level.Loaded?.Type == LevelData.LevelType.Outpost && + GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) { var reputationLoss = MathHelper.Clamp( (item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice, Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem); - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss); } item.StolenDuringRound = true; otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f); @@ -1843,7 +1875,7 @@ namespace Barotrauma } break; case "reportbrokendevices": - foreach (var item in Item.ItemList) + foreach (var item in Item.RepairableItems) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, character)) @@ -1924,11 +1956,12 @@ namespace Barotrauma bool isCurrentHull = character == Character && character.CurrentHull == hull; if (hull == null) { + float hullSafety = character.IsProtectedFromPressure ? 0 : 100; if (isCurrentHull) { - CurrentHullSafety = character.NeedsAir ? 0 : 100; + CurrentHullSafety = hullSafety; } - return CurrentHullSafety; + return hullSafety; } if (isCurrentHull && visibleHulls == null) { @@ -1936,10 +1969,9 @@ namespace Barotrauma visibleHulls = VisibleHulls; } bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); - bool ignoreWater = character.IsProtectedFromPressure(); - bool ignoreOxygen = HasDivingGear(character); + bool ignoreOxygen = HasDivingGear(character); bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.IsCurrentObjective(); - float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); + float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) { CurrentHullSafety = safety; @@ -1949,15 +1981,33 @@ namespace Barotrauma private static float CalculateHullSafety(Hull hull, IEnumerable visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) { - if (hull == null) { return character.NeedsAir ? 0 : 100; } - if (hull.LethalPressure > 0 && character.PressureProtection <= 0 && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { return 0; } + bool isProtectedFromPressure = character.IsProtectedFromPressure; + if (hull == null) { return isProtectedFromPressure ? 100 : 0; } + if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; } // Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower. // With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD. float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage)); - float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, hull.WaterPercentage / 100); - if (!character.NeedsAir) + float waterFactor = 1; + if (!ignoreWater) + { + if (visibleHulls != null) + { + // Take the visible hulls into account too, because otherwise multi-hull rooms on several floors (with platforms) will yield unexpected results. + float relativeWaterVolume = visibleHulls.Sum(s => s.WaterVolume) / visibleHulls.Sum(s => s.Volume); + waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume); + } + else + { + float relativeWaterVolume = hull.WaterVolume / hull.Volume; + waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume); + } + } + if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1) { oxygenFactor = 1; + } + if (isProtectedFromPressure) + { waterFactor = 1; } float fireFactor = 1; @@ -2005,6 +2055,10 @@ namespace Barotrauma public float GetHullSafety(Hull hull, Character character, IEnumerable visibleHulls = null) { + if (hull == null) + { + return CalculateHullSafety(hull, character, visibleHulls); + } if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety)) { hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls)); @@ -2019,6 +2073,10 @@ namespace Barotrauma public static float GetHullSafety(Hull hull, IEnumerable visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) { + if (hull == null) + { + return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); + } HullSafety hullSafety; if (character.AIController is HumanAIController controller) { @@ -2047,21 +2105,53 @@ namespace Barotrauma bool sameTeam = me.TeamID == other.TeamID; bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other); if (!teamGood) { return false; } - if (!me.IsSameSpeciesOrGroup(other)) { return false; } - if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) + if (other.IsPet) + { + // Hostile NPCs are hostile to all pets, unless they are in the same team. + if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; } + } + else + { + if (!me.IsSameSpeciesOrGroup(other)) { return false; } + } + if (GameMain.GameSession?.GameMode is CampaignMode) + { + if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) || + (me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC)) + { + Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other; + //NPCs that allow some campaign interaction are not turned hostile by low reputation + if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; } + if (!npc.IsEscorted && npc.AIController is HumanAIController npcAI) + { + return !npcAI.IsInHostileFaction(); + } + } + } + return true; + } + + public bool IsInHostileFaction() + { + if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; } + + Identifier npcFaction = Character.Faction; + Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty; + + if (npcFaction.IsEmpty) + { + //if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost + npcFaction = currentLocationFaction; + } + if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction) { var reputation = campaign.Map?.CurrentLocation?.Reputation; if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) { - return false; + return true; } } - if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet) - { - // Hostile NPCs are hostile to all pets, unless they are in the same team. - return false; - } - return true; + return false; } public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 5dc043af1..a98233d5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -26,7 +26,7 @@ namespace Barotrauma private float findPathTimer; - private const float buttonPressCooldown = 3; + private const float ButtonPressCooldown = 1; private float checkDoorsTimer; private float buttonPressTimer; @@ -96,7 +96,7 @@ namespace Barotrauma base.Update(speed); float step = 1.0f / 60.0f; checkDoorsTimer -= step; - if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen) + if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsFullyOpen) { buttonPressTimer = 0; } @@ -211,7 +211,7 @@ namespace Barotrauma currentTarget = target; Vector2 currentPos = host.SimPosition; pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin; - pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0; + pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure; var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0; if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) @@ -310,7 +310,7 @@ namespace Barotrauma // Only humanoids can climb ladders bool canClimb = character.AnimController is HumanoidAnimController; //if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically - if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius) + if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius) { diff.Y = 0.0f; } @@ -342,7 +342,7 @@ namespace Barotrauma CheckDoorsInPath(); doorsChecked = true; } - if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen) + if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsFullyOpen) { // We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again. Reset(); @@ -395,7 +395,7 @@ namespace Barotrauma } //at the same height as the waypoint float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y); - float colliderSize = (collider.height / 2 + collider.radius) * 1.25f; + float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f; if (heightDiff < colliderSize) { float heightFromFloor = character.AnimController.GetHeightFromFloor(); @@ -510,7 +510,7 @@ namespace Barotrauma private bool CanAccessDoor(Door door, Func buttonFilter = null) { if (door.IsBroken) { return true; } - if (!door.IsOpen) + if (door.IsClosed) { if (!door.Item.IsInteractable(character)) { return false; } if (!ShouldBreakDoor(door)) @@ -536,7 +536,7 @@ namespace Barotrauma } foreach (var linked in door.Item.linkedTo) { - if (!(linked is Item linkedItem)) { continue; } + if (linked is not Item linkedItem) { continue; } var button = linkedItem.GetComponent(); if (button == null) { continue; } if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button))) @@ -694,7 +694,7 @@ namespace Barotrauma if (door.Item.TryInteract(character, forceSelectKey: true)) { lastDoor = (door, shouldBeOpen); - buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0; + buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0; } else { @@ -712,7 +712,7 @@ namespace Barotrauma if (closestButton.Item.TryInteract(character, forceSelectKey: true)) { lastDoor = (door, shouldBeOpen); - buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0; + buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0; } else { @@ -785,7 +785,7 @@ namespace Barotrauma { if (hull.WaterVolume / hull.Rect.Width > 100.0f) { - if (!HumanAIController.HasDivingSuit(character)) + if (!HumanAIController.HasDivingSuit(character) && character.CharacterHealth.OxygenLowResistance < 1) { penalty += 500.0f; } @@ -808,7 +808,7 @@ namespace Barotrauma private float? GetSingleNodePenalty(PathNode node) { - if (node.Waypoint.isObstructed) { return null; } + 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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 5dc813a8c..2dcdc2e10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -244,7 +244,7 @@ namespace Barotrauma else { float squaredDistance = Vector2.DistanceSquared(character.SimPosition, _attachPos); - float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f; + float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.Radius, character.AnimController.Collider.Width), character.AnimController.Collider.Height) * 1.2f; if (squaredDistance < targetDistance * targetDistance) { //close enough to a wall -> attach diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs index 3b74d869d..e273488e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs @@ -107,7 +107,6 @@ namespace Barotrauma { return MentalType.Normal; } - // test this later int psychosisIndex = (int)(affliction.Strength / (affliction.Prefab.MaxStrength / MentalTypeCount) * Rand.Range(1f, 1.2f)); psychosisIndex = Math.Clamp(psychosisIndex, 0, 4); MentalType mentalType = psychosisIndex switch diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 4ddfe05c1..2fb0e0027 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -83,11 +83,16 @@ namespace Barotrauma { if (GameMain.GameSession.RoundDuration < 120.0f && speaker?.CurrentHull != null && + GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f && (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) { currentFlags.Add("EnterOutpost".ToIdentifier()); } + if (Level.Loaded.IsEndBiome) + { + currentFlags.Add("EndLevel".ToIdentifier()); + } } if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f) { @@ -117,7 +122,7 @@ namespace Barotrauma foreach (Affliction affliction in afflictions) { var currentEffect = affliction.GetActiveEffect(); - if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag.Value) && !currentFlags.Contains(currentEffect.DialogFlag)) + if (currentEffect is { DialogFlag.IsEmpty: false } && !currentFlags.Contains(currentEffect.DialogFlag)) { currentFlags.Add(currentEffect.DialogFlag); } @@ -126,6 +131,10 @@ namespace Barotrauma if (speaker.TeamID == CharacterTeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost) { currentFlags.Add("OutpostNPC".ToIdentifier()); + if (GameMain.GameSession?.Level?.StartLocation?.Faction is Faction faction) + { + currentFlags.Add($"OutpostNPC{faction.Prefab.Identifier}".ToIdentifier()); + } } if (speaker.CampaignInteractionType != CampaignMode.InteractionType.None) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index cb7523470..3a16cf84d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -256,7 +256,9 @@ namespace Barotrauma if (!AllowOutsideSubmarine && character.Submarine == null) { return false; } if (AllowInAnySub) { return true; } if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; } - return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID); + return character.Submarine.TeamID == character.TeamID || + character.Submarine.TeamID == character.OriginalTeamID || + character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID || sub.TeamID == character.OriginalTeamID); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 3957e400a..066b2b9a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -648,11 +648,11 @@ namespace Barotrauma { statusEffects = statusEffects.Concat(hitEffects); } - float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == "stun" ? a.Strength : 0); + float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0); float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se => { float stunAmount = 0; - var stunAffliction = se.Afflictions.Find(a => a.Identifier == "stun"); + var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType); if (stunAffliction != null) { stunAmount = stunAffliction.Strength; @@ -1176,30 +1176,31 @@ namespace Barotrauma if (sqrDistance > repairTool.Range * repairTool.Range) { return; } } float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy); - if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor) + if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor) { if (myBodies == null) { myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); } - var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true); - if (pickedBody != null) + // Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay) + var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter); + foreach (var body in pickedBodies) { Character target = null; - if (pickedBody.UserData is Character c) + if (body.UserData is Character c) { target = c; } - else if (pickedBody.UserData is Limb limb) + else if (body.UserData is Limb limb) { target = limb.character; } - if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target))) + if (target != null && target != Enemy && HumanAIController.IsFriendly(target)) { - UseWeapon(deltaTime); + return; } } + UseWeapon(deltaTime); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 08d7ea70c..4c1d874d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -200,7 +201,8 @@ namespace Barotrauma (container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) || ItemToContain == null || ItemToContain.Removed || !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, - SpeakIfFails = !objectiveManager.IsCurrentOrder() + SpeakIfFails = !objectiveManager.IsCurrentOrder(), + endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 5dcbdb17e..aee20f6ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -19,7 +19,6 @@ namespace Barotrauma private AIObjectiveGetItem getExtinguisherObjective; private AIObjectiveGoTo gotoObjective; - private float useExtinquisherTimer; public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -44,7 +43,8 @@ namespace Barotrauma } else { - float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y); + float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; + float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y); yDist = yDist > 100 ? yDist * 3 : 0; float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist; float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); @@ -119,24 +119,18 @@ namespace Barotrauma Abandon = true; break; } - float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange; - float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y); - bool inRange = xDist + yDist < extinguisher.Range; - // Use the hull position, because the fire x pos is sometimes inside a wall -> the bot can't ever see it and continues running towards the wall. - ISpatialEntity lookTarget = character.CurrentHull == targetHull || character.CurrentHull.linkedTo.Contains(targetHull) ? targetHull : fs as ISpatialEntity; - bool move = !inRange || !character.CanSeeTarget(lookTarget); - if ((inRange && character.CanSeeTarget(lookTarget)) || useExtinquisherTimer > 0) + float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X); + float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y); + float dist = xDist + yDist; + bool inRange = dist < extinguisher.Range; + bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull); + bool moveCloser = !isInDamageRange && (!inRange || !character.CanSeeTarget(targetHull)); + bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull)); + if (operateExtinguisher) { - useExtinquisherTimer += deltaTime; - if (useExtinquisherTimer > 2.0f) - { - useExtinquisherTimer = 0.0f; - } - // Aim character.CursorPosition = fs.Position; Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition; - float dist = fromCharacterToFireSource.Length(); - character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2); + character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2); if (extinguisherItem.RequireAimToUse) { character.SetInput(InputType.Aim, false, true); @@ -148,25 +142,29 @@ namespace Barotrauma { character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f); } + // Prevents running into the flames. + objectiveManager.CurrentObjective.ForceWalk = true; } - if (move) + if (moveCloser) { - //go to the first firesource - if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f)) - { - DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(), - TargetName = fs.Hull.DisplayName - }, - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref gotoObjective))) + if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f) + { + DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(), + TargetName = fs.Hull.DisplayName, + }, + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref gotoObjective))) { gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull); } } - else + else if (!operateExtinguisher || isInDamageRange) { - character.AIController.SteeringManager.Reset(); + // Don't walk into the flames. + RemoveSubObjective(ref gotoObjective); + SteeringManager.Reset(); } + // Only target one fire source at the time. break; } } @@ -177,8 +175,20 @@ namespace Barotrauma base.Reset(); getExtinguisherObjective = null; gotoObjective = null; - useExtinquisherTimer = 0; sinTime = 0; + SteeringManager.Reset(); + } + + protected override void OnCompleted() + { + base.OnCompleted(); + SteeringManager.Reset(); + } + + protected override void OnAbandon() + { + base.OnAbandon(); + SteeringManager.Reset(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index e525b613c..7bbe5e18b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -24,7 +24,7 @@ namespace Barotrauma protected override float TargetEvaluation() { if (Targets.None()) { return 0; } - if (!character.IsOnPlayerTeam) { return 100; } + if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return 100; } if (character.IsSecurity) { return 100; } if (objectiveManager.IsOrder(this)) { return 100; } // If there's any security officers onboard, leave fighting for them. @@ -66,7 +66,13 @@ namespace Barotrauma if (target.CurrentHull == null) { return false; } if (HumanAIController.IsFriendly(character, target)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } - if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID) { return false; } + if (!targetCharactersInOtherSubs) + { + if (character.Submarine.TeamID != target.Submarine.TeamID && character.OriginalTeamID != target.Submarine.TeamID) + { + return false; + } + } if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } if (target.IsArrested) { return false; } if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index e299a8edb..221067c28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -47,19 +47,12 @@ namespace Barotrauma } if (character.CurrentHull == null) { - if (!character.NeedsAir) - { - Priority = 0; - } - else - { - Priority = ( - objectiveManager.HasOrder(o => o.Priority > 0) || - objectiveManager.HasOrder(o => o.Priority > 0) || - objectiveManager.HasActiveObjective() || - objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) - && HumanAIController.HasDivingSuit(character) ? 0 : 100; - } + Priority = ( + objectiveManager.HasOrder(o => o.Priority > 0) || + objectiveManager.HasOrder(o => o.Priority > 0) || + objectiveManager.HasActiveObjective() || + objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) + && ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100; } else { @@ -118,6 +111,11 @@ namespace Barotrauma if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) { Priority -= priorityDecrease * deltaTime; + if (currenthullSafety >= 100) + { + // Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock. + Priority = 0; + } } else { @@ -140,8 +138,8 @@ namespace Barotrauma { if (resetPriority) { return; } var currentHull = character.CurrentHull; + bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0); bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); - bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0; if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull)) { bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit); @@ -221,7 +219,11 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) { - AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50) + AllowGoingOutside = + character.IsProtectedFromPressure || + character.CurrentHull == null || + character.CurrentHull.IsTaggedAirlock() || + character.CurrentHull.LeadsOutside(character) }, onCompleted: () => { @@ -352,8 +354,8 @@ namespace Barotrauma //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.Any()) + float hullSuitability = EstimateHullSuitability(character, hull); + if (hulls.None()) { hulls.Add(hull); } @@ -448,9 +450,12 @@ namespace Barotrauma { hullSafety = 100; } + float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; + float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y); + yDist = yDist > 100 ? yDist * 3 : 0; + float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist; // Huge preference for closer targets - float distance = Vector2.DistanceSquared(character.WorldPosition, potentialHull.WorldPosition); - float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); + float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance)); hullSafety *= distanceFactor; // If the target is not inside a friendly submarine, considerably reduce the hull safety. // Intentionally exclude wrecks from this check diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 9604579dd..dd0b1e20b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -155,17 +155,21 @@ namespace Barotrauma bool canOperate = toLeak.LengthSquared() < reach * reach; if (canOperate) { - TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak), - onAbandon: () => Abandon = true, - onCompleted: () => + TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak) + { + // Use an empty filter to override the default + EndNodeFilter = n => true + }, + onAbandon: () => Abandon = true, + onCompleted: () => + { + if (CheckObjectiveSpecific()) { IsCompleted = true; } + else { - if (CheckObjectiveSpecific()) { IsCompleted = true; } - else - { - // Failed to operate. Probably too far. - Abandon = true; - } - }); + // Failed to operate. Probably too far. + Abandon = true; + } + }); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index b3cdaed85..2f3e75a45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -123,6 +123,11 @@ namespace Barotrauma return ignoredTags; } + public static Func CreateEndNodeFilter(ISpatialEntity targetEntity) + { + return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach); + } + private bool CheckInventory() { if (IdentifiersOrTags == null) { return false; } @@ -155,11 +160,6 @@ namespace Barotrauma Abandon = true; return; } - if (character.Submarine == null) - { - Abandon = true; - return; - } if (IdentifiersOrTags != null && !isDoneSeeking) { if (checkInventory) @@ -171,9 +171,14 @@ namespace Barotrauma } if (!isDoneSeeking) { + if (character.Submarine == null) + { + Abandon = true; + return; + } if (!AllowDangerousPressure) { - bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0 && character.PressureProtection <= 0; + bool dangerousPressure = !character.IsProtectedFromPressure && (character.CurrentHull == null || character.CurrentHull.LethalPressure > 0); if (dangerousPressure) { #if DEBUG @@ -192,6 +197,11 @@ namespace Barotrauma return; } } + else if (character.Submarine == null) + { + Abandon = true; + return; + } if (targetItem == null || targetItem.Removed) { #if DEBUG @@ -307,7 +317,8 @@ namespace Barotrauma { // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget, - SpeakIfFails = false + SpeakIfFails = false, + endNodeFilter = CreateEndNodeFilter(moveToTarget) }; }, onAbandon: () => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 684713e02..e3a5efe07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -33,6 +33,11 @@ namespace Barotrauma public bool DebugLogWhenFails { get; set; } = true; public bool UsePathingOutside { get; set; } = true; + /// + /// Which event action created this objective (if any) + /// + public EventAction SourceEventAction; + public float ExtraDistanceWhileSwimming; public float ExtraDistanceOutsideSub; private float _closeEnoughMultiplier = 1; @@ -45,6 +50,7 @@ namespace Barotrauma private readonly float minDistance = 50; private readonly float seekGapsInterval = 1; private float seekGapsTimer; + private bool cantFindDivingGear; /// /// Display units @@ -85,7 +91,7 @@ namespace Barotrauma /// public bool UseDistanceRelativeToAimSourcePos { get; set; } = false; - public override bool AbandonWhenCannotCompleteSubjectives => !repeat; + public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowOutsideSubmarine => AllowGoingOutside; public override bool AllowInAnySub => true; @@ -258,48 +264,73 @@ namespace Barotrauma } if (!Abandon) { - if (getDivingGearIfNeeded && !character.LockHands) + if (getDivingGearIfNeeded) { Character followTarget = Target as Character; - bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); - bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); - if (Mimic) + bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure; + bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); + bool tryToGetDivingSuit = needsDivingSuit; + if (Mimic && !character.IsImmuneToPressure) { if (HumanAIController.HasDivingSuit(followTarget)) { - needsDivingGear = true; - needsDivingSuit = true; + tryToGetDivingGear = true; + tryToGetDivingSuit = true; } - else if (HumanAIController.HasDivingMask(followTarget)) + else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1) { - needsDivingGear = true; + tryToGetDivingGear = true; } } bool needsEquipment = false; float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character); - if (needsDivingSuit) + if (tryToGetDivingSuit) { needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen); } - else if (needsDivingGear) + else if (tryToGetDivingGear) { needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen); } - if (needsEquipment) + if (character.LockHands) + { + cantFindDivingGear = true; + } + if (cantFindDivingGear && needsDivingSuit) + { + // Don't try to reach the target without a suit because it's lethal. + Abandon = true; + return; + } + if (needsEquipment && !cantFindDivingGear) { SteeringManager.Reset(); - if (findDivingGear != null && !findDivingGear.CanBeCompleted) - { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref findDivingGear)); - } - else - { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref findDivingGear)); - } + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager), + onAbandon: () => + { + cantFindDivingGear = true; + if (needsDivingSuit) + { + // Shouldn't try to reach the target without a suit, because it's lethal. + Abandon = true; + } + else + { + // Try again without requiring the diving suit + RemoveSubObjective(ref findDivingGear); + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), + onAbandon: () => + { + Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); + RemoveSubObjective(ref findDivingGear); + }, + onCompleted: () => + { + RemoveSubObjective(ref findDivingGear); + }); + } + }, + onCompleted: () => RemoveSubObjective(ref findDivingGear)); return; } } @@ -593,7 +624,7 @@ namespace Barotrauma } else if (target is Character c) { - return c.CurrentHull; + return c.CurrentHull ?? c.AnimController.CurrentHull; } else if (target is Structure structure) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 72afb8181..3625f7a1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -170,7 +170,8 @@ namespace Barotrauma TargetHull = character.CurrentHull; } - if (behavior == BehaviorType.StayInHull) + bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); + if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid) { currentTarget = TargetHull; bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing; @@ -190,9 +191,6 @@ namespace Barotrauma } else { - bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || - (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); - if (currentTarget != null && !currentTargetIsInvalid) { if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index b8c94aca0..ec2a5ad30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -98,7 +98,7 @@ namespace Barotrauma { foreach (var item in itemContainer.ContainableItems) { - if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished) + if (CheckStatusEffects(item.StatusEffects) == CheckStatus.Finished) { return CheckStatus.Finished; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index aed77cb77..06305e134 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -23,6 +23,11 @@ namespace Barotrauma private AIObjectiveGoTo goToObjective; private AIObjectiveGetItem getItemObjective; + /// + /// If undefined, a default filter will be used. + /// + public Func EndNodeFilter; + public bool Override { get; set; } = true; public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null); @@ -222,7 +227,7 @@ namespace Barotrauma { target.Item.TryInteract(character, forceSelectKey: true); } - if (component.AIOperate(deltaTime, character, this)) + if (component.CrewAIOperate(deltaTime, character, this)) { isDoneOperating = completionCondition == null || completionCondition(); } @@ -232,7 +237,7 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50) { TargetName = target.Item.Name, - endNodeFilter = node => node.Waypoint.Ladders == null + endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); @@ -290,7 +295,7 @@ namespace Barotrauma } return; } - if (component.AIOperate(deltaTime, character, this)) + if (component.CrewAIOperate(deltaTime, character, this)) { isDoneOperating = completionCondition == null || completionCondition(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index e8b5dea26..9c06fb8b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -17,7 +17,6 @@ namespace Barotrauma private AIObjectiveGoTo goToObjective; private AIObjectiveContainItem refuelObjective; - private float previousCondition = -1; private RepairTool repairTool; private const float WaitTimeBeforeRepair = 0.5f; @@ -196,15 +195,7 @@ namespace Barotrauma Abandon = true; } } - if (previousCondition == -1) - { - previousCondition = Item.Condition; - } - else if (Item.Condition < previousCondition) - { - // If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it. - Abandon = true; - } + CheckPreviousCondition(deltaTime); } if (Abandon) { @@ -229,7 +220,6 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, constructor: () => { - previousCondition = -1; var objective = new AIObjectiveGoTo(Item, character, objectiveManager) { TargetName = Item.Name @@ -251,6 +241,27 @@ namespace Barotrauma } } + private const float conditionCheckDelay = 1; + private float conditionCheckTimer; + private float previousCondition; + private void CheckPreviousCondition(float deltaTime) + { + if (Item == null || Item.Removed) { return; } + conditionCheckTimer -= deltaTime; + if (conditionCheckTimer > 0) { return; } + conditionCheckTimer = conditionCheckDelay; + if (previousCondition > -1 && Item.Condition < previousCondition) + { + // If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it. + Abandon = true; + } + else + { + // If the previous condition is not yet stored or if it's valid (greater or equal to current condition), save the condition for the next check here. + previousCondition = Item.Condition; + } + } + private void FindRepairTool() { foreach (Repairable repairable in Item.Repairables) @@ -303,7 +314,6 @@ namespace Barotrauma base.Reset(); goToObjective = null; refuelObjective = null; - previousCondition = -1; repairTool = null; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index b75a3152e..5a93ceb26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -139,14 +139,14 @@ namespace Barotrauma recursive: true); } } - if (character.Submarine != null) + if (character.Submarine != null && targetCharacter.CurrentHull != null) { if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) { // Incapacitated target is not in a safe place -> Move to a safe place first if (character.SelectedCharacter != targetCharacter) { - if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) + if (HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) { character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", ("[targetname]", targetCharacter.Name, FormatCapitals.No), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 20be4494f..50f03a240 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -71,11 +71,11 @@ namespace Barotrauma { float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f); vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100; - if (affliction.Prefab.AfflictionType == "paralysis") + if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) { vitality -= affliction.Strength; } - else if (affliction.Prefab.AfflictionType == "poison") + else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType) { vitality -= affliction.Strength; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index c96c97b8c..14fc0d495 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -12,6 +12,9 @@ namespace Barotrauma private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective; private bool usingEscapeBehavior, isSteeringThroughGap; + public override bool AllowOutsideSubmarine => true; + public override bool AllowInAnySub => true; + public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier) { ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 042e913cd..0b48320e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -465,6 +465,10 @@ namespace Barotrauma public readonly OrderTarget TargetPosition; private ISpatialEntity targetSpatialEntity; + + /// + /// Note this property doesn't return the follow target of the Follow objective, as expected! + /// public ISpatialEntity TargetSpatialEntity { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 9e9398673..88db60899 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -348,6 +348,7 @@ namespace Barotrauma { if (body.UserData is Submarine) { return false; } if (body.UserData is Structure s && !s.IsPlatform) { return false; } + if (body.UserData is Voronoi2.VoronoiCell) { return false; } if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { return false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index b1a83f852..874a081bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -47,7 +47,8 @@ namespace Barotrauma public void SetOrder(Character orderedCharacter) { OrderedCharacter = orderedCharacter; - if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option))) + if (OrderedCharacter.AIController is HumanAIController humanAI && + humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option) && o.TargetEntity == TargetItem)) { if (orderedCharacter != CommandingCharacter) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs index d4d1ad1ad..402d3ed94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -17,7 +17,20 @@ namespace Barotrauma float GetTargetingImportance(Entity entity) { float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition); - return MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance); + + float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance * 0.5f); + if (TargetItem.Submarine != null) + { + Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition; + Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition; + if (Vector2.Dot(dir, submarineDir) < 0) + { + //direction from the weapon to the target is opposite to the direction from the sub to the weapon + // = the turret is most likely on the wrong side of the sub, reduce importance + importance *= 0.1f; + } + } + return importance; } public override void CalculateImportanceSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index b4a18d756..06be740df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -229,7 +229,7 @@ namespace Barotrauma #if DEBUG ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it was already being attended by " + shipIssueWorker.OrderedCharacter); #endif - attendedIssues.Add(shipIssueWorker); + InsertIssue(shipIssueWorker, attendedIssues); } else { @@ -237,12 +237,19 @@ namespace Barotrauma ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it is not attended to"); #endif shipIssueWorker.RemoveOrder(); - availableIssues.Add(shipIssueWorker); + InsertIssue(shipIssueWorker, availableIssues); } } - availableIssues.Sort((x, y) => y.Importance.CompareTo(x.Importance)); - attendedIssues.Sort((x, y) => x.Importance.CompareTo(y.Importance)); + static void InsertIssue(ShipIssueWorker issue, List list) + { + int index = 0; + while (index < list.Count && list[index].Importance > issue.Importance) + { + index++; + } + list.Insert(index, issue); + } ShipIssueWorker mostImportantIssue = availableIssues.FirstOrDefault(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs index 7b2abeada..bbd554491 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs @@ -13,6 +13,7 @@ namespace Barotrauma private readonly float minDistFromClosest; private readonly float maxDistFromCenter; private readonly float cohesion; + public bool ForceActive { get; private set; } public List Members { get; private set; } = new List(); public HashSet ActiveMembers { get; private set; } = new HashSet(); @@ -26,9 +27,10 @@ namespace Barotrauma public SwarmBehavior(XElement element, EnemyAIController ai) { this.ai = ai; - minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat("mindistfromclosest", 10.0f)); - maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat("maxdistfromcenter", 1000.0f)); - cohesion = element.GetAttributeFloat("cohesion", 1) / 10; + minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat(nameof(minDistFromClosest), 10.0f)); + maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat(nameof(maxDistFromCenter), 1000.0f)); + cohesion = element.GetAttributeFloat(nameof(cohesion), 1) / 10; + ForceActive = element.GetAttributeBool(nameof(ForceActive), false); } public static void CreateSwarm(IEnumerable swarm) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index b50108c6d..34092a282 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -8,16 +8,90 @@ using System; namespace Barotrauma { - partial class WreckAI : IServerSerializable + internal class SubmarineTurretAI { - public Submarine Wreck { get; private set; } + public Submarine Submarine { get; protected set; } + protected readonly List turrets = new List(); + public Identifier FriendlyTag; + public SubmarineTurretAI(Submarine submarine, Identifier friendlyTag = default) + { + FriendlyTag = friendlyTag; + Submarine = submarine; + foreach (Item item in Item.ItemList) + { + if (item.Submarine != Submarine) { continue; } + var turret = item.GetComponent(); + if (turret != null) + { + turrets.Add(turret); + // Set false, because we manage the turrets in the Update method. + turret.AutoOperate = false; + // Set to full condition, because items don't work when they are broken. + turret.Item.Condition = turret.Item.MaxCondition; + foreach (MapEntity linkedEntity in turret.Item.linkedTo) + { + if (linkedEntity is Item linkedItem) + { + linkedItem.Condition = linkedItem.MaxCondition; + } + } + } + } + LoadAllTurrets(); + } + + public virtual void Update(float deltaTime) + { + if (Submarine == null || Submarine.Removed) { return; } + OperateTurrets(deltaTime, FriendlyTag); + } + + protected virtual void LoadAllTurrets() + { + foreach (var turret in turrets) + { + LoadTurret(turret); + } + } + + protected void LoadTurret(Turret turret, Func ammoFilter = null) + { + foreach (var linkedItem in turret.Item.GetLinkedEntities()) + { + var container = linkedItem.GetComponent(); + if (container == null) { continue; } + for (int i = 0; i < container.Inventory.Capacity; i++) + { + if (container.Inventory.GetItemAt(i) != null) { continue; } + if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && (ammoFilter == null || ammoFilter(ip)), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab) + { + Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Submarine); + if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) + { + turret.Item.Remove(); + } + } + } + } + } + + protected void OperateTurrets(float deltaTime, Identifier friendlyTag) + { + foreach (var turret in turrets) + { + turret.UpdateAutoOperate(deltaTime, friendlyTag); + } + } + } + + partial class WreckAI : SubmarineTurretAI, IServerSerializable + { public bool IsAlive { get; private set; } private readonly List allItems; private readonly List thalamusItems; private readonly List thalamusStructures; - private readonly List turrets = new List(); private readonly List wayPoints = new List(); private readonly List hulls = new List(); private readonly List spawnOrgans = new List(); @@ -25,7 +99,7 @@ namespace Barotrauma private bool initialCellsSpawned; - public readonly WreckAIConfig Config; + public WreckAIConfig Config { get; private set; } private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; @@ -44,15 +118,10 @@ namespace Barotrauma return wreckAI; } - private WreckAI(Submarine wreck) + private WreckAI(Submarine wreck) : base(wreck) { - Wreck = wreck; - Config = WreckAIConfig.GetRandom(); - if (Config == null) - { - DebugConsole.ThrowError("WreckAI: No wreck AI config found!"); - return; - } + GetConfig(); + if (Config == null) { return; } var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p)); var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient); if (brainPrefab == null) @@ -60,20 +129,20 @@ namespace Barotrauma DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI."); return; } - allItems = Wreck.GetItems(false); + allItems = wreck.GetItems(false); thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab)); - hulls.AddRange(Wreck.GetHulls(false)); + hulls.AddRange(wreck.GetHulls(false)); var potentialBrainHulls = new List<(Hull hull, float weight)>(); - brain = new Item(brainPrefab, Vector2.Zero, Wreck); + brain = new Item(brainPrefab, Vector2.Zero, wreck); thalamusItems.Add(brain); Point minSize = brain.Rect.Size.Multiply(brain.Scale); // Bigger hulls are allowed, but not preferred more than what's sufficent. Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f); // Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls. - Rectangle shrinkedBounds = ToolBox.GetWorldBounds(Wreck.WorldPosition.ToPoint(), new Point(Wreck.Borders.Width - 500, Wreck.Borders.Height)); + Rectangle shrinkedBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width - 500, wreck.Borders.Height)); foreach (Hull hull in hulls) { - float distanceFromCenter = Vector2.Distance(Wreck.WorldPosition, hull.WorldPosition); + float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition); float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter)); float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width)); float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height)); @@ -121,7 +190,7 @@ namespace Barotrauma var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient); if (backgroundPrefab != null) { - new Structure(brainHull.Rect, backgroundPrefab, Wreck); + new Structure(brainHull.Rect, backgroundPrefab, wreck); } var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient); if (horizontalWallPrefab != null) @@ -129,8 +198,8 @@ namespace Barotrauma int height = (int)horizontalWallPrefab.Size.Y; int halfHeight = height / 2; int quarterHeight = halfHeight / 2; - new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); - new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck); + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck); } var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient); if (verticalWallPrefab != null) @@ -138,50 +207,13 @@ namespace Barotrauma int width = (int)verticalWallPrefab.Size.X; int halfWidth = width / 2; int quarterWidth = halfWidth / 2; - new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck); - new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck); + new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck); + new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck); } - foreach (Item item in allItems) + foreach (Item item in thalamusItems) { - if (thalamusItems.Contains(item)) - { - // Ensure that thalamus items are visible - item.HiddenInGame = false; - } - else - { - // Load regular turrets - var turret = item.GetComponent(); - if (turret != null) - { - foreach (var linkedItem in item.GetLinkedEntities()) - { - var container = linkedItem.GetComponent(); - if (container == null) { continue; } - for (int i = 0; i < container.Inventory.Capacity; i++) - { - if (container.Inventory.GetItemAt(i) != null) { continue; } - if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && - Config.ForbiddenAmmunition.None(id => id == ip.Identifier), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab) - { - Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck); - if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) - { - item.Remove(); - } - } - } - } - } - } - } - foreach (var item in allItems) - { - var turret = item.GetComponent(); - if (turret != null) - { - turrets.Add(turret); - } + // Ensure that thalamus items are visible + item.HiddenInGame = false; if (item.HasTag(Config.Spawner)) { if (!spawnOrgans.Contains(item)) @@ -195,16 +227,34 @@ namespace Barotrauma } } } - wayPoints.AddRange(Wreck.GetWaypoints(false)); + wayPoints.AddRange(wreck.GetWaypoints(false)); IsAlive = true; - thalamusStructures = GetThalamusEntities(Wreck, Config.Entity).ToList(); + thalamusStructures = GetThalamusEntities(wreck, Config.Entity).ToList(); + } + + private void GetConfig() + { + Config ??= WreckAIConfig.GetRandom(); + if (Config == null) + { + DebugConsole.ThrowError("WreckAI: No wreck AI config found!"); + } + } + + protected override void LoadAllTurrets() + { + GetConfig(); + foreach (var turret in turrets) + { + LoadTurret(turret, ip => Config.ForbiddenAmmunition.None(id => id == ip.Identifier)); + } } private readonly List destroyedOrgans = new List(); - public void Update(float deltaTime) + public override void Update(float deltaTime) { if (!IsAlive) { return; } - if (Wreck == null || Wreck.Removed) + if (Submarine == null || Submarine.Removed) { Remove(); return; @@ -223,34 +273,60 @@ namespace Barotrauma } } destroyedOrgans.ForEach(o => spawnOrgans.Remove(o)); - bool someoneNearby = false; + bool isSomeoneNearby = false; float minDist = Sonar.DefaultSonarRange * 2.0f; - foreach (Submarine submarine in Submarine.Loaded) +#if SERVER + foreach (var client in GameMain.Server.ConnectedClients) { - if (submarine.Info.Type != SubmarineType.Player) { continue; } - if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + var spectatePos = client.SpectatePos; + if (spectatePos.HasValue) { - someoneNearby = true; - break; + if (IsCloseEnough(spectatePos.Value, minDist)) + { + isSomeoneNearby = true; + break; + } } } - foreach (Character c in Character.CharacterList) +#else + if (IsCloseEnough(GameMain.GameScreen.Cam.Position, minDist)) { - if (c != Character.Controlled && !c.IsRemotePlayer) { continue; } - if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + isSomeoneNearby = true; + } +#endif + if (!isSomeoneNearby) + { + foreach (Submarine submarine in Submarine.Loaded) { - someoneNearby = true; - break; + if (submarine.Info.Type != SubmarineType.Player) { continue; } + if (IsCloseEnough(submarine.WorldPosition, minDist)) + { + isSomeoneNearby = true; + break; + } } } - if (!someoneNearby) { return; } - OperateTurrets(deltaTime); + if (!isSomeoneNearby) + { + foreach (Character c in Character.CharacterList) + { + if (!c.IsPlayer && !c.IsOnPlayerTeam) { continue; } + if (IsCloseEnough(c.WorldPosition, minDist)) + { + isSomeoneNearby = true; + break; + } + } + } + if (!isSomeoneNearby) { return; } + OperateTurrets(deltaTime, Config.Entity); if (!IsClient) { if (!initialCellsSpawned) { SpawnInitialCells(); } UpdateReinforcements(deltaTime); } } + private bool IsCloseEnough(Vector2 targetPos, float minDist) => Vector2.DistanceSquared(targetPos, Submarine.WorldPosition) < minDist * minDist; private void SpawnInitialCells() { @@ -287,7 +363,7 @@ namespace Barotrauma // Snap all tendons foreach (Item item in turret.ActiveProjectiles) { - if (item.GetComponent()?.IsStuckToTarget ?? false) + if (item.GetComponent() is { IsStuckToTarget: true }) { item.Condition = 0; } @@ -314,7 +390,7 @@ namespace Barotrauma { // Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this. float maxDistance = Sonar.DefaultSonarRange; - if (Vector2.DistanceSquared(character.WorldPosition, Wreck.WorldPosition) < maxDistance * maxDistance) + if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance) { character.Kill(CauseOfDeathType.Unknown, null); } @@ -333,7 +409,7 @@ namespace Barotrauma public void Remove() { Kill(); - RemoveThalamusItems(Wreck); + RemoveThalamusItems(Submarine); thalamusItems?.Clear(); thalamusStructures?.Clear(); } @@ -387,7 +463,7 @@ namespace Barotrauma return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t)); } - void UpdateReinforcements(float deltaTime) + private void UpdateReinforcements(float deltaTime) { if (spawnOrgans.Count == 0) { return; } cellSpawnTimer -= deltaTime; @@ -398,7 +474,7 @@ namespace Barotrauma } } - bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null) + private bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null) { cell = null; if (protectiveCells.Count >= MaxCellCount) { return false; } @@ -424,19 +500,6 @@ namespace Barotrauma cellSpawnTimer = GetSpawnTime(); return true; } - - void OperateTurrets(float deltaTime) - { - foreach (var turret in turrets) - { - // Never target other creatures than humans with the turrets. - turret.ThalamusOperate(this, deltaTime, - !turret.Item.HasTag("ignorecharacters"), - targetOtherCreatures: false, - !turret.Item.HasTag("ignoresubmarines"), - turret.Item.HasTag("ignoreaimdelay")); - } - } void OnCellDeath(Character character, CauseOfDeath causeOfDeath) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 140aa7c04..1606de078 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -541,11 +541,11 @@ namespace Barotrauma float wobbleStrength = 0.0f; if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem) { - wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage"); + wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: AfflictionPrefab.DamageType); } if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem) { - wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage"); + wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: AfflictionPrefab.DamageType); } if (wobbleStrength <= 0.1f) { return 0.0f; } wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 4e3da61fa..6d726f848 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -135,8 +135,14 @@ namespace Barotrauma public override void UpdateAnim(float deltaTime) { - if (Frozen) return; - if (MainLimb == null) { return; } + //wait a bit for the ragdoll to "settle" (for joints to force the limbs to appropriate positions) before starting to animate + if (Timing.TotalTime - character.SpawnTime < 0.1f) { return; } + if (Frozen) { return; } + if (MainLimb == null) + { + ResetState(); + return; + } var mainLimb = MainLimb; levitatingCollider = !IsHanging; @@ -164,6 +170,7 @@ namespace Barotrauma //cannot walk but on dry land -> wiggle around UpdateDying(deltaTime); } + ResetState(); return; } else @@ -176,11 +183,17 @@ namespace Barotrauma { var lowestLimb = FindLowestLimb(); - Collider.SetTransform(new Vector2( - Collider.SimPosition.X, - Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)), - 0.0f); - + if (InWater) + { + Collider.SetTransform(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f); + } + else + { + Collider.SetTransform(new Vector2( + Collider.SimPosition.X, + Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)), + 0.0f); + } Collider.Enabled = true; } @@ -223,6 +236,7 @@ namespace Barotrauma if (character.SelectedCharacter != null) { DragCharacter(character.SelectedCharacter, deltaTime); + ResetState(); return; } if (character.AnimController.AnimationTestPose) @@ -230,7 +244,11 @@ namespace Barotrauma ApplyTestPose(); } //don't flip when simply physics is enabled - if (SimplePhysicsEnabled) { return; } + if (SimplePhysicsEnabled) + { + ResetState(); + return; + } if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !Aiming) { @@ -264,43 +282,47 @@ namespace Barotrauma } } - if (!CurrentFishAnimation.Flip) { return; } - if (IsStuck) { return; } - if (character.AIController != null && !character.AIController.CanFlip) { return; } - - flipCooldown -= deltaTime; - if (TargetDir != Direction.None && TargetDir != dir) + if (!IsStuck && CurrentFishAnimation.Flip && character.AIController is not { CanFlip: false }) { - flipTimer += deltaTime; - // Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example). - float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2; - if (CurrentHull != null) + flipCooldown -= deltaTime; + if (TargetDir != Direction.None && TargetDir != dir) { - // Enemy movement speeds are halved inside submarines - requiredSpeed /= 2; - } - bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed; - bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y); - if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards)) - || character.IsRemotePlayer) - { - Flip(); - if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) + flipTimer += deltaTime; + // Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example). + float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2; + if (CurrentHull != null) { - Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true); + // Enemy movement speeds are halved inside submarines + requiredSpeed /= 2; } + bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed; + bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y); + if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards)) + || character.IsRemotePlayer) + { + Flip(); + if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) + { + Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true); + } + flipTimer = 0.0f; + flipCooldown = CurrentFishAnimation.FlipCooldown; + } + } + else + { flipTimer = 0.0f; - flipCooldown = CurrentFishAnimation.FlipCooldown; } } - else + ResetState(); + + void ResetState() { - flipTimer = 0.0f; + wasAiming = aiming; + aiming = false; + wasAimingMelee = aimingMelee; + aimingMelee = false; } - wasAiming = aiming; - aiming = false; - wasAimingMelee = aimingMelee; - aimingMelee = false; } private bool CanDrag(Character target) @@ -458,24 +480,34 @@ namespace Barotrauma t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); } } - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); + if (Collider.BodyType == BodyType.Dynamic) + { + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); + } //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } mainLimb.PullJointEnabled = true; - if (aiming && movement.Length() <= 0.1f) - { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); - Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir; - TargetMovement = new Vector2(0.0f, -0.1f); - float newRotation = MathUtils.VectorToAngle(diff); - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); - } - - if (!isMoving) + if (!isMoving && !CurrentSwimParams.UpdateAnimationWhenNotMoving) { WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5); mainLimb.PullJointWorldAnchorB = Collider.SimPosition; + if (aiming) + { + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir; + TargetMovement = new Vector2(0.0f, -0.1f); + float newRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver2 * Dir); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier * 2); + if (TorsoAngle.HasValue) + { + Limb torso = GetLimb(LimbType.Torso); + if (torso != null) + { + SmoothRotateWithoutWrapping(torso, newRotation + TorsoAngle.Value * Dir, mainLimb, TorsoTorque * 2); + } + } + } } else { @@ -688,9 +720,12 @@ namespace Barotrauma { movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f); - Collider.LinearVelocity = new Vector2( - movement.X, - Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y); + if (Collider.BodyType == BodyType.Dynamic) + { + Collider.LinearVelocity = new Vector2( + movement.X, + Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y); + } //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index bb4f57c72..f78151840 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -170,7 +170,7 @@ namespace Barotrauma { get { - float shoulderHeight = Collider.height / 2.0f; + float shoulderHeight = Collider.Height / 2.0f; if (inWater) { shoulderHeight += 0.4f; @@ -308,7 +308,7 @@ namespace Barotrauma Collider.SetTransform(new Vector2( Collider.SimPosition.X, - Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)), + Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)), Collider.Rotation); Collider.FarseerBody.ResetDynamics(); @@ -459,7 +459,8 @@ namespace Barotrauma void UpdateStanding() { - if (CurrentGroundedParams == null) { return; } + var currentGroundedParams = CurrentGroundedParams; + if (currentGroundedParams == null) { return; } Vector2 handPos; Limb leftFoot = GetLimb(LimbType.LeftFoot); @@ -482,7 +483,7 @@ namespace Barotrauma walkCycleMultiplier *= 1.5f; } - float getUpForce = CurrentGroundedParams.GetUpForce / RagdollParams.JointScale; + float getUpForce = currentGroundedParams.GetUpForce / RagdollParams.JointScale; Vector2 colliderPos = GetColliderBottom(); if (Math.Abs(TargetMovement.X) > 1.0f) @@ -583,7 +584,7 @@ namespace Barotrauma } float stepLift = TargetMovement.X == 0.0f ? 0 : - (float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100); + (float)Math.Sin(WalkPos * currentGroundedParams.StepLiftFrequency + MathHelper.Pi * currentGroundedParams.StepLiftOffset) * (currentGroundedParams.StepLiftAmount / 100); float y = colliderPos.Y + stepLift; @@ -598,7 +599,7 @@ namespace Barotrauma if (!head.Disabled) { - y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier; + y = colliderPos.Y + stepLift * currentGroundedParams.StepLiftHeadMultiplier; if (HeadPosition.HasValue) { y += HeadPosition.Value; } if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; } head.PullJointWorldAnchorB = @@ -615,18 +616,18 @@ namespace Barotrauma if (TorsoAngle.HasValue && !torso.Disabled) { float torsoAngle = TorsoAngle.Value; - float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); if (Crouching && !movingHorizontally && !Aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; - torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); + torso.body.SmoothRotate(torsoAngle * Dir, currentGroundedParams.TorsoTorque); } if (!head.Disabled) { - if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) + if (!Aiming && currentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) { float headAngle = HeadAngle.Value; if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } - head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); + head.body.SmoothRotate(headAngle * Dir, currentGroundedParams.HeadTorque); } else { @@ -665,16 +666,16 @@ namespace Barotrauma if (footPos.Y < 0.0f) { footPos.Y = -0.15f; } //make the character limp if the feet are damaged - float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength("damage", foot, true); + float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.DamageType, foot, true); footPos.X *= MathHelper.Lerp(1.0f, 0.75f, MathHelper.Clamp(footAfflictionStrength / 50.0f, 0.0f, 1.0f)); - if (CurrentGroundedParams.FootLiftHorizontalFactor > 0) + if (currentGroundedParams.FootLiftHorizontalFactor > 0) { // Calculate the foot y dynamically based on the foot position relative to the waist, // so that the foot aims higher when it's behind the waist and lower when it's in the front. float xDiff = (foot.SimPosition.X - waistPos.X + FootMoveOffset.X) * Dir; - float min = MathUtils.InverseLerp(1, 0, CurrentGroundedParams.FootLiftHorizontalFactor); - float max = 1 + MathUtils.InverseLerp(0, 1, CurrentGroundedParams.FootLiftHorizontalFactor); + float min = MathUtils.InverseLerp(1, 0, currentGroundedParams.FootLiftHorizontalFactor); + float max = 1 + MathUtils.InverseLerp(0, 1, currentGroundedParams.FootLiftHorizontalFactor); float xFactor = MathHelper.Lerp(min, max, MathUtils.InverseLerp(RagdollParams.JointScale, -RagdollParams.JointScale, xDiff)); footPos.Y *= xFactor; } @@ -698,19 +699,19 @@ namespace Barotrauma { foot.DebugRefPos = colliderPos; foot.DebugTargetPos = colliderPos + footPos; - MoveLimb(foot, colliderPos + footPos, CurrentGroundedParams.FootMoveStrength); + MoveLimb(foot, colliderPos + footPos, currentGroundedParams.FootMoveStrength); FootIK(foot, colliderPos + footPos, - CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); + currentGroundedParams.LegBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians); } } //calculate the positions of hands handPos = torso.SimPosition; - handPos.X = -walkPosX * CurrentGroundedParams.HandMoveAmount.X; + handPos.X = -walkPosX * currentGroundedParams.HandMoveAmount.X; - float lowerY = CurrentGroundedParams.HandClampY; + float lowerY = currentGroundedParams.HandClampY; - handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * CurrentGroundedParams.HandMoveAmount.Y)); + handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * currentGroundedParams.HandMoveAmount.Y)); Vector2 posAddition = new Vector2(Math.Sign(movement.X) * HandMoveOffset.X, HandMoveOffset.Y); @@ -718,13 +719,13 @@ namespace Barotrauma { HandIK(rightHand, torso.SimPosition + posAddition + new Vector2(-handPos.X, (Math.Sign(walkPosX) == Math.Sign(Dir)) ? handPos.Y : lowerY), - CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); + currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength); } if (leftHand != null && !leftHand.Disabled) { HandIK(leftHand, torso.SimPosition + posAddition + new Vector2(handPos.X, (Math.Sign(walkPosX) == Math.Sign(-Dir)) ? handPos.Y : lowerY), - CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); + currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength); } } else @@ -755,8 +756,8 @@ namespace Barotrauma { foot.DebugRefPos = colliderPos; foot.DebugTargetPos = footPos; - float footMoveForce = CurrentGroundedParams.FootMoveStrength; - float legBendTorque = CurrentGroundedParams.LegBendTorque; + float footMoveForce = currentGroundedParams.FootMoveStrength; + float legBendTorque = currentGroundedParams.LegBendTorque; if (Crouching) { // Keeps the pose @@ -764,7 +765,7 @@ namespace Barotrauma footMoveForce *= 2; } MoveLimb(foot, footPos, footMoveForce); - FootIK(foot, footPos, legBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); + FootIK(foot, footPos, legBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians); } } @@ -780,7 +781,7 @@ namespace Barotrauma var arm = GetLimb(armType); if (arm != null && Math.Abs(arm.body.AngularVelocity) < 10.0f) { - arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * CurrentGroundedParams.ArmMoveStrength); + arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * currentGroundedParams.ArmMoveStrength); } //get the elbow to a neutral rotation @@ -791,14 +792,14 @@ namespace Barotrauma if (elbow != null) { float diff = elbow.JointAngle - (Dir > 0 ? elbow.LowerLimit : elbow.UpperLimit); - forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * CurrentGroundedParams.ArmMoveStrength); + forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * currentGroundedParams.ArmMoveStrength); } } // Try to keep the wrist straight LimbJoint wrist = GetJointBetweenLimbs(foreArmType, hand.type); if (wrist != null) { - hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * CurrentGroundedParams.HandMoveStrength); + hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * currentGroundedParams.HandMoveStrength); } } } @@ -840,14 +841,11 @@ namespace Barotrauma if (head == null) { return; } if (torso == null) { return; } - const float DisableMovementAboveSurfaceThreshold = 50.0f; - if (currentHull != null && character.CurrentHull != null) { float surfacePos = GetSurfaceY(); float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); surfaceLimiter = Math.Max(1.0f, surfaceThreshold - surfacePos); - if (surfaceLimiter > DisableMovementAboveSurfaceThreshold) { return; } } Limb leftHand = GetLimb(LimbType.LeftHand); @@ -917,6 +915,7 @@ namespace Barotrauma RotateHead(head); } + const float DisableMovementAboveSurfaceThreshold = 50.0f; //dont try to move upwards if head is already out of water if (surfaceLimiter > 1.0f && TargetMovement.Y > 0.0f) { @@ -936,8 +935,8 @@ namespace Barotrauma //turn head above the water head.body.ApplyTorque(Dir); } + movement.Y *= Math.Max(0, 1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold)); - movement.Y = movement.Y * (1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold)); } bool isNotRemote = true; @@ -956,7 +955,13 @@ namespace Barotrauma t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); } } - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); + Vector2 targetVelocity = movement; + //if we're too high above the surface, don't touch the vertical velocity of the collider unless we're heading down + if (surfaceLimiter > DisableMovementAboveSurfaceThreshold) + { + targetVelocity.Y = Math.Min(Collider.LinearVelocity.Y, movement.Y); + }; + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, targetVelocity, t); } WalkPos += movement.Length(); @@ -1130,7 +1135,7 @@ namespace Barotrauma ladderSimPos -= currentHull.Submarine.SimPosition; } - float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.radius - Collider.height / 2.0f; + 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; @@ -1225,7 +1230,7 @@ namespace Barotrauma 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; + float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.Height; if (floorFixture != null && !floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) && !floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) && @@ -1524,13 +1529,15 @@ namespace Barotrauma Limb leftHand = GetLimb(LimbType.LeftHand); Limb rightHand = GetLimb(LimbType.RightHand); - Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm); - if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); } - if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; } + Limb targetLeftHand = + target.AnimController.GetLimb(LimbType.LeftForearm) ?? + target.AnimController.GetLimb(LimbType.Torso) ?? + target.AnimController.MainLimb; - Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm); - if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); } - if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; } + Limb targetRightHand = + target.AnimController.GetLimb(LimbType.RightForearm) ?? + target.AnimController.GetLimb(LimbType.Torso) ?? + target.AnimController.MainLimb; if (!target.AllowInput) { @@ -1546,10 +1553,7 @@ namespace Barotrauma return; } Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso); - if (targetTorso == null) - { - targetTorso = target.AnimController.MainLimb; - } + targetTorso ??= target.AnimController.MainLimb; if (target.AnimController.Dir != Dir) { target.AnimController.Flip(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index b5e116dab..cc28d59c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -173,18 +173,18 @@ namespace Barotrauma if (value == colliderIndex || collider == null) { return; } if (value >= collider.Count || value < 0) { return; } - if (collider[colliderIndex].height < collider[value].height) + if (collider[colliderIndex].Height < collider[value].Height) { Vector2 pos1 = collider[colliderIndex].SimPosition; - pos1.Y -= collider[colliderIndex].height * ColliderHeightFromFloor; + pos1.Y -= collider[colliderIndex].Height * ColliderHeightFromFloor; Vector2 pos2 = pos1; - pos2.Y += collider[value].height * 1.1f; + pos2.Y += collider[value].Height * 1.1f; if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall) && !(f.Body.UserData is Submarine))) { return; } } Vector2 pos = collider[colliderIndex].SimPosition; - pos.Y -= collider[colliderIndex].height * 0.5f; - pos.Y += collider[value].height * 0.5f; + pos.Y -= collider[colliderIndex].Height * 0.5f; + pos.Y += collider[value].Height * 0.5f; collider[value].SetTransform(pos, collider[colliderIndex].Rotation); collider[value].LinearVelocity = collider[colliderIndex].LinearVelocity; @@ -575,6 +575,10 @@ namespace Barotrauma protected void AddLimb(LimbParams limbParams) { + if (limbParams.ID < 0 || limbParams.ID > 255) + { + throw new Exception($"Invalid limb params in limb \"{limbParams.Type}\". \"{limbParams.ID}\" is not a valid limb ID."); + } byte ID = Convert.ToByte(limbParams.ID); Limb limb = new Limb(this, character, limbParams); limb.body.FarseerBody.OnCollision += OnLimbCollision; @@ -680,6 +684,10 @@ namespace Barotrauma } return true; } + else if (character.Submarine != null && structure.Submarine != null && character.Submarine != structure.Submarine) + { + return false; + } Vector2 colliderBottom = GetColliderBottom(); if (structure.IsPlatform) @@ -1189,7 +1197,7 @@ namespace Barotrauma { inWater = false; headInWater = false; - RefreshFloorY(ignoreStairs: Stairs == null); + RefreshFloorY(deltaTime, ignoreStairs: Stairs == null); } //ragdoll isn't in any room -> it's in the water else if (currentHull == null) @@ -1201,10 +1209,12 @@ namespace Barotrauma { headInWater = false; inWater = false; - RefreshFloorY(ignoreStairs: Stairs == null); + RefreshFloorY(deltaTime, ignoreStairs: Stairs == null); if (currentHull.WaterPercentage > 0.001f) { - float waterSurface = ConvertUnits.ToSimUnits(GetSurfaceY()); + (float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY(); + float waterSurfaceY = ConvertUnits.ToSimUnits(waterSurfaceDisplayUnits); + float ceilingY = ConvertUnits.ToSimUnits(ceilingDisplayUnits); if (targetMovement.Y < 0.0f) { Vector2 colliderBottom = GetColliderBottom(); @@ -1214,13 +1224,21 @@ namespace Barotrauma { //set floorY to the position of the floor in the hull below the character var lowerHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(colliderBottom), useWorldCoordinates: false); - if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); + if (lowerHull != null) + { + floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); + } } } float standHeight = HeadPosition ?? TorsoPosition ?? Collider.GetMaxExtent() * 0.5f; - if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.8f) + if (Collider.SimPosition.Y < waterSurfaceY) { - inWater = true; + //too deep to stand up, or not enough room to stand up + if (waterSurfaceY - floorY > standHeight * 0.8f || + ceilingY - floorY < standHeight * 0.8f) + { + inWater = true; + } } } } @@ -1281,21 +1299,32 @@ namespace Barotrauma limb.Update(deltaTime); } - if (!inWater && character.AllowInput && levitatingCollider && Collider.LinearVelocity.Y > -ImpactTolerance && onGround) + if (!inWater && character.AllowInput && levitatingCollider) { - float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.height * 0.5f) + Collider.radius + ColliderHeightFromFloor; - if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f && onGround) + if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance) { - if (Stairs != null) + float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor; + if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f) { - Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, - (targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f); + if (Stairs != null) + { + Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, + (targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f); + } + else + { + Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f); + } } - else + } + else + { + // Falling -> ragdoll briefly if we are not moving at all, because we are probably stuck. + if (Collider.LinearVelocity == Vector2.Zero) { - Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f); + character.IsRagdolled = true; } - } + } } UpdateProjSpecific(deltaTime, cam); forceNotStanding = false; @@ -1525,15 +1554,24 @@ namespace Barotrauma lastFloorCheckPos = Vector2.Zero; } - private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false) + // Force check floor y at least once a second so that we'll drop through gaps that we are standing upon. + private const float FloorYStaleTime = 1; + private float floorYCheckTimer; + private void RefreshFloorY(float deltaTime, Limb refLimb = null, bool ignoreStairs = false) { + floorYCheckTimer -= deltaTime; PhysicsBody refBody = refLimb == null ? Collider : refLimb.body; - if (Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f || lastFloorCheckIgnoreStairs != ignoreStairs || lastFloorCheckIgnorePlatforms != IgnorePlatforms) + if (floorYCheckTimer < 0 || + lastFloorCheckIgnoreStairs != ignoreStairs || + lastFloorCheckIgnorePlatforms != IgnorePlatforms || + Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f) { floorY = GetFloorY(refBody.SimPosition, ignoreStairs); lastFloorCheckPos = refBody.SimPosition; lastFloorCheckIgnoreStairs = ignoreStairs; lastFloorCheckIgnorePlatforms = IgnorePlatforms; + // Add some randomness to prevent all stationary characters doing the checks at the same frame. + floorYCheckTimer = FloorYStaleTime * Rand.Range(0.9f, 1.1f); } } @@ -1616,7 +1654,7 @@ namespace Barotrauma { floorFixture = standOnFloorFixture; standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction; - if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f) + if (rayStart.Y - standOnFloorY < Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor * 1.2f) { onGround = true; if (standOnFloorFixture.CollisionCategories == Physics.CollisionStairs) @@ -1655,22 +1693,34 @@ namespace Barotrauma } } + /// + /// Get the position of the surface of water at the position of the character, in display units (taking into account connected hulls above the hull the character is in) + /// public float GetSurfaceY() + { + return GetWaterSurfaceAndCeilingY().WaterSurfaceY; + } + + /// + /// Get the position of the surface of water and the ceiling (= upper edge of the hull) at the position of the character, in display units (taking into account connected hulls above the hull the character is in). + /// + private (float WaterSurfaceY, float CeilingY) GetWaterSurfaceAndCeilingY() { //check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside if (currentHull == null || character.CurrentHull == null) { - return float.PositiveInfinity; + return (float.PositiveInfinity, float.PositiveInfinity); } - - float surfacePos = currentHull.Surface; + + float surfaceY = currentHull.Surface; + float ceilingY = currentHull.Rect.Y; float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); //if the hull is almost full of water, check if there's a water-filled hull above it //and use its water surface instead of the current hull's if (currentHull.Rect.Y - currentHull.Surface < 5.0f) - { - GetSurfacePos(currentHull, ref surfacePos); - void GetSurfacePos(Hull hull, ref float prevSurfacePos) + { + GetSurfacePos(currentHull, ref surfaceY, ref ceilingY); + void GetSurfacePos(Hull hull, ref float prevSurfacePos, ref float ceilingPos) { if (prevSurfacePos > surfaceThreshold) { return; } foreach (Gap gap in hull.ConnectedGaps) @@ -1681,6 +1731,7 @@ namespace Barotrauma //if the gap is above us and leads outside, there's no surface to limit the movement if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y) { + ceilingPos += 100000.0f; prevSurfacePos += 100000.0f; return; } @@ -1689,15 +1740,16 @@ namespace Barotrauma { if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull) { - prevSurfacePos = Math.Max(surfacePos, otherHull.Surface); - GetSurfacePos(otherHull, ref prevSurfacePos); + prevSurfacePos = Math.Max(surfaceY, otherHull.Surface); + ceilingPos = Math.Max(ceilingPos, otherHull.Rect.Y); + GetSurfacePos(otherHull, ref prevSurfacePos, ref ceilingPos); break; } } } } } - return surfacePos; + return (surfaceY, ceilingY); } public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true) @@ -1803,22 +1855,36 @@ namespace Barotrauma private bool collisionsDisabled; + private double lastObstacleRayCastTime; protected void CheckDistFromCollider() { - float allowedDist = Math.Max(Math.Max(Collider.radius, Collider.width), Collider.height) * 2.0f; + float allowedDist = Math.Max(Math.Max(Collider.Radius, Collider.Width), Collider.Height) * 2.0f; allowedDist = Math.Max(allowedDist, 1.0f); float resetDist = allowedDist * 5.0f; + float obstacleCheckDist = 0.3f; + Vector2 diff = Collider.SimPosition - MainLimb.SimPosition; float distSqrd = diff.LengthSquared(); - if (distSqrd > resetDist * resetDist) + bool shouldReset = distSqrd > resetDist * resetDist; + if (!shouldReset && distSqrd > obstacleCheckDist * obstacleCheckDist) + { + if (Timing.TotalTime > lastObstacleRayCastTime + 1 && + Submarine.PickBody(Collider.SimPosition, MainLimb.SimPosition, collisionCategory: Physics.CollisionWall) != null) + { + shouldReset = true; + lastObstacleRayCastTime = Timing.TotalTime; + } + } + + if (shouldReset) { //ragdoll way too far, reset position SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true); } - if (distSqrd > allowedDist * allowedDist) + else if (distSqrd > allowedDist * allowedDist) { //ragdoll too far from the collider, disable collisions until it's close enough //(in case the ragdoll has gotten stuck somewhere) @@ -1840,7 +1906,7 @@ namespace Barotrauma collisionsDisabled = false; //force collision categories to be updated prevCollisionCategory = Category.None; - } + } } partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 702512ca4..2a9aa8211 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -181,13 +181,13 @@ namespace Barotrauma [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float LevelWallDamage { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool Ranged { get; set; } - [Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks.")] + [Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks."), Editable] public bool AvoidFriendlyFire { get; set; } - [Serialize(20f, IsPropertySaveable.Yes)] + [Serialize(20f, IsPropertySaveable.Yes, description: "Only affects ranged attacks."), Editable] public float RequiredAngle { get; set; } [Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable] @@ -199,6 +199,12 @@ namespace Barotrauma [Serialize(-1, IsPropertySaveable.Yes, description: "Reference to the limb we apply the aim rotation to. By default same as the attack limb. Only affects ranged attacks."), Editable] public int RotationLimbIndex { get; set; } + [Serialize(0f, IsPropertySaveable.Yes, description:"How much the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items). Default 0 means the weapon is not swayed at all."), Editable] + public float SwayAmount { get; set; } + + [Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable] + public float SwayFrequency { get; set; } + /// /// Legacy support. Use Afflictions. /// @@ -337,9 +343,10 @@ namespace Barotrauma return (Duration == 0.0f) ? LevelWallDamage : LevelWallDamage * deltaTime; } - public float GetItemDamage(float deltaTime) + public float GetItemDamage(float deltaTime, float multiplier = 1) { - return (Duration == 0.0f) ? ItemDamage : ItemDamage * deltaTime; + float dmg = ItemDamage * multiplier; + return (Duration == 0.0f) ? dmg : dmg * deltaTime; } public float GetTotalDamage(bool includeStructureDamage = false) @@ -421,13 +428,7 @@ namespace Barotrauma } break; case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - Conditionals.Add(new PropertyConditional(attribute)); - } - } + Conditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 5ee296c48..a03315f66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -136,6 +136,12 @@ namespace Barotrauma public bool IsEscorted { get; set; } public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty; + public bool DoesBleed + { + get => Params.Health.DoesBleed; + set => Params.Health.DoesBleed = value; + } + public readonly Dictionary Properties; public Dictionary SerializableProperties { @@ -173,6 +179,13 @@ namespace Barotrauma } } + private Identifier? faction; + public Identifier Faction + { + get { return faction ?? HumanPrefab?.Faction ?? Identifier.Empty; } + set { faction = value; } + } + private CharacterTeamType teamID; public CharacterTeamType TeamID { @@ -184,6 +197,13 @@ namespace Barotrauma } } + + private CharacterTeamType? originalTeamID; + public CharacterTeamType OriginalTeamID + { + get { return originalTeamID ?? teamID; } + } + private Wallet wallet; public Wallet Wallet @@ -205,7 +225,7 @@ namespace Barotrauma protected readonly Dictionary activeTeamChanges = new Dictionary(); protected ActiveTeamChange currentTeamChange; - const string OriginalTeamIdentifier = "original"; + private const string OriginalChangeTeamIdentifier = "original"; private void ThrowIfAccessingWalletsInSingleplayer() { @@ -220,20 +240,16 @@ namespace Barotrauma public void SetOriginalTeam(CharacterTeamType newTeam) { - TryRemoveTeamChange(OriginalTeamIdentifier); + TryRemoveTeamChange(OriginalChangeTeamIdentifier); currentTeamChange = new ActiveTeamChange(newTeam, ActiveTeamChange.TeamChangePriorities.Base); - TryAddNewTeamChange(OriginalTeamIdentifier, currentTeamChange); + TryAddNewTeamChange(OriginalChangeTeamIdentifier, currentTeamChange); } - protected void ChangeTeam(CharacterTeamType newTeam) + private void ChangeTeam(CharacterTeamType newTeam) { - if (newTeam == teamID) - { - return; - } - teamID = newTeam; - if (info != null) { info.TeamID = newTeam; } - + if (newTeam == teamID) { return; } + if (originalTeamID == null) { originalTeamID = teamID; } + TeamID = newTeam; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; @@ -277,7 +293,7 @@ namespace Barotrauma { if (currentTeamChange == removedTeamChange) { - currentTeamChange = activeTeamChanges[OriginalTeamIdentifier]; + currentTeamChange = activeTeamChanges[OriginalChangeTeamIdentifier]; } } return activeTeamChanges.Remove(identifier); @@ -311,7 +327,9 @@ namespace Barotrauma } } - public bool IsOnPlayerTeam => TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2; + public bool IsOnPlayerTeam => teamID == CharacterTeamType.Team1 || teamID == CharacterTeamType.Team2; + + public bool IsOriginallyOnPlayerTeam => originalTeamID == CharacterTeamType.Team1 || originalTeamID == CharacterTeamType.Team2; public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator; public CombatAction CombatAction; @@ -360,7 +378,7 @@ namespace Barotrauma public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier(); - public Identifier Group => Params.Group; + public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group; public bool IsHumanoid => Params.Humanoid; @@ -458,10 +476,15 @@ namespace Barotrauma } set { - if (info != null && info != value) info.Remove(); - + if (info != null && info != value) + { + info.Remove(); + } info = value; - if (info != null) info.Character = this; + if (info != null) + { + info.Character = this; + } } } @@ -521,8 +544,13 @@ namespace Barotrauma } set { + bool wasHidden = HideFace; hideFaceTimer = MathHelper.Clamp(hideFaceTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f); - if (info != null && info.IsDisguisedAsAnother != HideFace) info.CheckDisguiseStatus(true); + bool isHidden = HideFace; + if (isHidden != wasHidden && info != null && info.IsDisguisedAsAnother != isHidden) + { + info.CheckDisguiseStatus(true); + } } } @@ -728,7 +756,7 @@ namespace Barotrauma /// public bool InPressure { - get { return CurrentHull == null || CurrentHull.LethalPressure > 5.0f; } + get { return CurrentHull == null || CurrentHull.LethalPressure > 0.0f; } } /// @@ -752,7 +780,7 @@ namespace Barotrauma get { if (IsUnconscious) { return true; } - return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.Identifier == "paralysis" && a.Strength >= a.Prefab.MaxStrength); + return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == AfflictionPrefab.ParalysisType && a.Strength >= a.Prefab.MaxStrength); } } @@ -836,7 +864,7 @@ namespace Barotrauma public float Bleeding { - get { return CharacterHealth.GetAfflictionStrength("bleeding", true); } + get { return CharacterHealth.GetAfflictionStrength(AfflictionPrefab.BleedingType, true); } } private bool speechImpedimentSet; @@ -1041,7 +1069,7 @@ namespace Barotrauma public bool InWater => AnimController is AnimController { InWater: true }; - public bool IsLowInOxygen => NeedsOxygen && OxygenAvailable < CharacterHealth.LowOxygenThreshold; + public bool IsLowInOxygen => CharacterHealth.OxygenAmount < 100; public bool GodMode = false; @@ -1099,6 +1127,12 @@ namespace Barotrauma public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID; + public float AITurretPriority + { + get => Params.AITurretPriority; + private set => Params.AITurretPriority = value; + } + public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath); public OnDeathHandler OnDeath; @@ -1619,7 +1653,7 @@ namespace Barotrauma { DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\"."); } - else if (humanPrefab.GiveItems(this, Submarine)) + else if (humanPrefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint)) { return; } @@ -1691,7 +1725,7 @@ namespace Barotrauma if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue)) { skillLevel += skillValue; - break; + break; } } @@ -1700,9 +1734,7 @@ namespace Barotrauma } skillLevel += GetStatValue(GetSkillStatType(skillIdentifier)); - - - return skillLevel; + return Math.Max(skillLevel, 0); } // TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner @@ -1791,20 +1823,8 @@ namespace Barotrauma public void StackSpeedMultiplier(float val) { - if (val < 1f) - { - if (val < greatestNegativeSpeedMultiplier) - { - greatestNegativeSpeedMultiplier = val; - } - } - else - { - if (val > greatestPositiveSpeedMultiplier) - { - greatestPositiveSpeedMultiplier = val; - } - } + greatestNegativeSpeedMultiplier = Math.Min(val, greatestNegativeSpeedMultiplier); + greatestPositiveSpeedMultiplier = Math.Max(val, greatestPositiveSpeedMultiplier); } public void ResetSpeedMultiplier() @@ -1827,20 +1847,8 @@ namespace Barotrauma public void StackHealthMultiplier(float val) { - if (val < 1f) - { - if (val < greatestNegativeHealthMultiplier) - { - greatestNegativeHealthMultiplier = val; - } - } - else - { - if (val > greatestPositiveHealthMultiplier) - { - greatestPositiveHealthMultiplier = val; - } - } + greatestNegativeHealthMultiplier = Math.Min(val, greatestNegativeHealthMultiplier); + greatestPositiveHealthMultiplier = Math.Max(val, greatestPositiveHealthMultiplier); } private void CalculateHealthMultiplier() @@ -1900,7 +1908,7 @@ namespace Barotrauma { if (limb != null) { - sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: "damage")); + sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: AfflictionPrefab.DamageType)); } return Math.Clamp(sum, 0, 1f); } @@ -2196,24 +2204,7 @@ namespace Barotrauma if (SelectedItem != null) { - if (IsKeyDown(InputType.Aim) || !SelectedItem.RequireAimToSecondaryUse) - { - SelectedItem.SecondaryUse(deltaTime, this); - } - if (IsKeyDown(InputType.Use) && SelectedItem != null && !SelectedItem.IsShootable) - { - if (!SelectedItem.RequireAimToUse || IsKeyDown(InputType.Aim)) - { - SelectedItem.Use(deltaTime, this); - } - } - if (IsKeyDown(InputType.Shoot) && SelectedItem != null && SelectedItem.IsShootable) - { - if (!SelectedItem.RequireAimToUse || IsKeyDown(InputType.Aim)) - { - SelectedItem.Use(deltaTime, this); - } - } + tryUseItem(SelectedItem, deltaTime); } if (SelectedCharacter != null) @@ -2695,7 +2686,7 @@ namespace Barotrauma //character is outside but cursor position inside if (cursorPosition.Y > Level.Loaded.Size.Y) { - var sub = Submarine.FindContaining(cursorPosition); + var sub = Submarine.FindContainingInLocalCoordinates(cursorPosition); if (sub != null) cursorPosition += sub.Position; } } @@ -2872,7 +2863,7 @@ namespace Barotrauma } #endif } - else + else if (!IsClimbing) { #if CLIENT if (Controlled == this) @@ -2920,9 +2911,9 @@ namespace Barotrauma CharacterHealth.OpenHealthWindow = null; #endif } - else if (IsKeyHit(InputType.Health) && (SelectedItem != null || SelectedSecondaryItem != null)) + else if (IsKeyHit(InputType.Health) && SelectedItem != null) { - SelectedItem = SelectedSecondaryItem = null; + SelectedItem = null; } else if (focusedItem != null) { @@ -3017,7 +3008,9 @@ namespace Barotrauma for (int i = 0; i < CharacterList.Count; i++) { - CharacterList[i].Update(deltaTime, cam); + var character = CharacterList[i]; + System.Diagnostics.Debug.Assert(character != null && !character.Removed); + character.Update(deltaTime, cam); } } @@ -3108,8 +3101,7 @@ namespace Barotrauma if (NeedsAir) { //implode if not protected from pressure, and either outside or in a high-pressure hull - if (!IsProtectedFromPressure() && - (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)) + if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)) { if (CharacterHealth.PressureKillDelay <= 0.0f) { @@ -3136,15 +3128,17 @@ namespace Barotrauma PressureTimer = 0.0f; } } - else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && - PressureProtection < (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f) && - WorldPosition.Y < CharacterHealth.CrushDepth && !HasAbilityFlag(AbilityFlags.ImmuneToPressure)) + else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && !IsProtectedFromPressure) { - //implode if below crush depth, and either outside or in a high-pressure hull - if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f) + float realWorldDepth = Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 0.0f; + if (PressureProtection < realWorldDepth && realWorldDepth > CharacterHealth.CrushDepth) { - Implode(); - if (IsDead) { return; } + //implode if below crush depth, and either outside or in a high-pressure hull + if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f) + { + Implode(); + if (IsDead) { return; } + } } } @@ -4064,17 +4058,7 @@ namespace Barotrauma CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacterKill); if (!IsOnPlayerTeam) { return; } - if (CreatureMetrics.Instance.Killed.Contains(target.SpeciesName)) { return; } - CreatureMetrics.Instance.Killed.Add(target.SpeciesName); - AddEncounter(target); - } - - public void AddEncounter(Character other) - { - if (!IsOnPlayerTeam) { return; } - if (CreatureMetrics.Instance.Encountered.Contains(other.SpeciesName)) { return; } - CreatureMetrics.Instance.Encountered.Add(other.SpeciesName); - CreatureMetrics.Instance.RecentlyEncountered.Add(other.SpeciesName); + CreatureMetrics.RecordKill(target.SpeciesName); } public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false) @@ -4109,13 +4093,6 @@ namespace Barotrauma } } -#if CLIENT - if (Params.UseBossHealthBar && Controlled != null && Controlled.teamID == attacker?.teamID) - { - CharacterHUD.ShowBossHealthBar(this); - } -#endif - Vector2 dir = hitLimb.WorldPosition - worldPosition; if (Math.Abs(attackImpulse) > 0.0f) { @@ -4157,13 +4134,24 @@ namespace Barotrauma if (attacker != null && attacker != this && !attacker.Removed) { AddAttacker(attacker, attackResult.Damage); - AddEncounter(attacker); - attacker.AddEncounter(this); + if (IsOnPlayerTeam) + { + CreatureMetrics.AddEncounter(attacker.SpeciesName); + } + if (attacker.IsOnPlayerTeam) + { + CreatureMetrics.AddEncounter(SpeciesName); + } } ApplyStatusEffects(ActionType.OnDamaged, 1.0f); hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); } - +#if CLIENT + if (Params.UseBossHealthBar && Controlled != null && Controlled.teamID == attacker?.teamID) + { + CharacterHUD.ShowBossHealthBar(this, attackResult.Damage); + } +#endif return attackResult; } @@ -4181,7 +4169,8 @@ namespace Barotrauma { if (affliction.Prefab.IsBuff) { continue; } if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; } - if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis") + if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || + affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) { if (!Params.Health.PoisonImmunity) { @@ -4261,7 +4250,7 @@ namespace Barotrauma if (Screen.Selected != GameMain.GameScreen) { return; } if (newStun > 0 && Params.Health.StunImmunity) { - if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) + if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } @@ -4288,7 +4277,7 @@ namespace Barotrauma float eatingRegen = Params.Health.HealthRegenerationWhenEating; if (eatingRegen > 0) { - CharacterHealth.ReduceAfflictionOnAllLimbs("damage".ToIdentifier(), eatingRegen * deltaTime); + CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.DamageType, eatingRegen * deltaTime); } } if (statusEffects.TryGetValue(actionType, out var statusEffectList)) @@ -4298,7 +4287,7 @@ namespace Barotrauma if (statusEffect.type == ActionType.OnDamaged) { if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; } - if (statusEffect.OnlyPlayerTriggered) + if (statusEffect.OnlyWhenDamagedByPlayer) { if (LastAttacker == null || !LastAttacker.IsPlayer) { @@ -4356,6 +4345,10 @@ namespace Barotrauma { statusEffect.Apply(actionType, deltaTime, this, this); } + if (statusEffect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null) + { + statusEffect.Apply(actionType, deltaTime, this, CurrentHull); + } } if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered) { @@ -4494,9 +4487,12 @@ namespace Barotrauma OnDeath?.Invoke(this, CauseOfDeath); - var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer); - CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller); - CauseOfDeath.Killer?.RecordKill(this); + if (CauseOfDeath.Type != CauseOfDeathType.Disconnected) + { + var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer); + CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller); + CauseOfDeath.Killer?.RecordKill(this); + } if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen) { @@ -4517,6 +4513,9 @@ namespace Barotrauma { foreach (Item heldItem in HeldItems.ToList()) { + //if the item is both wearable and holdable, and currently worn, don't drop the item + var wearable = heldItem.GetComponent(); + if (wearable is { IsActive: true }) { continue; } heldItem.Drop(this); } } @@ -4563,6 +4562,11 @@ namespace Barotrauma SetStun(0.0f, true); isDead = false; + if (info != null) + { + info.CauseOfDeath = null; + } + foreach (LimbJoint joint in AnimController.LimbJoints) { var revoluteJoint = joint.revoluteJoint; @@ -4586,10 +4590,7 @@ namespace Barotrauma limb.IsSevered = false; } - if (GameMain.GameSession != null) - { - GameMain.GameSession.ReviveCharacter(this); - } + GameMain.GameSession?.ReviveCharacter(this); } public override void Remove() @@ -4657,6 +4658,7 @@ namespace Barotrauma Submarine = null; AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), lerp: false); AnimController.FindHull(worldPos, setSubmarine: true); + CurrentHull = AnimController.CurrentHull; if (AIController is HumanAIController humanAI) { humanAI.PathSteering?.ResetPath(); @@ -4707,7 +4709,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - newItem.CreateStatusEvent(); + newItem.CreateStatusEvent(loadingRound: true); } #if SERVER newItem.GetComponent()?.SyncHistory(); @@ -4903,34 +4905,36 @@ namespace Barotrauma return visibleHulls; } - public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) + public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => GetRelativeSimPosition(this, target, worldPos); + + public static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? worldPos = null) { - Vector2 targetPos = target.SimPosition; + Vector2 targetPos = to.SimPosition; if (worldPos.HasValue) { Vector2 wp = worldPos.Value; - if (target.Submarine != null) + if (to.Submarine != null) { - wp -= target.Submarine.Position; + wp -= to.Submarine.Position; } targetPos = ConvertUnits.ToSimUnits(wp); } - if (Submarine == null && target.Submarine != null) + if (from.Submarine == null && to.Submarine != null) { // outside and targeting inside - targetPos += target.Submarine.SimPosition; + targetPos += to.Submarine.SimPosition; } - else if (Submarine != null && target.Submarine == null) + else if (from.Submarine != null && to.Submarine == null) { // inside and targeting outside - targetPos -= Submarine.SimPosition; + targetPos -= from.Submarine.SimPosition; } - else if (Submarine != target.Submarine) + else if (from.Submarine != to.Submarine) { - if (Submarine != null && target.Submarine != null) + if (from.Submarine != null && to.Submarine != null) { // both inside, but in different subs - Vector2 diff = Submarine.SimPosition - target.Submarine.SimPosition; + Vector2 diff = from.Submarine.SimPosition - to.Submarine.SimPosition; targetPos -= diff; } } @@ -4952,13 +4956,14 @@ namespace Barotrauma public bool HasJob(Identifier identifier) => Info?.Job?.Prefab.Identifier == identifier; - public bool IsProtectedFromPressure() - { - return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); - } + /// + /// Is the character currently protected from the pressure by immunity/ability or a status effect (e.g. from a diving suit). + /// + public bool IsProtectedFromPressure => IsImmuneToPressure || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); - // Talent logic begins here. Should be encapsulated to its own controller soon + public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure); + #region Talents private readonly List characterTalents = new List(); public void LoadTalents() @@ -5032,6 +5037,49 @@ namespace Barotrauma return info.UnlockedTalents.Contains(identifier); } + public bool HasUnlockedAllTalents() + { + if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) + { + foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) + { + foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) + { + if (!talentOption.HasMaxTalents(info.UnlockedTalents)) + { + return false; + } + } + } + } + return true; + } + + public bool HasTalents() + { + return characterTalents.Any(); + } + + public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, abilityObject); + } + } + + public void CheckTalents(AbilityEffectType abilityEffectType) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, null); + } + } + + partial void OnTalentGiven(TalentPrefab talentPrefab); + + #endregion + private readonly HashSet sameRoomHulls = new(); /// @@ -5058,24 +5106,6 @@ namespace Barotrauma return sameRoomHulls.Contains(character.CurrentHull); } - public bool HasUnlockedAllTalents() - { - if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) - { - foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) - { - foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) - { - if (!talentOption.HasMaxTalents(info.UnlockedTalents)) - { - return false; - } - } - } - } - return true; - } - public static IEnumerable GetFriendlyCrew(Character character) { if (character is null) @@ -5085,27 +5115,6 @@ namespace Barotrauma return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead); } - public bool HasTalents() - { - return characterTalents.Any(); - } - - public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject) - { - foreach (var characterTalent in characterTalents) - { - characterTalent.CheckTalent(abilityEffectType, abilityObject); - } - } - - public void CheckTalents(AbilityEffectType abilityEffectType) - { - foreach (var characterTalent in characterTalents) - { - characterTalent.CheckTalent(abilityEffectType, null); - } - } - public bool HasRecipeForItem(Identifier recipeIdentifier) { return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); @@ -5169,7 +5178,6 @@ namespace Barotrauma #endif partial void OnMoneyChanged(int prevAmount, int newAmount); - partial void OnTalentGiven(TalentPrefab talentPrefab); /// /// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now. @@ -5345,7 +5353,7 @@ namespace Barotrauma public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other); - public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); + public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group); public void StopClimbing() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 49c273024..9d0379987 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -11,6 +12,31 @@ using Barotrauma.Abilities; namespace Barotrauma { + [NetworkSerialize] + internal readonly record struct NetJobVariant(Identifier Identifier, byte Variant) : INetSerializableStruct + { + [return: MaybeNull] + public JobVariant ToJobVariant() + { + if (!JobPrefab.Prefabs.TryGet(Identifier, out JobPrefab jobPrefab) || jobPrefab.HiddenJob) { return null; } + return new JobVariant(jobPrefab, Variant); + } + + public static NetJobVariant FromJobVariant(JobVariant jobVariant) => new NetJobVariant(jobVariant.Prefab.Identifier, (byte)jobVariant.Variant); + } + + [NetworkSerialize(ArrayMaxSize = byte.MaxValue)] + internal readonly record struct NetCharacterInfo(string NewName, + ImmutableArray Tags, + byte HairIndex, + byte BeardIndex, + byte MoustacheIndex, + byte FaceAttachmentIndex, + Color SkinColor, + Color HairColor, + Color FacialHairColor, + ImmutableArray JobVariants) : INetSerializableStruct; + class CharacterInfoPrefab { public readonly ImmutableArray Heads; @@ -315,6 +341,8 @@ namespace Barotrauma public HashSet UnlockedTalents { get; private set; } = new HashSet(); + public (Identifier factionId, float reputation) MinReputationToHire; + /// /// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to cull them from the selection /// @@ -508,8 +536,11 @@ namespace Barotrauma public List CurrentOrders { get; } = new List(); - //unique ID given to character infos in MP - //used by clients to identify which infos are the same to prevent duplicate characters in round summary + + /// + /// Unique ID given to character infos in MP. Non-persistent. + /// Used by clients to identify which infos are the same to prevent duplicate characters in round summary. + /// public ushort ID; public List SpriteTags @@ -667,7 +698,6 @@ namespace Barotrauma { Name = GetRandomName(randSync); } - TryLoadNameAndTitle(npcIdentifier); SetPersonalityTrait(); @@ -824,6 +854,8 @@ namespace Barotrauma MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0); UnlockedTalents = new HashSet(); + MinReputationToHire = (infoElement.GetAttributeIdentifier("factionId", Identifier.Empty), infoElement.GetAttributeFloat("minreputation", 0.0f)); + foreach (var subElement in infoElement.Elements()) { bool jobCreated = false; @@ -919,17 +951,25 @@ namespace Barotrauma } } + /// + /// Returns a presumably (not guaranteed) unique hash using the (current) Name, appearence, and job. + /// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique. + /// public int GetIdentifier() { - return GetIdentifier(Name); + return GetIdentifierHash(Name); } + /// + /// Returns a presumably (not guaranteed) unique hash using the OriginalName, appearence, and job. + /// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique. + /// public int GetIdentifierUsingOriginalName() { - return GetIdentifier(OriginalName); + return GetIdentifierHash(OriginalName); } - private int GetIdentifier(string name) + private int GetIdentifierHash(string name) { int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet.OrderBy(s => s))); id ^= Head.HairIndex << 12; @@ -1152,7 +1192,7 @@ namespace Barotrauma partial void LoadAttachmentSprites(); - private int CalculateSalary() + public int CalculateSalary() { if (Name == null || Job == null) { return 0; } @@ -1394,6 +1434,13 @@ namespace Barotrauma charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath)); + if (MinReputationToHire.factionId != default) + { + charElement.Add( + new XAttribute("factionId", Name), + new XAttribute("minreputation", MinReputationToHire.reputation)); + } + if (Character != null) { if (Character.AnimController.CurrentHull != null) @@ -1471,7 +1518,10 @@ namespace Barotrauma break; } } - targetAvailableInNextLevel = !isOutside && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null && (isOnConnectedLinkedSub || entitySub == Submarine.MainSub); + targetAvailableInNextLevel = + !isOutside && + GameMain.GameSession?.Campaign is not { SwitchedSubsThisRound: true } && + (isOnConnectedLinkedSub || entitySub == Submarine.MainSub); if (!targetAvailableInNextLevel) { if (!order.Prefab.CanBeGeneralized) @@ -1502,7 +1552,7 @@ namespace Barotrauma } if (order.OrderGiver != null) { - orderElement.Add(new XAttribute("ordergiverinfoid", order.OrderGiver.Info.ID)); + orderElement.Add(new XAttribute("ordergiver", order.OrderGiver.Info?.GetIdentifier())); } if (order.TargetSpatialEntity?.Submarine is Submarine targetSub) { @@ -1596,8 +1646,8 @@ namespace Barotrauma continue; } var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0); - int orderGiverInfoId = orderElement.GetAttributeInt("ordergiverinfoid", -1); - var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.ID == orderGiverInfoId) : null; + int orderGiverInfoId = orderElement.GetAttributeInt("ordergiver", -1); + var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId) : null; Entity targetEntity = null; switch (targetType) { @@ -1661,6 +1711,7 @@ namespace Barotrauma { targetId = GetOffsetId(parentSub, targetId); targetEntity = Entity.FindEntityByID(targetId); + return targetEntity != null; } else { @@ -1674,8 +1725,8 @@ namespace Barotrauma { DebugConsole.AddWarning($"Trying to load a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order doesn't require a target so a more generic version of the order will be loaded instead."); } + return true; } - return true; } } return orders; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index cb1b33763..d38730699 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; -using static Barotrauma.CharacterInfo; namespace Barotrauma { @@ -19,6 +17,8 @@ namespace Barotrauma public string Name => Identifier.Value; public Identifier VariantOf { get; } + public CharacterPrefab ParentPrefab { get; set; } + public void InheritFrom(CharacterPrefab parent) { ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); @@ -38,7 +38,7 @@ namespace Barotrauma } } - private XElement originalElement; + private readonly XElement originalElement; public ContentXElement ConfigElement { get; private set; } public CharacterInfoPrefab CharacterInfoPrefab { get; private set; } @@ -49,10 +49,6 @@ namespace Barotrauma public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile; public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName); - /// - /// Searches for a character config file from all currently selected content packages, - /// or from a specific package if the contentPackage parameter is given. - /// public static CharacterPrefab FindBySpeciesName(Identifier speciesName) { if (!Prefabs.ContainsKey(speciesName)) { return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 33368e40a..e6c09cc3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -14,11 +15,15 @@ namespace Barotrauma public Dictionary SerializableProperties { get; set; } - public float PendingAdditionStrength { get; set; } - public float AdditionStrength { get; set; } + public float PendingGrainEffectStrength { get; set; } + public float GrainEffectStrength { get; set; } private float fluctuationTimer; + private AfflictionPrefab.Effect activeEffect; + private float prevActiveEffectStrength; + protected bool activeEffectDirty = true; + protected float _strength; [Serialize(0f, IsPropertySaveable.Yes), Editable] @@ -42,10 +47,11 @@ namespace Barotrauma float newValue = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); if (newValue > _strength) { - PendingAdditionStrength = Prefab.GrainBurst; + PendingGrainEffectStrength = Prefab.GrainBurst; Duration = Prefab.Duration; } _strength = newValue; + activeEffectDirty = true; } } @@ -68,8 +74,7 @@ namespace Barotrauma public float DamagePerSecondTimer; public float PreviousVitalityDecrease; - public float StrengthDiminishMultiplier = 1.0f; - public Affliction MultiplierSource; + public (float Value, Affliction Source) StrengthDiminishMultiplier = (1.0f, null); public readonly Dictionary PeriodicEffectTimers = new Dictionary(); @@ -95,7 +100,7 @@ namespace Barotrauma prefab?.ReloadSoundsIfNeeded(); #endif Prefab = prefab; - PendingAdditionStrength = Prefab.GrainBurst; + PendingGrainEffectStrength = Prefab.GrainBurst; _strength = strength; Identifier = prefab.Identifier; @@ -147,7 +152,16 @@ namespace Barotrauma MathHelper.Clamp((int)Math.Floor(strength / maxStrength * strengthTexts.Length), 0, strengthTexts.Length - 1)]; } - public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength); + public AfflictionPrefab.Effect GetActiveEffect() + { + if (activeEffectDirty) + { + activeEffect = Prefab.GetActiveEffect(_strength); + prevActiveEffectStrength = _strength; + activeEffectDirty = false; + } + return activeEffect; + } public float GetVitalityDecrease(CharacterHealth characterHealth) { @@ -158,14 +172,14 @@ namespace Barotrauma { if (strength < Prefab.ActivationThreshold) { return 0.0f; } strength = MathHelper.Clamp(strength, 0.0f, Prefab.MaxStrength); - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; } float currVitalityDecrease = MathHelper.Lerp( currentEffect.MinVitalityDecrease, currentEffect.MaxVitalityDecrease, - (strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); if (currentEffect.MultiplyByMaxVitality) { @@ -186,11 +200,11 @@ namespace Barotrauma float amount = MathHelper.Lerp( currentEffect.MinGrainStrength, currentEffect.MaxGrainStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); - if (Prefab.GrainBurst > 0 && AdditionStrength > amount) + if (Prefab.GrainBurst > 0 && GrainEffectStrength > amount) { - return Math.Min(AdditionStrength, 1.0f); + return Math.Min(GrainEffectStrength, 1.0f); } return amount; @@ -206,7 +220,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinScreenDistort, currentEffect.MaxScreenDistort, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } public float GetRadialDistortStrength() @@ -219,7 +233,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinRadialDistort, currentEffect.MaxRadialDistort, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } public float GetChromaticAberrationStrength() @@ -232,7 +246,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinChromaticAberration, currentEffect.MaxChromaticAberration, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } public float GetAfflictionOverlayMultiplier() @@ -247,7 +261,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinAfflictionOverlayAlphaMultiplier, currentEffect.MaxAfflictionOverlayAlphaMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public Color GetFaceTint() @@ -259,7 +273,7 @@ namespace Barotrauma return Color.Lerp( currentEffect.MinFaceTint, currentEffect.MaxFaceTint, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public Color GetBodyTint() @@ -271,7 +285,7 @@ namespace Barotrauma return Color.Lerp( currentEffect.MinBodyTint, currentEffect.MaxBodyTint, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public float GetScreenBlurStrength() @@ -284,7 +298,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinScreenBlur, currentEffect.MaxScreenBlur, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + currentEffect.GetStrengthFactor(this)) * GetScreenEffectFluctuation(currentEffect); } private float GetScreenEffectFluctuation(AfflictionPrefab.Effect currentEffect) @@ -302,7 +316,7 @@ namespace Barotrauma float amount = MathHelper.Lerp( currentEffect.MinSkillMultiplier, currentEffect.MaxSkillMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); return amount; } @@ -333,7 +347,7 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinResistance, currentEffect.MaxResistance, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public float GetSpeedMultiplier() @@ -344,26 +358,21 @@ namespace Barotrauma return MathHelper.Lerp( currentEffect.MinSpeedMultiplier, currentEffect.MaxSpeedMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); } public float GetStatValue(StatTypes statType) { - if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return 0.0f; } + if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return 0.0f; } - if (currentEffect.AfflictionStatValues.TryGetValue(statType, out var value)) - { - return MathHelper.Lerp( - value.minValue, - value.maxValue, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); - } - return 0.0f; + if (!currentEffect.AfflictionStatValues.TryGetValue(statType, out var appliedStat)) { return 0.0f; } + + return MathHelper.Lerp(appliedStat.MinValue, appliedStat.MaxValue, currentEffect.GetStrengthFactor(this)); } public bool HasFlag(AbilityFlags flagType) { - if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; } + if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return false; } return currentEffect.AfflictionAbilityFlags.HasFlag(flagType); } @@ -401,13 +410,16 @@ namespace Barotrauma fluctuationTimer += deltaTime * currentEffect.ScreenEffectFluctuationFrequency; fluctuationTimer %= 1.0f; - if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted + if (currentEffect.StrengthChange < 0) // Only apply StrengthDiminish.Multiplier if affliction is being weakened { - float durationMultiplier = 1 / (1 + (Prefab.IsBuff ? characterHealth.Character.GetStatValue(StatTypes.BuffDurationMultiplier) - : characterHealth.Character.GetStatValue(StatTypes.DebuffDurationMultiplier))); + float stat = characterHealth.Character.GetStatValue( + Prefab.IsBuff + ? StatTypes.BuffDurationMultiplier + : StatTypes.DebuffDurationMultiplier); - _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier * durationMultiplier; + float durationMultiplier = 1f / (1f + stat); + _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier.Value * durationMultiplier; } else if (currentEffect.StrengthChange > 0) // Reduce strengthening of afflictions if resistant { @@ -415,6 +427,7 @@ namespace Barotrauma } // 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); + activeEffectDirty |= !MathUtils.NearlyEqual(prevActiveEffectStrength, _strength); foreach (StatusEffect statusEffect in currentEffect.StatusEffects) { @@ -426,14 +439,14 @@ namespace Barotrauma { amount /= Prefab.GrainBurst; } - if (PendingAdditionStrength >= 0) + if (PendingGrainEffectStrength >= 0) { - AdditionStrength += amount; - PendingAdditionStrength -= deltaTime; + GrainEffectStrength += amount; + PendingGrainEffectStrength -= deltaTime; } - else if (AdditionStrength > 0) + else if (GrainEffectStrength > 0) { - AdditionStrength -= amount; + GrainEffectStrength -= amount; } } @@ -442,7 +455,10 @@ namespace Barotrauma var currentEffect = GetActiveEffect(); if (currentEffect != null) { - currentEffect.StatusEffects.ForEach(se => ApplyStatusEffect(type, se, deltaTime, characterHealth, targetLimb)); + foreach (var statusEffect in currentEffect.StatusEffects) + { + ApplyStatusEffect(type, statusEffect, deltaTime, characterHealth, targetLimb); + } } } @@ -481,6 +497,7 @@ namespace Barotrauma { _nonClampedStrength = strength; _strength = _nonClampedStrength; + activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength); } public bool ShouldShowIcon(Character afflictedCharacter) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs index 44643c736..bc8ac1329 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs @@ -1,5 +1,8 @@ namespace Barotrauma { + /// + /// A special affliction type that increases the character's Bloodloss affliction with a rate relative to the strength of the bleeding. + /// class AfflictionBleeding : Affliction { public AfflictionBleeding(AfflictionPrefab prefab, float strength) : diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 523835530..20a19d64f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -7,6 +7,10 @@ using Microsoft.Xna.Framework; namespace Barotrauma { + /// + /// A special affliction type that gradually makes the character turn into another type of character. + /// See for more details. + /// partial class AfflictionHusk : Affliction { public enum InfectionState @@ -22,7 +26,7 @@ namespace Barotrauma private Character character; - private bool stun = true; + private bool stun = false; private readonly List huskInfection = new List(); @@ -43,6 +47,7 @@ namespace Barotrauma DeactivateHusk(); highestStrength = 0; } + activeEffectDirty = true; } } private float highestStrength; @@ -62,6 +67,7 @@ namespace Barotrauma private float DormantThreshold => HuskPrefab.DormantThreshold; private float ActiveThreshold => HuskPrefab.ActiveThreshold; private float TransitionThreshold => HuskPrefab.TransitionThreshold; + private float TransformThresholdOnDeath => HuskPrefab.TransformThresholdOnDeath; public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) @@ -216,7 +222,8 @@ namespace Barotrauma private void DeactivateHusk() { if (character?.AnimController == null || character.Removed) { return; } - if (Prefab is AfflictionPrefabHusk { NeedsAir: false }) + if (Prefab is AfflictionPrefabHusk { NeedsAir: false } && + !character.CharacterHealth.GetAllAfflictions().Any(a => a != this && a.Prefab is AfflictionPrefabHusk { NeedsAir: false })) { character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 997d13a25..87b22795b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Xml.Linq; using Barotrauma.Extensions; using System.Collections.Immutable; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -56,8 +57,77 @@ namespace Barotrauma public override void Dispose() { } } + /// + /// AfflictionPrefabHusk is a special type of affliction that has added functionality for husk infection. + /// class AfflictionPrefabHusk : AfflictionPrefab { + // Use any of these to define which limb the appendage is attached to. + // If multiple are defined, the order of preference is: id, name, type. + public readonly int AttachLimbId; + public readonly string AttachLimbName; + public readonly LimbType AttachLimbType; + + /// + /// The minimum strength at which husk infection will be in the dormant stage. + /// It must be less than or equal to ActiveThreshold. + /// + public readonly float DormantThreshold; + + /// + /// The minimum strength at which husk infection will be in the active stage. + /// It must be greater than or equal to DormantThreshold and less than or equal to TransitionThreshold. + /// + public readonly float ActiveThreshold; + + /// + /// The minimum strength at which husk infection will be in its final stage. + /// It must be greater than or equal to ActiveThreshold. + /// + public readonly float TransitionThreshold; + + /// + /// The minimum strength the affliction must have for the affected character + /// to transform into a husk upon death. + /// + public readonly float TransformThresholdOnDeath; + + /// + /// The species of husk to convert the affected character to + /// once husk infection reaches its final stage. + /// + public readonly Identifier HuskedSpeciesName; + + /// + /// If set to true, all buffs are transferred to the converted + /// character after husk transformation is complete. + /// + public readonly bool TransferBuffs; + + /// + /// If set to true, the affected player will see on-screen messages describing husk infection symptoms + /// and affected bots will speak about their current husk infection stage. + /// + public readonly bool SendMessages; + + /// + /// If set to true, affected characters will have their speech impeded once the affliction + /// reaches the dormant stage. + /// + public readonly bool CauseSpeechImpediment; + + /// + /// If not set to true, affected characters will no longer require air + /// once the affliction reaches the active stage. + /// + public readonly bool NeedsAir; + + /// + /// If set to true, affected players will retain control of their character + /// after transforming into a husk. + /// + public readonly bool ControlHusk; + public AfflictionPrefabHusk(ContentXElement element, AfflictionsFile file, Type type = null) : base(element, file, type) { HuskedSpeciesName = element.GetAttributeIdentifier("huskedspeciesname", Identifier.Empty); @@ -68,7 +138,6 @@ namespace Barotrauma } // Remove "[speciesname]" for backward support (we don't use it anymore) HuskedSpeciesName = HuskedSpeciesName.Remove("[speciesname]").ToIdentifier(); - TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty(), trim: true); if (TargetSpecies.Length == 0) { DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange); @@ -79,7 +148,7 @@ namespace Barotrauma { AttachLimbId = attachElement.GetAttributeInt("id", -1); AttachLimbName = attachElement.GetAttributeString("name", null); - AttachLimbType = Enum.TryParse(attachElement.GetAttributeString("type", "none"), true, out LimbType limbType) ? limbType : LimbType.None; + AttachLimbType = attachElement.GetAttributeEnum("type", LimbType.None); } else { @@ -97,175 +166,282 @@ namespace Barotrauma DormantThreshold = element.GetAttributeFloat("dormantthreshold", MaxStrength * 0.5f); ActiveThreshold = element.GetAttributeFloat("activethreshold", MaxStrength * 0.75f); TransitionThreshold = element.GetAttributeFloat("transitionthreshold", MaxStrength); + + if (DormantThreshold > ActiveThreshold) + { + DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})"); + } + if (ActiveThreshold > TransitionThreshold) + { + DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})"); + } + TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold); } - - // Use any of these to define which limb the appendage is attached to. - // If multiple are defined, the order of preference is: id, name, type. - public readonly int AttachLimbId; - public readonly string AttachLimbName; - public readonly LimbType AttachLimbType; - - public float ActiveThreshold, DormantThreshold, TransitionThreshold; - public float TransformThresholdOnDeath; - - public readonly Identifier HuskedSpeciesName; - public readonly Identifier[] TargetSpecies; - - public readonly bool TransferBuffs; - public readonly bool SendMessages; - public readonly bool CauseSpeechImpediment; - public readonly bool NeedsAir; - public readonly bool ControlHusk; } + /// + /// AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character. + /// There are multiple sub-types of afflictions such as AfflictionPrefabHusk, AfflictionPsychosis and AfflictionBleeding that can be used for additional functionality. + /// + /// When defining a new affliction, the type will be determined by the element name. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// class AfflictionPrefab : PrefabWithUintIdentifier { - public class Effect + /// + /// Effects are the primary way to add functionality to afflictions. + /// + /// + /// + /// + /// Enables the specified flag on the character as long as the effect is active. + /// + /// + /// + /// Flag that will be enabled for the character as long as the effect is active. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Which ability flag to enable. + /// + /// + /// + public sealed class Effect { //this effect is applied when the strength is within this range - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Minimum affliction strength required for this effect to be active.")] public float MinStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Maximum affliction strength for which this effect will be active.")] public float MaxStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's lowest strength.")] public float MinVitalityDecrease { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "The amount of vitality that is lost at this effect's highest strength.")] public float MaxVitalityDecrease { get; private set; } - //how much the strength of the affliction changes per second - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much the affliction's strength changes every second while this effect is active.")] public float StrengthChange { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "If set to true, MinVitalityDecrease and MaxVitalityDecrease represent a fraction of the affected character's maximum " + + "vilatily, with 1 meaning 100%, instead of the same amount for all species.")] public bool MultiplyByMaxVitality { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's lowest strength.")] public float MinScreenBlur { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's highest strength.")] public float MaxScreenBlur { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's lowest strength.")] public float MinScreenDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Generic distortion effect strength at this effect's highest strength.")] public float MaxScreenDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's lowest strength.")] public float MinRadialDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radial distortion effect strength at this effect's highest strength.")] public float MaxRadialDistort { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's lowest strength.")] public float MinChromaticAberration { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Chromatic aberration effect strength at this effect's highest strength.")] public float MaxChromaticAberration { get; private set; } - [Serialize("255,255,255,255", IsPropertySaveable.No)] + [Serialize("255,255,255,255", IsPropertySaveable.No, description: "Radiation grain effect color.")] public Color GrainColor { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's lowest strength.")] public float MinGrainStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Radiation grain effect strength at this effect's highest strength.")] public float MaxGrainStrength { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: + "The maximum rate of fluctuation to apply to visual effects caused by this affliction effect. " + + "Effective fluctuation is proportional to the affliction's current strength.")] public float ScreenEffectFluctuationFrequency { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for the affliction overlay's opacity at this effect's lowest strength. " + + "See the list of elements for more details.")] public float MinAfflictionOverlayAlphaMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for the affliction overlay's opacity at this effect's highest strength. " + + "See the list of elements for more details.")] public float MaxAfflictionOverlayAlphaMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for every buff's decay rate at this effect's lowest strength. " + + "Only applies to afflictions of class BuffDurationIncrease.")] public float MinBuffMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: + "Multiplier for every buff's decay rate at this effect's highest strength. " + + "Only applies to afflictions of class BuffDurationIncrease.")] public float MaxBuffMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's lowest strength.")] public float MinSpeedMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to the affected character's speed at this effect's highest strength.")] public float MaxSpeedMultiplier { get; private set; } - - [Serialize(1.0f, IsPropertySaveable.No)] + + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's lowest strength.")] public float MinSkillMultiplier { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier to apply to all of the affected character's skill levels at this effect's highest strength.")] public float MaxSkillMultiplier { get; private set; } - private readonly Identifier[] resistanceFor; - public IReadOnlyList ResistanceFor => resistanceFor; + /// + /// A list of identifiers of afflictions that the affected character will be + /// resistant to when this effect is active. + /// + public readonly ImmutableArray ResistanceFor; - [Serialize(0.0f, IsPropertySaveable.No)] + [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; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, + description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's highest strength.")] public float MaxResistance { get; private set; } - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "Identifier used by AI to determine conversation lines to say when this effect is active.")] public Identifier DialogFlag { get; private set; } - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "Tag that enemy AI may use to target the affected character when this effect is active.")] public Identifier Tag { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + description: "Color to tint the affected character's face with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character's face.")] public Color MinFaceTint { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + description: "Color to tint the affected character's face with at this effect's highest strength. The alpha channel is used to determine how much to tint the character's face.")] public Color MaxFaceTint { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + description: "Color to tint the affected character's entire body with at this effect's lowest strength. The alpha channel is used to determine how much to tint the character.")] public Color MinBodyTint { get; private set; } - [Serialize("0,0,0,0", IsPropertySaveable.No)] + [Serialize("0,0,0,0", IsPropertySaveable.No, + 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; } /// - /// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character + /// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength. /// - public Identifier[] BlockTransformation { get; private set; } + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public readonly struct AppliedStatValue + { + /// + /// Which StatType to apply + /// + public readonly StatTypes StatType; - public readonly Dictionary AfflictionStatValues = new Dictionary(); - public AbilityFlags AfflictionAbilityFlags; + /// + /// Minimum value to apply + /// + public readonly float MinValue; + + /// + /// Minimum value to apply + /// + public readonly float MaxValue; + + /// + /// Constant value to apply, will be ignored if MinValue or MaxValue are set + /// + private readonly float Value; + + public AppliedStatValue(ContentXElement element) + { + Value = element.GetAttributeFloat("value", 0.0f); + StatType = element.GetAttributeEnum("stattype", StatTypes.None); + MinValue = element.GetAttributeFloat("minvalue", Value); + MaxValue = element.GetAttributeFloat("maxvalue", Value); + } + } + + /// + /// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character. + /// + public readonly ImmutableArray BlockTransformation; + + /// + /// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength. + /// + public readonly ImmutableDictionary AfflictionStatValues; + + public readonly AbilityFlags AfflictionAbilityFlags; //statuseffects applied on the character when the affliction is active - public readonly List StatusEffects = new List(); + public readonly ImmutableArray StatusEffects; public Effect(ContentXElement element, string parentDebugName) { SerializableProperty.DeserializeProperties(this, element); - resistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty()); - BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty()); + ResistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty())!.ToImmutableArray(); + BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty())!.ToImmutableArray(); + var afflictionStatValues = new Dictionary(); + var statusEffects = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "statuseffect": - StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); + statusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); break; case "statvalue": - var statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), parentDebugName); - - float defaultValue = subElement.GetAttributeFloat("value", 0f); - float minValue = subElement.GetAttributeFloat("minvalue", defaultValue); - float maxValue = subElement.GetAttributeFloat("maxvalue", defaultValue); - - AfflictionStatValues.TryAdd(statType, (minValue, maxValue)); + var newStatValue = new AppliedStatValue(subElement); + afflictionStatValues.Add(newStatValue.StatType, newStatValue); break; case "abilityflag": - var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName); + AbilityFlags flagType = subElement.GetAttributeEnum("flagtype", AbilityFlags.None); + if (flagType is AbilityFlags.None) + { + DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\"."); + continue; + } AfflictionAbilityFlags |= flagType; break; case "affliction": @@ -273,21 +449,71 @@ namespace Barotrauma break; } } + AfflictionStatValues = afflictionStatValues.ToImmutableDictionary(); + StatusEffects = statusEffects.ToImmutableArray(); } + + /// + /// Returns 0 if affliction.Strength is MinStrength, + /// 1 if affliction.Strength is MaxStrength + /// + public float GetStrengthFactor(Affliction affliction) + => MathUtils.InverseLerp( + MinStrength, + MaxStrength, + affliction.Strength); } - public class Description + /// + /// Description element can be used to define descriptions for the affliction that are shown at specific conditions. + /// For example a description that only shows to other players or only at certain strength levels. + /// + /// + /// + /// Raw text for the description. + /// + /// + public sealed class Description { public enum TargetType { + /// + /// Everyone can see the description. + /// Any, + /// + /// Only the affected character can see the description. + /// Self, + /// + /// The affected character cannot see the description but others can. + /// OtherCharacter } + /// + /// Raw text for the description. + /// public readonly LocalizedString Text; + + /// + /// Text tag used to set the text from the localization files. + /// public readonly Identifier TextTag; - public readonly float MinStrength, MaxStrength; + + /// + /// Minimum strength required for the description to be shown. + /// + public readonly float MinStrength; + + /// + /// Maximum strength required for the description to be shown. + /// + public readonly float MaxStrength; + + /// + /// Who can see the description. + /// public readonly TargetType Target; public Description(ContentXElement element, AfflictionPrefab affliction) @@ -317,7 +543,23 @@ namespace Barotrauma } } - public class PeriodicEffect + /// + /// PeriodicEffect applies StatusEffects to the character periodically. + /// + /// + /// + /// + /// How often the status effect is applied in seconds. + /// Setting this attribute will set both the min and max interval to the specified value. + /// + /// + /// Minimum interval between applying the status effect in seconds. + /// + /// + /// Maximum interval between applying the status effect in seconds. + /// + /// + public sealed class PeriodicEffect { public readonly List StatusEffects = new List(); public readonly float MinInterval, MaxInterval; @@ -344,65 +586,151 @@ namespace Barotrauma } } + public static readonly Identifier DamageType = "damage".ToIdentifier(); + public static readonly Identifier BurnType = "burn".ToIdentifier(); + public static readonly Identifier BleedingType = "bleeding".ToIdentifier(); + public static readonly Identifier ParalysisType = "paralysis".ToIdentifier(); + public static readonly Identifier PoisonType = "poison".ToIdentifier(); + public static readonly Identifier StunType = "stun".ToIdentifier(); + public static readonly Identifier EMPType = "emp".ToIdentifier(); + 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 HuskInfectionType = "huskinfection".ToIdentifier(); + public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"]; public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"]; public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"]; - public static AfflictionPrefab Bleeding => Prefabs["bleeding"]; - public static AfflictionPrefab Burn => Prefabs["burn"]; + public static AfflictionPrefab Bleeding => Prefabs[BleedingType]; + public static AfflictionPrefab Burn => Prefabs[BurnType]; public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"]; public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"]; public static AfflictionPrefab Pressure => Prefabs["pressure"]; - public static AfflictionPrefab Stun => Prefabs["stun"]; + public static AfflictionPrefab Stun => Prefabs[StunType]; public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"]; + public static readonly PrefabCollection Prefabs = new PrefabCollection(); - public override void Dispose() { } - public static IEnumerable List => Prefabs; - // Arbitrary string that is used to identify the type of the affliction. - public readonly Identifier AfflictionType; + public override void Dispose() { } private readonly ContentXElement configElement; - - //Does the affliction affect a specific limb or the whole character - public readonly bool LimbSpecific; - - //If not a limb-specific affliction, which limb is the indicator shown on in the health menu - //(e.g. mental health problems on head, lack of oxygen on torso...) - public readonly LimbType IndicatorLimb; public readonly LocalizedString Name; - public readonly Identifier TranslationIdentifier; - public readonly bool IsBuff; - public readonly bool AffectMachines; - public readonly bool HealableInMedicalClinic; - public readonly float HealCostMultiplier; - public readonly int BaseHealCost; - public readonly bool ShowBarInHealthMenu; - + public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription; private readonly LocalizedString defaultDescription; public readonly ImmutableList Descriptions; + /// + /// Arbitrary string that is used to identify the type of the affliction. + /// + public readonly Identifier AfflictionType; + + /// + /// If set to true, the affliction affects individual limbs. Otherwise, it affects the whole character. + /// + public readonly bool LimbSpecific; + + /// + /// If the affliction doesn't affect individual limbs, this attribute determines + /// where the game will render the affliction's indicator when viewed in the + /// in-game health UI. + /// + /// For example, the psychosis indicator is rendered on the head, and low oxygen + /// is rendered on the torso. + /// + public readonly LimbType IndicatorLimb; + + /// + /// Can be set to the identifier of another affliction to make this affliction + /// reuse the same name and description. + /// + public readonly Identifier TranslationIdentifier; + + /// + /// If set to true, the game will recognize this affliction as a buff. + /// This means, among other things, that bots won't attempt to treat it, + /// and the health UI will render the affected limb in green rather than red. + /// + public readonly bool IsBuff; + + /// + /// If set to true, this affliction can affect characters that are marked as + /// machines, such as the Fractal Guardian. + /// + public readonly bool AffectMachines; + + /// + /// If set to true, this affliction can be healed at the medical clinic. + /// + /// + /// + /// false if the affliction is a buff or has the type "geneticmaterialbuff" or "geneticmaterialdebuff", true otherwise. + /// + /// + public readonly bool HealableInMedicalClinic; + + /// + /// How much each unit of this affliction's strength will add + /// to the cost of healing at the medical clinic. + /// + public readonly float HealCostMultiplier; + + /// + /// The minimum cost of healing this affliction at the medical clinic. + /// + public readonly int BaseHealCost; + + /// + /// If set to false, the health UI will not show the strength of the affliction + /// as a bar under its indicator. + /// + public readonly bool ShowBarInHealthMenu; + + /// + /// If set to true, this affliction's icon will be hidden from the HUD + /// after 5 seconds. + /// public readonly bool HideIconAfterDelay; - //how high the strength has to be for the affliction to take affect + /// + /// How high the strength has to be for the affliction to take effect + /// public readonly float ActivationThreshold = 0.0f; - //how high the strength has to be for the affliction icon to be shown in the UI + + /// + /// How high the strength has to be for the affliction icon to be shown in the UI + /// public readonly float ShowIconThreshold = 0.05f; - //how high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface + + /// + /// How high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface + /// public readonly float ShowIconToOthersThreshold = 0.05f; + + /// + /// The maximum strength this affliction can have. + /// public readonly float MaxStrength = 100.0f; + /// + /// The strength of the radiation grain effect to apply + /// when the strength of this affliction increases. + /// public readonly float GrainBurst; - //how high the strength has to be for the affliction icon to be shown with a health scanner + /// + /// How high the strength has to be for the affliction icon to be shown with a health scanner + /// public readonly float ShowInHealthScannerThreshold = 0.05f; - //how strong the affliction needs to be before bots attempt to treat it + /// + /// How strong the affliction needs to be before bots attempt to treat it + /// public readonly float TreatmentThreshold = 5.0f; /// @@ -411,25 +739,57 @@ namespace Barotrauma public ImmutableHashSet IgnoreTreatmentIfAfflictedBy; /// - /// The affliction is automatically removed after this time. 0 = unlimited + /// The duration of the affliction, in seconds. If set to 0, the affliction does not expire. /// public readonly float Duration; - //how much karma changes when a player applies this affliction to someone (per strength of the affliction) + /// + /// How much karma changes when a player applies this affliction to someone (per strength of the affliction) + /// public float KarmaChangeOnApplied; + /// + /// Opacity of the burn effect (darker tint) on limbs affected by this affliction. 1 = full strength. + /// public readonly float BurnOverlayAlpha; + + /// + /// Opacity of the bloody damage overlay on limbs affected by this affliction. 1 = full strength. + /// public readonly float DamageOverlayAlpha; - //steam achievement given when the affliction is removed from the controlled character + /// + /// Steam achievement given when the controlled character receives the affliction + /// + public readonly Identifier AchievementOnReceived; + + /// + /// Steam achievement given when the affliction is removed from the controlled character + /// public readonly Identifier AchievementOnRemoved; - public readonly Sprite Icon; + /// + /// A gradient that defines which color to render this affliction's icon + /// with, based on the affliction's current strength. + /// public readonly Color[] IconColors; - public readonly Sprite AfflictionOverlay; + /// + /// If set to true and the affliction has an AfflictionOverlay element, + /// the overlay's opacity will be strictly proportional to its strength. + /// Otherwise, the overlay's opacity will be determined based on its + /// activation threshold and effects. + /// public readonly bool AfflictionOverlayAlphaIsLinear; + /// + /// If set to true, this affliction will not persist between rounds. + /// + public readonly bool ResetBetweenRounds; + + /// + /// Should damage particles be emitted when a character receives this affliction? Only relevant if the affliction is of the type "bleeding" or "damage". + /// public readonly bool DamageParticles; /// @@ -444,7 +804,20 @@ namespace Barotrauma /// public readonly float WeaponsSkillGain; + /// + /// A list of species this affliction is allowed to affect. + /// + public Identifier[] TargetSpecies { get; protected set; } + + /// + /// Effects to apply at various strength levels. + /// Only one effect can be applied at any given moment, so their ranges should be defined with no overlap. + /// private readonly List effects = new List(); + + /// + /// PeriodicEffect applies StatusEffects to the character periodically. + /// private readonly List periodicEffects = new List(); public IEnumerable Effects => effects; @@ -453,7 +826,17 @@ namespace Barotrauma private readonly ConstructorInfo constructor; - public readonly bool ResetBetweenRounds; + /// + /// Icon that's used in UI to represent this affliction. + /// + public readonly Sprite Icon; + + /// + /// A sprite that covers the affected player's entire screen when this affliction is active. + /// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier + /// and MaxAfflictionOverlayAlphaMultiplier + /// + public readonly Sprite AfflictionOverlay; public IEnumerable> TreatmentSuitability { @@ -481,7 +864,7 @@ namespace Barotrauma if (!string.IsNullOrEmpty(fallbackName)) { Name = Name.Fallback(fallbackName); - } + } defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}"); string fallbackDescription = element.GetAttributeString("description", ""); if (!string.IsNullOrEmpty(fallbackDescription)) @@ -536,13 +919,22 @@ namespace Barotrauma KarmaChangeOnApplied = element.GetAttributeFloat(nameof(KarmaChangeOnApplied), 0.0f); - CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", "")); - SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); + CauseOfDeathDescription = + TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("causeofdeathdescription", ""))) + .Fallback(element.GetAttributeString("causeofdeathdescription", "")); + SelfCauseOfDeathDescription = + TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("selfcauseofdeathdescription", ""))) + .Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); IconColors = element.GetAttributeColorArray(nameof(IconColors), null); AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false); + AchievementOnReceived = element.GetAttributeIdentifier(nameof(AchievementOnReceived), ""); AchievementOnRemoved = element.GetAttributeIdentifier(nameof(AchievementOnRemoved), ""); + TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty(), trim: true); + ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false); DamageParticles = element.GetAttributeBool(nameof(DamageParticles), true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs index 724184a11..a653041ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs @@ -1,5 +1,8 @@ namespace Barotrauma { + /// + /// A special affliction type that makes the character see and hear things that aren't there. + /// partial class AfflictionPsychosis : Affliction { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs index 408545fa2..fe656968e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs @@ -1,11 +1,12 @@ using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; namespace Barotrauma { + /// + /// A special affliction type that periodically inverts the character's controls and stuns the character. + /// The frequency and duration of the effects increases the higher the strength of the affliction is. + /// class AfflictionSpaceHerpes : Affliction { private float invertControlsCooldown = 60.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs index 730a08b4d..e3aaf11d9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs @@ -3,6 +3,10 @@ using System; namespace Barotrauma { + /// + /// A special affliction type that increases the duration of buffs (afflictions of the type "buff"). The increase is defined using the + /// and attributes of the affliction effect. + /// class BuffDurationIncrease : Affliction { public BuffDurationIncrease(AfflictionPrefab prefab, float strength) : base(prefab, strength) @@ -20,9 +24,9 @@ namespace Barotrauma { foreach (Affliction affliction in afflictions) { - if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) { continue; } - affliction.MultiplierSource = null; - affliction.StrengthDiminishMultiplier = 1f; + if (!affliction.Prefab.IsBuff || affliction == this || affliction.StrengthDiminishMultiplier.Source != this) { continue; } + affliction.StrengthDiminishMultiplier.Source = null; + affliction.StrengthDiminishMultiplier.Value = 1f; } } else @@ -31,10 +35,10 @@ namespace Barotrauma { if (!affliction.Prefab.IsBuff || affliction == this) { continue; } float multiplier = GetDiminishMultiplier(); - if (affliction.StrengthDiminishMultiplier < multiplier && affliction.MultiplierSource != this) { continue; } + if (affliction.StrengthDiminishMultiplier.Value < multiplier && affliction.StrengthDiminishMultiplier.Source != this) { continue; } - affliction.MultiplierSource = this; - affliction.StrengthDiminishMultiplier = multiplier; + affliction.StrengthDiminishMultiplier.Source = this; + affliction.StrengthDiminishMultiplier.Value = multiplier; } } } @@ -48,7 +52,7 @@ namespace Barotrauma float multiplier = MathHelper.Lerp( currentEffect.MinBuffMultiplier, currentEffect.MaxBuffMultiplier, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.GetStrengthFactor(this)); return 1.0f / Math.Max(multiplier, 0.001f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index dcfea21dc..250337640 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -184,7 +184,7 @@ namespace Barotrauma } } - public Color DefaultFaceTint = Color.TransparentBlack; + public static readonly Color DefaultFaceTint = Color.TransparentBlack; public Color FaceTint { @@ -339,12 +339,12 @@ namespace Barotrauma return null; } - public T GetAffliction(string identifier, bool allowLimbAfflictions = true) where T : Affliction + public T GetAffliction(Identifier identifier, bool allowLimbAfflictions = true) where T : Affliction { return GetAffliction(identifier, allowLimbAfflictions) as T; } - public Affliction GetAffliction(string identifier, Limb limb) + public Affliction GetAffliction(Identifier identifier, Limb limb) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { @@ -380,7 +380,7 @@ namespace Barotrauma /// The limb the affliction is attached to /// Does the affliction have to be attached to only the specific limb. /// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb. - public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific) + public float GetAfflictionStrength(Identifier afflictionType, Limb limb, bool requireLimbSpecific) { if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; } @@ -401,7 +401,7 @@ namespace Barotrauma return strength; } - public float GetAfflictionStrength(string afflictionType, bool allowLimbAfflictions = true) + public float GetAfflictionStrength(Identifier afflictionType, bool allowLimbAfflictions = true) { float strength = 0.0f; foreach (KeyValuePair kvp in afflictions) @@ -449,7 +449,11 @@ namespace Barotrauma var affliction = kvp.Key; resistance += affliction.GetResistance(afflictionPrefab.Identifier); } - return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); + + resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); + if (resistance > 1f) { resistance = 1f; } + + return resistance; } public float GetStatValue(StatTypes statType) @@ -483,16 +487,19 @@ namespace Barotrauma ReduceMatchingAfflictions(amount, treatmentAction); } - public void ReduceAfflictionOnAllLimbs(Identifier affliction, float amount, ActionType? treatmentAction = null) + public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) { - if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } - + if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } + matchingAfflictions.Clear(); - matchingAfflictions.AddRange(afflictions.Keys); - matchingAfflictions.RemoveAll(a => - a.Prefab.Identifier != affliction && - a.Prefab.AfflictionType != affliction); - + foreach (var affliction in afflictions) + { + if (affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) + { + matchingAfflictions.Add(affliction.Key); + } + } + ReduceMatchingAfflictions(amount, treatmentAction); } @@ -509,18 +516,21 @@ namespace Barotrauma ReduceMatchingAfflictions(amount, treatmentAction); } - public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier affliction, float amount, ActionType? treatmentAction = null) + public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) { - if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } + if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } - + matchingAfflictions.Clear(); - matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); - - matchingAfflictions.RemoveAll(a => - a.Prefab.Identifier != affliction && - a.Prefab.AfflictionType != affliction); - + var targetLimbHealth = limbHealths[targetLimb.HealthIndex]; + foreach (var affliction in afflictions) + { + if ((affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) && + affliction.Value == targetLimbHealth) + { + matchingAfflictions.Add(affliction.Key); + } + } ReduceMatchingAfflictions(amount, treatmentAction); } @@ -622,7 +632,7 @@ namespace Barotrauma KillIfOutOfVitality(); } - public float GetLimbDamage(Limb limb, string afflictionType = null) + public float GetLimbDamage(Limb limb, Identifier afflictionType) { float damageStrength; if (limb.IsSevered) @@ -635,16 +645,16 @@ namespace Barotrauma // Therefore with e.g. 80 health, the max damage per limb would be 40. // Having at least 40 damage on both legs would cause maximum limping. float max = MaxVitality / 2; - if (string.IsNullOrEmpty(afflictionType)) + if (afflictionType.IsEmpty) { - float damage = GetAfflictionStrength("damage", limb, true); - float bleeding = GetAfflictionStrength("bleeding", limb, true); - float burn = GetAfflictionStrength("burn", limb, true); + float damage = GetAfflictionStrength(AfflictionPrefab.DamageType, limb, true); + float bleeding = GetAfflictionStrength(AfflictionPrefab.BleedingType, limb, true); + float burn = GetAfflictionStrength(AfflictionPrefab.BurnType, limb, true); damageStrength = Math.Min(damage + bleeding + burn, max); } else { - damageStrength = Math.Min(GetAfflictionStrength("damage", limb, true), max); + damageStrength = Math.Min(GetAfflictionStrength(afflictionType, limb, true), max); } return damageStrength / max; } @@ -701,22 +711,17 @@ namespace Barotrauma if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; } if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } - if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") + if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType) { - if (Character.EmpVulnerability <= 0 || GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) - { - return; - } - } - if (Character.Params.Health.PoisonImmunity && (newAffliction.Prefab.AfflictionType == "poison" || newAffliction.Prefab.AfflictionType == "paralysis")) { return; } - if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; } - if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) - { - if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName)) + if (Character.EmpVulnerability <= 0 || GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } } + if (Character.Params.Health.PoisonImmunity && + (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { return; } + if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; } + if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; } Affliction existingAffliction = null; foreach (KeyValuePair kvp in afflictions) @@ -753,7 +758,9 @@ namespace Barotrauma Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), newAffliction.Source); afflictions.Add(copyAffliction, limbHealth); - + SteamAchievementManager.OnAfflictionReceived(copyAffliction, Character); + MedicalClinic.OnAfflictionCountChanged(Character); + Character.HealthUpdateInterval = 0.0f; CalculateVitality(); @@ -826,10 +833,16 @@ namespace Barotrauma } Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } + foreach (var affliction in afflictionsToRemove) { afflictions.Remove(affliction); - } + } + + if (afflictionsToRemove.Count is not 0) + { + MedicalClinic.OnAfflictionCountChanged(Character); + } } Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed)); @@ -883,6 +896,11 @@ namespace Barotrauma } } + /// + /// 0-1. + /// + public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab); + private void UpdateOxygen(float deltaTime) { if (!Character.NeedsOxygen) @@ -1137,16 +1155,14 @@ namespace Barotrauma } } - public IEnumerable GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys); - private readonly HashSet afflictionTags = new HashSet(); - public IEnumerable GetActiveAfflictionTags(IEnumerable afflictions) + public IEnumerable GetActiveAfflictionTags() { afflictionTags.Clear(); - foreach (Affliction affliction in afflictions) + foreach (Affliction affliction in afflictions.Keys) { var currentEffect = affliction.GetActiveEffect(); - if (currentEffect != null && !currentEffect.Tag.IsEmpty) + if (currentEffect is { Tag.IsEmpty: false }) { afflictionTags.Add(currentEffect.Tag); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index e3d7bedf4..e7d13cf51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -27,6 +27,32 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.No)] public float AimAccuracy { get; protected set; } + [Serialize(1f, IsPropertySaveable.No)] + public float SkillMultiplier { get; protected set; } + + [Serialize(0, IsPropertySaveable.No)] + public int ExperiencePoints { get; private set; } + + private readonly HashSet tags = new HashSet(); + + [Serialize("", IsPropertySaveable.Yes)] + public string Tags + { + get => string.Join(",", tags); + set + { + tags.Clear(); + if (!string.IsNullOrWhiteSpace(value)) + { + string[] splitTags = value.Split(','); + foreach (var tag in splitTags) + { + tags.Add(tag.ToIdentifier()); + } + } + } + } + private readonly HashSet moduleFlags = new HashSet(); [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the NPC prefer to spawn in.")] @@ -79,6 +105,15 @@ namespace Barotrauma public Identifier[] PreferredOutpostModuleTypes { get; protected set; } + [Serialize("", IsPropertySaveable.No)] + public Identifier Faction { get; set; } + + [Serialize("", IsPropertySaveable.No)] + public Identifier Group { get; set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool AllowDraggingIndefinitely { get; set; } + public XElement Element { get; protected set; } @@ -97,6 +132,11 @@ namespace Barotrauma this.NpcSetIdentifier = npcSetIdentifier; } + public IEnumerable GetTags() + { + return tags; + } + public IEnumerable GetModuleFlags() { return moduleFlags; @@ -148,7 +188,7 @@ namespace Barotrauma } } - public bool GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) + public bool GiveItems(Character character, Submarine submarine, WayPoint spawnPoint, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) { if (ItemSets == null || !ItemSets.Any()) { return false; } var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element; @@ -159,7 +199,7 @@ namespace Barotrauma int amount = itemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) { - InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents); + InitializeItem(character, itemElement, submarine, this, spawnPoint, createNetworkEvents: createNetworkEvents); } } } @@ -177,17 +217,27 @@ namespace Barotrauma CharacterInfo characterInfo; if (characterElement == null) { - characterInfo= new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier); + characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier, randSync: randSync); } else { characterInfo = new CharacterInfo(characterElement, Identifier); } + if (characterInfo.Job != null && !MathUtils.NearlyEqual(SkillMultiplier, 1.0f)) + { + foreach (var skill in characterInfo.Job.GetSkills()) + { + float newSkill = skill.Level * SkillMultiplier; + skill.IncreaseSkill(newSkill - skill.Level, increasePastMax: false); + } + characterInfo.Salary = characterInfo.CalculateSalary(); + } characterInfo.HumanPrefabIds = (NpcSetIdentifier, Identifier); + characterInfo.GiveExperience(ExperiencePoints); return characterInfo; } - public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, Item parentItem = null, bool createNetworkEvents = true) + public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true) { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); @@ -231,7 +281,7 @@ namespace Barotrauma IdCard idCardComponent = item.GetComponent(); if (idCardComponent != null) { - idCardComponent.Initialize(null, character); + idCardComponent.Initialize(spawnPoint, character); if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost)) { idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag; @@ -254,7 +304,7 @@ namespace Barotrauma int amount = childItemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) { - InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents); + InitializeItem(character, childItemElement, submarine, humanPrefab, spawnPoint, item, createNetworkEvents); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 2db2aaadf..e311800bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -50,7 +50,7 @@ namespace Barotrauma public override void Dispose() { } } - class JobVariant + internal class JobVariant { public JobPrefab Prefab; public int Variant; @@ -113,7 +113,7 @@ namespace Barotrauma public readonly LocalizedString Name; - [Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)] + [Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No, description: "How should the character behave when idling (not doing any particular task)?")] public AIObjectiveIdle.BehaviorType IdleBehavior { get; @@ -122,78 +122,63 @@ namespace Barotrauma public readonly LocalizedString Description; - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Can the character speak any random lines, or just ones specifically meant for the job?")] public bool OnlyJobSpecificDialog { get; private set; } - //the number of these characters in the crew the player starts with in the single player campaign - [Serialize(0, IsPropertySaveable.No)] + [Serialize(0, IsPropertySaveable.No, description: "The number of these characters in the crew the player starts with in the single player campaign.")] public int InitialCount { get; private set; } - //if set to true, a client that has chosen this as their preferred job will get it no matter what - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "If set to true, a client that has chosen this as their preferred job will get it regardless of the maximum number or the amount of spawnpoints in the sub.")] public bool AllowAlways { get; private set; } - //how many crew members can have the job (only one captain etc) - [Serialize(100, IsPropertySaveable.No)] + [Serialize(100, IsPropertySaveable.No, description: "How many crew members can have the job (e.g. only one captain etc).")] public int MaxNumber { get; private set; } - //how many crew members are REQUIRED to have the job - //(i.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference) - [Serialize(0, IsPropertySaveable.No)] + [Serialize(0, IsPropertySaveable.No, description: "How many crew members are required to have the job. I.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference.")] public int MinNumber { get; private set; } - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Minimum amount of karma a player must have to get assigned this job.")] public float MinKarma { get; private set; } - [Serialize(1.0f, IsPropertySaveable.No)] + [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplier on the base hiring cost when hiring the character from an outpost.")] public float PriceMultiplier { get; private set; } - // TODO: not used - [Serialize(10.0f, IsPropertySaveable.No)] - public float Commonness - { - get; - private set; - } - - //how much the vitality of the character is increased/reduced from the default value - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much the vitality of the character is increased/reduced from the default value (e.g. 10 = 110 total vitality if the default vitality is 100.).")] public float VitalityModifier { get; private set; } - //whether the job should be available to NPCs - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Hidden jobs are not selectable by players, but can be used by e.g. outpost NPCs.")] public bool HiddenJob { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index 41a894960..e8e08f791 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -21,7 +21,7 @@ namespace Barotrauma level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill); } - private Identifier iconJobId; + private readonly Identifier iconJobId; public Sprite Icon => !iconJobId.IsEmpty && JobPrefab.Prefabs.TryGet(iconJobId, out var jobPrefab) ? jobPrefab.Icon diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 79d2eadc6..5cea1b66f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -666,13 +666,13 @@ namespace Barotrauma switch (body.BodyShape) { case PhysicsBody.Shape.Circle: - attack.DamageRange = body.radius; + attack.DamageRange = body.Radius; break; case PhysicsBody.Shape.Capsule: - attack.DamageRange = body.height / 2 + body.radius; + attack.DamageRange = body.Height / 2 + body.Radius; break; case PhysicsBody.Shape.Rectangle: - attack.DamageRange = new Vector2(body.width / 2.0f, body.height / 2.0f).Length(); + attack.DamageRange = new Vector2(body.Width / 2.0f, body.Height / 2.0f).Length(); break; } attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); @@ -786,11 +786,12 @@ namespace Barotrauma } if (!foundMatchingModifier && random > affliction.Probability) { continue; } float finalDamageModifier = damageMultiplier; - if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == "emp") + if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { finalDamageModifier *= character.EmpVulnerability; } - if (!character.Params.Health.PoisonImmunity && (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis")) + if (!character.Params.Health.PoisonImmunity && + (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { finalDamageModifier *= character.PoisonVulnerability; } @@ -1108,7 +1109,7 @@ namespace Barotrauma Vector2 forceWorld = attack.CalculateAttackPhase(attack.RootTransitionEasing); forceWorld.X *= character.AnimController.Dir; character.AnimController.MainLimb.body.ApplyLinearImpulse(character.Mass * forceWorld, character.SimPosition, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - if (!attack.IsRunning) + if (!attack.IsRunning && !attack.Ranged) { // Set the main collider where the body lands after the attack if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f) @@ -1225,7 +1226,7 @@ namespace Barotrauma if (statusEffect.type == ActionType.OnDamaged) { if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; } - if (statusEffect.OnlyPlayerTriggered) + if (statusEffect.OnlyWhenDamagedByPlayer) { if (character.LastAttacker == null || !character.LastAttacker.IsPlayer) { @@ -1303,7 +1304,8 @@ namespace Barotrauma } private float blinkTimer; - private float blinkPhase; + public float BlinkPhase; + public bool FreezeBlinkState; private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime; @@ -1316,16 +1318,25 @@ namespace Barotrauma { if (blinkTimer > -TotalBlinkDurationOut) { - blinkPhase -= deltaTime; - if (blinkPhase > 0) + if (!FreezeBlinkState) + { + BlinkPhase -= deltaTime; + } + if (BlinkPhase > 0) { // in - float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, blinkPhase / Params.BlinkDurationIn)); + float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, BlinkPhase / Params.BlinkDurationIn)); body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true); + if (Params.UseTextureOffsetForBlinking) + { +#if CLIENT + ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetOut, Params.BlinkTextureOffsetIn, t); +#endif + } } else { - if (Math.Abs(blinkPhase) < Params.BlinkHoldTime) + if (Math.Abs(BlinkPhase) < Params.BlinkHoldTime) { // hold body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce, wrapAngle: true); @@ -1333,15 +1344,25 @@ namespace Barotrauma else { // out - float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut)); + //float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut)); + float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, (-BlinkPhase - Params.BlinkHoldTime) / Params.BlinkDurationOut)); body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true); + if (Params.UseTextureOffsetForBlinking) + { +#if CLIENT + ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetIn, Params.BlinkTextureOffsetOut, t); +#endif + } } } } else { // out - blinkPhase = Params.BlinkDurationIn; + if (!FreezeBlinkState) + { + BlinkPhase = Params.BlinkDurationIn; + } body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce, wrapAngle: true); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index e844e53a0..e902ba353 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -177,6 +177,9 @@ namespace Barotrauma set => SetFootAngles(FootAnglesInRadians, value); } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the animation be updated even if the character is not moving?"), Editable] + public bool UpdateAnimationWhenNotMoving { get; set; } + /// /// Key = limb id, value = angle in radians /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 04204dc96..ba88edee5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -56,6 +56,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No), Editable] public bool CanSpeak { get; set; } + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool ShowHealthBar { get; private set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool UseBossHealthBar { get; private set; } @@ -110,6 +113,12 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool DrawLast { get; set; } + [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons. Defaults to 1. Set 0 to tell the bots not to target this character at all. Distance to the target affects the decision making."), Editable] + public float AITurretPriority { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable] + public float AISlowTurretPriority { get; set; } + public readonly CharacterFile File; public XDocument VariantFile { get; private set; } @@ -214,7 +223,7 @@ namespace Barotrauma return true; } - public bool CompareGroup(Identifier group) => group != Identifier.Empty && Group != Identifier.Empty && group == Group; + public static bool CompareGroup(Identifier group1, Identifier group2) => group1 != Identifier.Empty && group2 != Identifier.Empty && group1 == group2; protected void CreateSubParams() { @@ -476,7 +485,7 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes), Editable] public bool DoesBleed { get; set; } - [Serialize(float.NegativeInfinity, IsPropertySaveable.Yes), Editable(minValue: float.NegativeInfinity, maxValue: 0)] + [Serialize(float.PositiveInfinity, IsPropertySaveable.Yes), Editable(minValue: 0, maxValue: float.PositiveInfinity)] public float CrushDepth { get; set; } // Make editable? @@ -512,7 +521,20 @@ namespace Barotrauma // TODO: limbhealths, sprite? - public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) { } + public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) + { + //backwards compatibility + if (CrushDepth < 0) + { + //invert y, convert to meters, and add 1000 to be on the safe side (previously the value would be from the bottom of the level) + float newCrushDepth = -CrushDepth * Physics.DisplayToRealWorldRatio + 1000; + DebugConsole.AddWarning($"Character \"{character.SpeciesName}\" has a negative crush depth. "+ + "Previously the crush depths were defined as display units (e.g. -30000 would correspond to 300 meters below the level), "+ + "but now they're in meters (e.g. 3000 would correspond to a depth of 3000 meters displayed on the nav terminal). "+ + $"Changing the crush depth from {CrushDepth} to {newCrushDepth}."); + CrushDepth = newCrushDepth; + } + } } public class InventoryParams : SubParam @@ -615,6 +637,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] public bool CanOpenDoors { get; private set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool UsePathFindingToGetInside { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable] public bool KeepDoorsClosed { get; private set; } @@ -823,10 +848,19 @@ namespace Barotrauma [Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] public float CircleStartDistance { get; private set; } - [Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)] + [Serialize(false, IsPropertySaveable.Yes, description:"Normally the target size is taken into account when calculating the distance to the target. Set this true to skip that.")] + public bool IgnoreTargetSize { get; private set; } + + [Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)] public float CircleRotationSpeed { get; private set; } - [Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)] + [Serialize(false, IsPropertySaveable.Yes, description:"When enabled, the circle rotation speed can change when the target is far. When this setting is disabled (default), the character will head directly towards the target when it's too far."), Editable] + public bool DynamicCircleRotationSpeed { get; private set; } + + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)] + public float CircleRandomRotationFactor { get; private set; } + + [Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)] public float CircleStrikeDistanceMultiplier { get; private set; } [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index d8f8992b0..be86bd432 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -6,6 +6,7 @@ using System.Linq; using Barotrauma.IO; using System.Xml; using Barotrauma.Extensions; +using FarseerPhysics; #if CLIENT using Barotrauma.SpriteDeformations; #endif @@ -621,13 +622,13 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float SteerForce { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Radius { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Height { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Width { get; set; } [Serialize(10f, IsPropertySaveable.Yes, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)] @@ -706,6 +707,15 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool OnlyBlinkInWater { get; set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool UseTextureOffsetForBlinking { get; set; } + + [Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)] + public Vector2 BlinkTextureOffsetIn { get; set; } + + [Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)] + public Vector2 BlinkTextureOffsetOut { get; set; } + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable] public TransitionMode BlinkTransitionIn { get; private set; } @@ -1026,15 +1036,18 @@ namespace Barotrauma } } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Radius { get; set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Height { get; set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Width { get; set; } + [Serialize(BodyType.Dynamic, IsPropertySaveable.Yes), Editable] + public BodyType BodyType { get; set; } + public ColliderParams(ContentXElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll) { Name = name; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index c07128f0f..26af153b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -19,15 +19,9 @@ namespace Barotrauma.Abilities foreach (XElement subElement in conditionElement.Elements()) { - if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase)) + if (subElement.NameAsIdentifier() == "conditional") { - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionals.Add(new PropertyConditional(attribute)); - } - } + conditionals.AddRange(PropertyConditional.FromXElement(subElement)); } } 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 23512f751..a71667245 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -14,45 +14,51 @@ namespace Barotrauma.Abilities { string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!; HashSet missionTypes = new HashSet(); + isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false); + foreach (string missionTypeString in missionTypeStrings) { if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None) { - DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type."); - return; + if (!isAffiliated) + { + DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type."); + } + continue; } missionTypes.Add(parsedMission); } missionType = missionTypes.ToImmutableHashSet(); - isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { if (abilityObject is IAbilityMission { Mission: { } mission }) { - if (isAffiliated) + if (!isAffiliated) { return CheckMissionType(); } + + if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } + + foreach (var (factionIdentifier, amount) in mission.ReputationRewards) { - if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } - - foreach (var (factionIdentifier, amount) in mission.ReputationRewards) + if (amount <= 0) { continue; } + if (GetMatchingFaction(factionIdentifier) is { } faction && + Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) { - if (amount <= 0) { continue; } - - Faction faction = factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier); - - if (faction?.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated) - { - return true; - } + return CheckMissionType(); } - - return false; } - return missionType.Contains(mission.Prefab.Type); + return false; + + Faction GetMatchingFaction(Identifier factionIdentifier) => + factionIdentifier == "location" + ? mission.OriginLocation?.Faction + : factions.FirstOrDefault(f => factionIdentifier == f.Prefab.Identifier); + + bool CheckMissionType() => missionType.IsEmpty || missionType.Contains(mission.Prefab.Type); } LogAbilityConditionError(abilityObject, typeof(IAbilityMission)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs index c4017a87f..237e15b5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -23,7 +23,6 @@ protected override bool MatchesConditionSpecific() { Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier); - return character.Info.GetSavedStatValue(statType, identifier) >= min; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs index 55c382e2a..7a4ceeb07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -4,6 +4,7 @@ using System; using Barotrauma.Extensions; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Barotrauma.Abilities { @@ -29,11 +30,33 @@ namespace Barotrauma.Abilities if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; } + ImmutableHashSet characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + HashSet> talentsTrees = new HashSet>(); foreach (TalentSubTree subTree in talentTree.TalentSubTrees) { if (subTree.Type != TalentTreeType.Specialization) { continue; } - talentsTrees.Add(subTree.AllTalentIdentifiers); + + HashSet identifiers = new HashSet(); + foreach (TalentOption option in subTree.TalentOptionStages) + { + foreach (Identifier identifier in option.TalentIdentifiers) + { + if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; } + + identifiers.Add(identifier); + } + + foreach (var (_, value) in option.ShowCaseTalents) + { + var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet(); + if (ids.Count is 0) { continue; } + + identifiers.Add(value.GetRandomUnsynced()); + } + } + + talentsTrees.Add(identifiers.ToImmutableHashSet()); } ImmutableHashSet selectedTalentTree = talentsTrees.GetRandomUnsynced(); @@ -44,6 +67,16 @@ namespace Barotrauma.Abilities Character.GiveTalent(identifier); } + + static bool IsShowCaseTalent(Identifier identifier, TalentOption option) + { + foreach (var (_, value) in option.ShowCaseTalents) + { + if (value.Contains(identifier)) { return true; } + } + + return false; + } } protected override void ApplyEffect(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index dc93405b4..d14c9df8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -131,6 +131,8 @@ 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; } + foreach (var subTree in talentTree!.TalentSubTrees) { if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; } @@ -152,6 +154,18 @@ namespace Barotrauma return false; } + public static bool IsTalentLocked(Identifier talentIdentifier, ImmutableHashSet characterList = null) + { + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); + + foreach (Character c in characterList) + { + if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; } + } + + return false; + } + public static List CheckTalentSelection(Character controlledCharacter, IEnumerable selectedTalents) { List viableTalents = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SlideshowsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SlideshowsFile.cs new file mode 100644 index 000000000..4c1ff0527 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SlideshowsFile.cs @@ -0,0 +1,15 @@ +namespace Barotrauma +{ + sealed class SlideshowsFile : GenericPrefabFile + { + protected override PrefabCollection Prefabs => SlideshowPrefab.Prefabs; + + public SlideshowsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "Slideshow"; + + protected override bool MatchesPlural(Identifier identifier) => identifier == "Slideshows"; + + protected override SlideshowPrefab CreatePrefab(ContentXElement element) => new SlideshowPrefab(this, element); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 88ff41f52..8ea7cb235 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -116,9 +116,12 @@ namespace Barotrauma public static void ThrowIfDuplicates(IEnumerable pkgs) { var contentPackages = pkgs as IList ?? pkgs.ToArray(); - if (contentPackages.Any(p1 => contentPackages.AtLeast(2, p2 => p1 == p2))) + foreach (ContentPackage cp in contentPackages) { - throw new InvalidOperationException($"Input contains duplicate packages"); + if (contentPackages.AtLeast(2, cp2 => cp == cp2)) + { + throw new InvalidOperationException($"Input contains duplicate packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})"); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index 428050ab2..74937c8ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -1,12 +1,11 @@ #nullable enable +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reflection.Metadata.Ecma335; using System.Xml.Linq; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -64,9 +63,14 @@ namespace Barotrauma public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def); public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def); - public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); - [return:NotNullIfNotNull("def")] - public ImmutableHashSet? GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim); + + [return: NotNullIfNotNull("def")] + public Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys) => Element.GetAttributeIdentifierArray(def, keys); + [return: NotNullIfNotNull("def")] + public Identifier[] GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); + [return: NotNullIfNotNull("def")] + public ImmutableHashSet GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim); + public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def); public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def); public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index a6cb260a5..0cb3e03cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -384,7 +384,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - PermissionPreset.List.Select(pp => pp.Name.Value).ToArray() + PermissionPreset.List.Select(pp => pp.DisplayName.Value).ToArray() }; })); @@ -737,7 +737,7 @@ namespace Barotrauma commands.Add(new Command("revive", "revive [character name]: Bring the specified character back from the dead. If the name parameter is omitted, the controlled character will be revived.", (string[] args) => { Character revivedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); - if (revivedCharacter == null) return; + if (revivedCharacter == null) { return; } revivedCharacter.Revive(); #if SERVER @@ -745,7 +745,7 @@ namespace Barotrauma { foreach (Client c in GameMain.Server.ConnectedClients) { - if (c.Character != revivedCharacter) continue; + if (c.Character != revivedCharacter) { continue; } //clients stop controlling the character when it dies, force control back GameMain.Server.SetClientCharacter(c, revivedCharacter); @@ -889,7 +889,15 @@ namespace Barotrauma ThrowError("Please specify an identifier and a value."); return; } - SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), args[1], SetDataAction.OperationType.Set); + if (float.TryParse(args[1], out float floatVal)) + { + SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), floatVal, SetDataAction.OperationType.Set); + } + else + { + SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), args[1], SetDataAction.OperationType.Set); + } + }, isCheat: true)); commands.Add(new Command("setskill", "setskill [all/identifier] [max/level] [character]: Set your skill level.", (string[] args) => @@ -1091,11 +1099,6 @@ namespace Barotrauma commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => { if (Submarine.MainSub == null) { return; } - if (Level.Loaded?.Type == LevelData.LevelType.Outpost && GameMain.GameSession != null) - { - NewMessage("The teleportsub command is unavailable in outpost levels!", Color.Red); - return; - } if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) { @@ -1260,6 +1263,22 @@ namespace Barotrauma } #endif + commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) => + { + if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + NewMessage("Reputation:"); + foreach (var faction in campaign.Factions) + { + NewMessage($" - {faction.Prefab.Name}: {faction.Reputation.Value}"); + } + } + else + { + ThrowError("Could not show reputation (no active campaign)."); + } + }, null)); + commands.Add(new Command("setlocationreputation", "setlocationreputation [value]: Set the reputation in the current location to the specified value.", (string[] args) => { if (GameMain.GameSession?.GameMode is CampaignMode campaign) @@ -1267,7 +1286,7 @@ namespace Barotrauma if (args.Length == 0) { return; } if (float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation)) { - campaign.Map.CurrentLocation.Reputation.SetReputation(reputation); + campaign.Map.CurrentLocation.Reputation?.SetReputation(reputation); } else { @@ -1424,7 +1443,7 @@ namespace Barotrauma commands.Add(new Command("kill", "kill [character]: Immediately kills the specified character.", (string[] args) => { Character killedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); - killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); + killedCharacter?.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null); }, () => { @@ -1860,6 +1879,7 @@ namespace Barotrauma commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true)); commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true)); commands.Add(new Command("debugdrawlocalization", "Toggle the localization debug drawing mode on/off (client-only). Colors all text that hasn't been fetched from a localization file magenta, making it easier to spot hard-coded or missing texts.", null, isCheat: false)); + commands.Add(new Command("debugdrawlos", "Toggle the los debug drawing mode on/off (client-only).", null, isCheat: true)); commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false)); commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null)); commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null)); @@ -1869,6 +1889,8 @@ namespace Barotrauma commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true)); commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true)); commands.Add(new Command("devmode", "Toggle the dev mode on/off (client-only).", null, isCheat: true)); + commands.Add(new Command("showmonsters", "Permanently unlocks all the monsters in the character editor. Use \"hidemonsters\" to undo.", null, isCheat: true)); + commands.Add(new Command("hidemonsters", "Permanently hides in the character editor all the monsters that haven't been encountered in the game. Use \"showmonsters\" to undo.", null, isCheat: true)); InitProjectSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index b9695d7dd..e20724d51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -12,21 +12,112 @@ namespace Barotrauma Exponential } + /// + /// ActionTypes define when a is executed. + /// public enum ActionType { - Always = 0, OnPicked = 1, OnUse = 2, OnSecondaryUse = 3, - OnWearing = 4, OnContaining = 5, OnContained = 6, OnNotContained = 7, - OnActive = 8, OnFailure = 9, OnBroken = 10, - OnFire = 11, InWater = 12, NotInWater = 13, + /// + /// Executes every frame regardless of the state of the entity. + /// + Always = 0, + /// + /// Executes when the item is picked up. Only valid for items. + /// + OnPicked = 1, + /// + /// Executes when the item is used. The meaning of "using" an item depends on the item, but generally it means the action that happens when holding the item and clicking LMB. Only valid for items. + /// + OnUse = 2, + /// + /// Executes when an item is held and the aim key is held. Only valid for items. + /// + OnSecondaryUse = 3, + /// + /// Executes continuously while the item is being worn. Only valid for wearable items. + /// + OnWearing = 4, + /// + /// Executes continuously when a specific Containable is inside an ItemContainer. Only valid for Containables defined in an ItemContainer component. + /// + OnContaining = 5, + /// + /// Executes continuously when the item is contained in some inventory. Only valid for items. + /// + OnContained = 6, + /// + /// Executes continuously when the item is NOT contained in an inventory. Only valid for items. + /// + OnNotContained = 7, + /// + /// Executes continuously when the item is active. The meaning of "active" depends on the item, but generally means the item is on, powered, and doing the thing it's intended for. Only valid for items. + /// + OnActive = 8, + /// + /// Executes when using the item fails due to a failed skill check. Only valid for items. + /// + OnFailure = 9, + /// + /// Executes when using the item's condition drops to 0. Only valid for items. + /// + OnBroken = 10, + /// + /// Executes continuously when the entity is within the damage range of fire. Valid for items and characters. + /// + OnFire = 11, + /// + /// Executes continuously when the entity is submerged. Valid for items and characters. + /// + InWater = 12, + /// + /// Executes continuously when the entity is NOT submerged. Valid for items and characters. + /// + NotInWater = 13, + /// + /// Executes when the entity hits something hard enough. For items, the threshold is determined by , + /// for characters by . Valid for items and characters. + /// OnImpact = 14, + /// + /// Executes continuously when the character is eating another character. Only valid for characters. + /// OnEating = 15, + /// + /// Executes when the entity receives damage from an external source (i.e. an affliction that increases in severity, or an item degrading by itself don't count). + /// Valid for items and characters. + /// OnDamaged = 16, + /// + /// Executes when the limb gets severed. Only valid for limbs. + /// OnSevered = 17, + /// + /// Executes when a produces an item (e.g. when a plant grows a fruit). Only valid for Growable items. + /// OnProduceSpawned = 18, - OnOpen = 19, OnClose = 20, + /// + /// Executes when a is opened. Only valid for doors. + /// + OnOpen = 19, + /// + /// Executes when a is closed. Only valid for doors. + /// + OnClose = 20, + /// + /// Executes when the entity spawns. Only valid for doors. + /// OnSpawn = 21, + /// + /// Executes when using the item succeeds based on a skill check. Only valid for items. + /// OnSuccess = 22, + /// + /// Executes when an Ability (an effect from a talent) triggers the status effect. Only valid in Abilities, the target can be either a character or an item depending on the type of Ability. + /// OnAbility = 23, + /// + /// Executes when the character dies. Only valid for characters. + /// OnDeath = OnBroken } @@ -75,86 +166,391 @@ namespace Barotrauma OnStatusEffectIdentifier, } + /// + /// StatTypes are used to alter several traits of a character. They are mostly used by talents. + /// + /// A lot of StatTypes use a "percentage" value. The way this works is that the value is 0 by default and 1 is added to the value of the stat type to get the final multiplier. + /// For example if the value is set to 0.2 then 1 is added to it making it 1.2 and that is used as a multiplier. + /// This makes it so values between -100% and +100% can be easily represented as -1 and 1 respectively. For example 0.5 would translate to 1.5 for +50% and -0.2 would translate to 0.8 for -20% multiplier. + /// public enum StatTypes { + /// + /// Used to indicate an invalid stat type. Should not be used. + /// None, - // Skills + + /// + /// Boosts electrical skill by a flat amount. + /// ElectricalSkillBonus, + + /// + /// Boosts helm skill by a flat amount. + /// HelmSkillBonus, - HelmSkillOverride, - MedicalSkillOverride, - WeaponsSkillOverride, - ElectricalSkillOverride, - MechanicalSkillOverride, + + /// + /// Boosts mechanical skill by a flat amount. + /// MechanicalSkillBonus, + + /// + /// Boosts medical skill by a flat amount. + /// MedicalSkillBonus, + + /// + /// Boosts weapons skill by a flat amount. + /// WeaponsSkillBonus, - // Character attributes + + /// + /// Boosts the character's helm skill to the given value if it's lower than the given value. + /// + HelmSkillOverride, + + /// + /// Boosts the character's medical skill to the given value if it's lower than the given value. + /// + MedicalSkillOverride, + + /// + /// Boosts the character's weapons skill to the given value if it's lower than the given value. + /// + WeaponsSkillOverride, + + /// + /// Boosts the character's electrical skill to the given value if it's lower than the given value. + /// + ElectricalSkillOverride, + + /// + /// Boosts the character's mechanical skill to the given value if it's lower than the given value. + /// + MechanicalSkillOverride, + + /// + /// Increases character's maximum vitality by a percentage. + /// MaximumHealthMultiplier, + + /// + /// Increases both walking and swimming speed of the character by a percentage. + /// MovementSpeed, + + /// + /// Increases the character's walking speed by a percentage. + /// WalkingSpeed, + + /// + /// Increases the character's swimming speed by a percentage. + /// SwimmingSpeed, + + /// + /// Decreases how long it takes for buffs applied to the character decay over time by a percentage. + /// Buffs are afflictions that have isBuff set to true. + /// BuffDurationMultiplier, + + /// + /// Decreases how long it takes for debuff applied to the character decay over time by a percentage. + /// Debuffs are afflictions that have isBuff set to false. + /// DebuffDurationMultiplier, + + /// + /// Increases the strength of afflictions that are applied to the character by a percentage. + /// Medicines are items that have the "medical" tag. + /// MedicalItemEffectivenessMultiplier, + + /// + /// Increases the resistance to pushing force caused by flowing water by a percentage. The resistance cannot be below 0% or higher than 100%. + /// FlowResistance, - // Combat + + /// + /// Increases how much damage the character deals via all attacks by a percentage. + /// AttackMultiplier, + + /// + /// Increases how much damage the character deals to other characters on the same team by a percentage. + /// TeamAttackMultiplier, + + /// + /// Decreases the reload time of ranged weapons held by the character by a percentage. + /// RangedAttackSpeed, + + /// + /// Decreases the reload time of submarine turrets operated by the character by a percentage. + /// TurretAttackSpeed, + + /// + /// Decreases the power consumption of submarine turrets operated by the character by a percentage. + /// TurretPowerCostReduction, + + /// + /// Increases how fast submarine turrets operated by the character charge up by a percentage. Affects turrets like pulse laser. + /// TurretChargeSpeed, + + /// + /// Increases how fast the character can swing melee weapons by a percentage. + /// MeleeAttackSpeed, + + /// + /// Increases the damage dealt by melee weapons held by the character by a percentage. + /// MeleeAttackMultiplier, - RangedAttackMultiplier, + + /// + /// Decreases the spread of ranged weapons held by the character by a percentage. + /// RangedSpreadReduction, - // Utility + + /// + /// Increases the repair speed of the character by a percentage. + /// RepairSpeed, + + /// + /// Increases the repair speed of the character when repairing mechanical items by a percentage. + /// MechanicalRepairSpeed, + + /// + /// Increase deconstruction speed of deconstructor operated by the character by a percentage. + /// DeconstructorSpeedMultiplier, + + /// + /// Increases the repair speed of repair tools that fix submarine walls by a percentage. + /// RepairToolStructureRepairMultiplier, + + /// + /// Increases the wall damage of tools that destroy submarine walls like plasma cutter by a percentage. + /// RepairToolStructureDamageMultiplier, + + /// + /// Increase the detach speed of items like minerals that require a tool to detach from the wall by a percentage. + /// RepairToolDeattachTimeMultiplier, + + /// + /// Allows the character to repair mechanical items past the maximum condition by a flat percentage amount. For example setting this to 0.1 allows the character to repair mechanical items to 110% condition. + /// MaxRepairConditionMultiplierMechanical, + + /// + /// Allows the character to repair electrical items past the maximum condition by a flat percentage amount. For example setting this to 0.1 allows the character to repair electrical items to 110% condition. + /// MaxRepairConditionMultiplierElectrical, + + /// + /// Increase the the quality of items crafted by the character by a flat amount. + /// Can be made to only affect certain item with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all items. + /// IncreaseFabricationQuality, + + /// + /// Boosts the condition of genes combined by the character by a flat amount. + /// GeneticMaterialRefineBonus, + + /// + /// Reduces the chance to taint a gene when combining genes by a percentage. Tainting probability can not go below 0% or above 100%. + /// GeneticMaterialTaintedProbabilityReductionOnCombine, + + /// + /// Increases the speed at which the character gains skills by a percentage. + /// SkillGainSpeed, + + /// + /// Whenever the character's skill level up add a flat amount of more skill levels to the character. + /// ExtraLevelGain, + + /// + /// Increases the speed at which the character gains helm skill by a percentage. + /// HelmSkillGainSpeed, + + /// + /// Increases the speed at which the character gains weapons skill by a percentage. + /// WeaponsSkillGainSpeed, + + /// + /// Increases the speed at which the character gains medical skill by a percentage. + /// MedicalSkillGainSpeed, + + /// + /// Increases the speed at which the character gains electrical skill by a percentage. + /// ElectricalSkillGainSpeed, + + /// + /// Increases the speed at which the character gains mechanical skill by a percentage. + /// MechanicalSkillGainSpeed, + + /// + /// Increases the strength of afflictions the character applies to other characters via medicine by a percentage. + /// Medicines are items that have the "medical" tag. + /// MedicalItemApplyingMultiplier, - MedicalItemDurationMultiplier, + + /// + /// Increases the strength of afflictions the character applies to other characters via medicine by a percentage. + /// Works only for afflictions that have isBuff set to true. + /// + BuffItemApplyingMultiplier, + + /// + /// Increases the strength of afflictions the character applies to other characters via medicine by a percentage. + /// Works only for afflictions that have "poison" type. + /// PoisonMultiplier, - // Tinker + + /// + /// Increases how long the character can tinker with items by a flat amount where 1 = 1 second. + /// TinkeringDuration, + + /// + /// Increases the effectiveness of the character's tinkerings by a percentage. + /// Tinkering strength affects the speed and effectiveness of the item that is being tinkered with. + /// TinkeringStrength, + + /// + /// Increases how much condition tinkered items lose when the character tinkers with them by a percentage. + /// TinkeringDamage, - // Misc + + /// + /// Increases how much reputation the character gains by a percentage. + /// Can be made to only affect certain factions with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all factions. + /// ReputationGainMultiplier, + + /// + /// Increases how much reputation the character loses by a percentage. + /// Can be made to only affect certain factions with a given tag types by specifying a tag via CharacterAbilityGivePermanentStat, when no tag is specified the ability affects all factions. + /// ReputationLossMultiplier, + + /// + /// Increases how much money the character gains from missions by a percentage. + /// MissionMoneyGainMultiplier, + + /// + /// Increases how much talent experience the character gains from all sources by a percentage. + /// ExperienceGainMultiplier, + + /// + /// Increases how much talent experience the character gains from missions by a percentage. + /// MissionExperienceGainMultiplier, + + /// + /// Increases how many missions the characters crew can have at the same time by a flat amount. + /// ExtraMissionCount, + + /// + /// Increases how many items are in stock in special sales in the store by a flat amount. + /// ExtraSpecialSalesCount, + + /// + /// Increases how much money is gained from selling items to the store by a percentage. + /// StoreSellMultiplier, + + /// + /// Decreases the prices of items in affiliated store by a percentage. + /// StoreBuyMultiplierAffiliated, + + /// + /// Decreases the prices of items in all stores by a percentage. + /// StoreBuyMultiplier, + + /// + /// Decreases the price of upgrades and submarines in affiliated outposts by a percentage. + /// + ShipyardBuyMultiplierAffiliated, + + /// + /// Decreases the price of upgrades and submarines in all outposts by a percentage. + /// + ShipyardBuyMultiplier, + + /// + /// Limits how many of a certain item can be attached to the wall in the submarine at the same time. + /// Has to be used with CharacterAbilityGivePermanentStat to specify the tag of the item that is affected. Does nothing if no tag is specified. + /// MaxAttachableCount, + + /// + /// Increase the radius of explosions caused by the character by a percentage. + /// ExplosionRadiusMultiplier, + + /// + /// Increases the damage of explosions caused by the character by a percentage. + /// ExplosionDamageMultiplier, + + /// + /// Decreases the time it takes to fabricate items on fabricators operated by the character by a percentage. + /// FabricationSpeed, + + /// + /// Increases how much damage the character deals to ballast flora by a percentage. + /// BallastFloraDamageMultiplier, + + /// + /// Increases the time it takes for the character to pass out when out of oxygen. + /// HoldBreathMultiplier, + + /// + /// Used to set the character's apprencticeship to a certain job. + /// Used by the "apprenticeship" talent and requires a job to be specified via CharacterAbilityGivePermanentStat. + /// Apprenticeship, - Affiliation, - CPRBoost + + /// + /// Increases the revival chance of the character when performing CPR by a percentage. + /// + CPRBoost, + + /// + /// Can be used to prevent certain talents from being unlocked by specifying the talent's identifier via CharacterAbilityGivePermanentStat. + /// + LockedTalents } internal enum ItemTalentStats @@ -172,22 +568,77 @@ namespace Barotrauma FabricationSpeed } + /// + /// AbilityFlags are a set of toggleable flags that can be applied to characters. + /// [Flags] public enum AbilityFlags { + /// + /// Used to indicate an erroneous ability flag. Should not be used. + /// None = 0, + + /// + /// Character will not be able to run. + /// MustWalk = 0x1, + + /// + /// Character is immune to pressure. + /// ImmuneToPressure = 0x2, + + /// + /// Character won't be targeted by enemy AI. + /// IgnoredByEnemyAI = 0x4, + + /// + /// Character can drag corpses without a movement speed penalty. + /// MoveNormallyWhileDragging = 0x8, + + /// + /// Character is able to tinker with items. + /// CanTinker = 0x10, + + /// + /// Character is able to tinker with fabricators and deconstructors. + /// CanTinkerFabricatorsAndDeconstructors = 0x20, + + /// + /// Allows items tinkered by the character to consume no power. + /// TinkeringPowersDevices = 0x40, + + /// + /// Allows the character to gain skills past 100. + /// GainSkillPastMaximum = 0x80, + + /// + /// Allows the character to retain experience when respawning as a new character. + /// RetainExperienceForNewCharacter = 0x100, + + /// + /// Allows CharacterAbilityApplyStatusEffectsToLastOrderedCharacter to affect the last 2 characters ordered. + /// AllowSecondOrderedTarget = 0x200, + + /// + /// Character will stay conscious even if their vitality drops below 0. + /// AlwaysStayConscious = 0x400, - CanNotDieToAfflictions = 0x800, + + /// + /// Prevents afflictions on the character from dropping the characters vitality below the kill threshold. + /// The character can still die from sources like getting crushed by pressure or if their head is severed. + /// + CanNotDieToAfflictions = 0x800 } [Flags] @@ -225,4 +676,4 @@ namespace Barotrauma Local, Radio } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 2275bcd06..dcd1deeea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -110,7 +110,7 @@ namespace Barotrauma state = 1; break; case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) return; + if (!Submarine.MainSub.AtEitherExit) { return; } Finish(); state = 2; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 192664d17..1b41e3bf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -9,6 +9,8 @@ namespace Barotrauma public event Action Finished; protected bool isFinished; + public int RandomSeed; + protected readonly EventPrefab prefab; public EventPrefab Prefab => prefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index cce7bd22c..af4f0c713 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -15,20 +16,13 @@ namespace Barotrauma { DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed."); } - foreach (var attribute in element.Attributes()) - { - if (PropertyConditional.IsValid(attribute) && !IsTargetTagAttribute(attribute)) - { - Conditional = new PropertyConditional(attribute); - break; - } - } + Conditional = PropertyConditional.FromXElement(element, IsNotTargetTagAttribute).FirstOrDefault(); if (Conditional == null) { DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed."); } - static bool IsTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() == "targettag"; + static bool IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag"; } private string GetEventName() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index 47c205aee..bd30a9b4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -21,7 +21,7 @@ namespace Barotrauma protected object? value2; protected object? value1; - protected PropertyConditional.OperatorType Operator { get; set; } + protected PropertyConditional.ComparisonOperatorType Operator { get; set; } public CheckDataAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { @@ -56,23 +56,13 @@ namespace Barotrauma { if (GameMain.GameSession?.GameMode is not CampaignMode campaignMode) { return false; } - string[] splitString = Condition.Split(' '); - string value; - if (splitString.Length > 0) + (Operator, string value) = PropertyConditional.ExtractComparisonOperatorFromConditionString(Condition); + if (Operator == PropertyConditional.ComparisonOperatorType.None) { - //the first part of the string is the operator, skip it - value = string.Join(" ", splitString.Skip(1)); - } - else - { - DebugConsole.ThrowError($"{Condition} is too short, it should start with an operator followed by a boolean or a floating point value."); + DebugConsole.ThrowError($"{Condition} is invalid, it should start with an operator followed by a boolean or a floating point value."); return false; } - string op = splitString[0]; - Operator = PropertyConditional.GetOperatorType(op); - if (Operator == PropertyConditional.OperatorType.None) { return false; } - if (CheckAgainstMetadata) { object? metadata1 = campaignMode.CampaignMetadata.GetValue(Identifier); @@ -82,8 +72,8 @@ namespace Barotrauma { return Operator switch { - PropertyConditional.OperatorType.Equals => metadata1 == metadata2, - PropertyConditional.OperatorType.NotEquals => metadata1 != metadata2, + PropertyConditional.ComparisonOperatorType.Equals => metadata1 == metadata2, + PropertyConditional.ComparisonOperatorType.NotEquals => metadata1 != metadata2, _ => false }; } @@ -139,9 +129,9 @@ namespace Barotrauma value2 = val2; switch (Operator) { - case PropertyConditional.OperatorType.Equals: + case PropertyConditional.ComparisonOperatorType.Equals: return val1 == val2; - case PropertyConditional.OperatorType.NotEquals: + case PropertyConditional.ComparisonOperatorType.NotEquals: return val1 != val2; default: DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a boolean (was {Operator} for {val2})."); @@ -166,17 +156,17 @@ namespace Barotrauma value2 = val2; switch (Operator) { - case PropertyConditional.OperatorType.Equals: + case PropertyConditional.ComparisonOperatorType.Equals: return MathUtils.NearlyEqual(val1, val2); - case PropertyConditional.OperatorType.GreaterThan: + case PropertyConditional.ComparisonOperatorType.GreaterThan: return val1 > val2; - case PropertyConditional.OperatorType.GreaterThanEquals: + case PropertyConditional.ComparisonOperatorType.GreaterThanEquals: return val1 >= val2; - case PropertyConditional.OperatorType.LessThan: + case PropertyConditional.ComparisonOperatorType.LessThan: return val1 < val2; - case PropertyConditional.OperatorType.LessThanEquals: + case PropertyConditional.ComparisonOperatorType.LessThanEquals: return val1 <= val2; - case PropertyConditional.OperatorType.NotEquals: + case PropertyConditional.ComparisonOperatorType.NotEquals: return !MathUtils.NearlyEqual(val1, val2); } @@ -195,9 +185,9 @@ namespace Barotrauma bool equals = string.Equals(val1, val2, StringComparison.OrdinalIgnoreCase); switch (Operator) { - case PropertyConditional.OperatorType.Equals: + case PropertyConditional.ComparisonOperatorType.Equals: return equals; - case PropertyConditional.OperatorType.NotEquals: + case PropertyConditional.ComparisonOperatorType.NotEquals: return !equals; default: DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a string (was {Operator} for {val2})."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index e84bd7676..d202db5b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -33,7 +33,7 @@ namespace Barotrauma public int ItemContainerIndex { get; set; } private readonly IReadOnlyList conditionals; - + private readonly Identifier[] itemIdentifierSplit; private readonly Identifier[] itemTags; @@ -44,13 +44,7 @@ namespace Barotrauma var conditionalList = new List(); foreach (ContentXElement subElement in element.GetChildElements("conditional")) { - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionalList.Add(new PropertyConditional(attribute)); - } - } + 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 1c5c8f0b5..a01b1abd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -200,7 +200,7 @@ namespace Barotrauma } } - private int[] GetEndingOptions() + public int[] GetEndingOptions() { List endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList(); if (!ContinueConversation) { endings.Add(-1); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 2c298853d..be2f85fbd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -1,12 +1,13 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Xml.Linq; -using Barotrauma.Networking; -using Microsoft.Xna.Framework; +using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { - class MissionAction : EventAction + partial class MissionAction : EventAction { [Serialize("", IsPropertySaveable.Yes)] public Identifier MissionIdentifier { get; set; } @@ -14,8 +15,10 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier MissionTag { get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "The type of the location the mission will be unlocked in (if empty, any location can be selected).")] - public string LocationType { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier RequiredFaction { get; set; } + + public ImmutableArray LocationTypes { get; } [Serialize(0, IsPropertySaveable.Yes, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")] public int MinLocationDistance { get; set; } @@ -28,6 +31,8 @@ namespace Barotrauma private bool isFinished; + private readonly Random random; + public MissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty) @@ -38,6 +43,8 @@ namespace Barotrauma { DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored."); } + LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); + random = new MTRandom(parentEvent.RandomSeed); } public override bool IsFinished(ref string goTo) @@ -56,14 +63,14 @@ namespace Barotrauma if (GameMain.GameSession.GameMode is CampaignMode campaign) { Mission unlockedMission = null; - var unlockLocation = FindUnlockLocation(); + var unlockLocation = FindUnlockLocation(MinLocationDistance, UnlockFurtherOnMap, LocationTypes); if (unlockLocation == null && CreateLocationIfNotFound) { //find an empty location at least 3 steps away, further on the map - var emptyLocation = FindUnlockLocationRecursive(campaign.Map.CurrentLocation, Math.Max(MinLocationDistance, 3), "none", true, new HashSet()); + var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable()); if (emptyLocation != null) { - emptyLocation.ChangeType(Barotrauma.LocationType.Prefabs[LocationType]); + emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]); unlockLocation = emptyLocation; } } @@ -72,11 +79,11 @@ namespace Barotrauma { if (!MissionIdentifier.IsEmpty) { - unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); + unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); } else if (!MissionTag.IsEmpty) { - unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag); + unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag, random); } if (campaign is MultiPlayerCampaign mpCampaign) { @@ -84,7 +91,9 @@ namespace Barotrauma } if (unlockedMission != null) { - if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] ==null) + unlockedMission.OriginLocation = campaign.Map.CurrentLocation; + campaign.Map.Discover(unlockLocation, checkTalents: false); + if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] == null) { DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\"."); } @@ -99,66 +108,86 @@ namespace Barotrauma IconColor = unlockedMission.Prefab.IconColor }; #else + missionsUnlockedThisRound.Add(unlockedMission); NotifyMissionUnlock(unlockedMission); - #endif +#endif } } else { - DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationType}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})"); + DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationTypes}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})"); } } isFinished = true; } - private Location FindUnlockLocation() + private Location FindUnlockLocation(int minDistance, bool unlockFurtherOnMap, IEnumerable locationTypes) { var campaign = GameMain.GameSession.GameMode as CampaignMode; - if (string.IsNullOrEmpty(LocationType) && MinLocationDistance <= 1) + if (LocationTypes.Length == 0 && minDistance <= 1) { return campaign.Map.CurrentLocation; } - return FindUnlockLocationRecursive(campaign.Map.CurrentLocation, 0, LocationType, UnlockFurtherOnMap, new HashSet()); + var currentLocation = campaign.Map.CurrentLocation; + int distance = 0; + HashSet checkedLocations = new HashSet(); + HashSet pendingLocations = new HashSet() { currentLocation }; + do + { + List currentLocations = pendingLocations.ToList(); + pendingLocations.Clear(); + foreach (var location in currentLocations) + { + checkedLocations.Add(location); + if (IsLocationValid(currentLocation, location, unlockFurtherOnMap, distance, minDistance, locationTypes)) + { + return location; + } + else + { + foreach (LocationConnection connection in location.Connections) + { + var otherLocation = connection.OtherLocation(location); + if (checkedLocations.Contains(otherLocation)) { continue; } + pendingLocations.Add(otherLocation); + } + } + } + distance++; + } while (pendingLocations.Any()); + + return null; } - private Location FindUnlockLocationRecursive(Location currLocation, int currDistance, string locationType, bool unlockFurtherOnMap, HashSet checkedLocations) + private bool IsLocationValid(Location currLocation, Location location, bool unlockFurtherOnMap, int distance, int minDistance, IEnumerable locationTypes) { - var campaign = GameMain.GameSession.GameMode as CampaignMode; - if (currLocation.Type.Identifier == locationType && currDistance >= MinLocationDistance && - (!unlockFurtherOnMap || currLocation.MapPosition.X > campaign.Map.CurrentLocation.MapPosition.X)) + if (!RequiredFaction.IsEmpty) { - return currLocation; + if (location.Faction?.Prefab.Identifier != RequiredFaction && + location.SecondaryFaction?.Prefab.Identifier != RequiredFaction) + { + return false; + } } - checkedLocations.Add(currLocation); - foreach (LocationConnection connection in currLocation.Connections) + if (!locationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && locationTypes.Contains("AnyOutpost".ToIdentifier()))) { - var otherLocation = connection.OtherLocation(currLocation); - if (checkedLocations.Contains(otherLocation)) { continue; } - var unlockLocation = FindUnlockLocationRecursive(otherLocation, ++currDistance, locationType, unlockFurtherOnMap, checkedLocations); - if (unlockLocation != null) { return unlockLocation; } + return false; } - return null; + if (distance < minDistance) + { + return false; + } + if (unlockFurtherOnMap && location.MapPosition.X < currLocation.MapPosition.X) + { + return false; + } + return true; } public override string ToDebugString() { return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier)})"; } - -#if SERVER - private void NotifyMissionUnlock(Mission mission) - { - foreach (Client client in GameMain.Server.ConnectedClients) - { - IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); - outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION); - outmsg.WriteIdentifier(mission.Prefab.Identifier); - outmsg.WriteString(mission.Name.Value); - GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); - } - } -#endif } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs new file mode 100644 index 000000000..d6deb9b1c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs @@ -0,0 +1,66 @@ +namespace Barotrauma +{ + class MissionStateAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionIdentifier { get; set; } + + public enum OperationType + { + Set, + Add + } + + [Serialize(OperationType.Set, IsPropertySaveable.Yes)] + public OperationType Operation { get; set; } + + [Serialize(0, IsPropertySaveable.Yes)] + public int State { get; set; } + + private bool isFinished; + + public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + State = element.GetAttributeInt("value", State); + if (MissionIdentifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured."); + } + } + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + foreach (Mission mission in GameMain.GameSession.Missions) + { + if (mission.Prefab.Identifier != MissionIdentifier) { continue; } + switch (Operation) + { + case OperationType.Set: + mission.State = State; + break; + case OperationType.Add: + mission.State += 1; + break; + } + } + + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs new file mode 100644 index 000000000..013b48771 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs @@ -0,0 +1,91 @@ +namespace Barotrauma +{ + class ModifyLocationAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Faction { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier SecondaryFaction { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string Name { get; set; } + + private bool isFinished; + + public ModifyLocationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + } + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + if (GameMain.GameSession.GameMode is CampaignMode campaign) + { + var location = campaign.Map.CurrentLocation; + if (location != null) + { + if (!Faction.IsEmpty) + { + var faction = campaign.Factions.Find(f => f.Prefab.Identifier == Faction); + if (faction == null) + { + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{Faction}\"."); + } + else + { + location.Faction = faction; + } + } + if (!SecondaryFaction.IsEmpty) + { + var secondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == SecondaryFaction); + if (secondaryFaction == null) + { + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{SecondaryFaction}\"."); + } + else + { + location.SecondaryFaction = secondaryFaction; + } + } + if (!Type.IsEmpty) + { + var locationType = LocationType.Prefabs.Find(lt => lt.Identifier == Type); + if (locationType == null) + { + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a location type with the identifier \"{Type}\"."); + } + else if (!location.LocationTypeChangesBlocked) + { + location.ChangeType(campaign, locationType); + } + } + if (!string.IsNullOrEmpty(Name)) + { + location.ForceName(TextManager.Get(Name).Fallback(Name).Value); + } + } + } + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ModifyLocationAction)}"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index ee26b9283..7905486c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -1,8 +1,6 @@ -using Barotrauma.Networking; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -12,7 +10,7 @@ namespace Barotrauma public Identifier NPCTag { get; set; } [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes)] - public CharacterTeamType TeamTag { get; set; } + public CharacterTeamType TeamID { get; set; } [Serialize(false, IsPropertySaveable.Yes)] public bool AddToCrew { get; set; } @@ -24,10 +22,13 @@ namespace Barotrauma public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { + //backwards compatibility + TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum("team", TeamID)); + var enums = Enum.GetValues(typeof(CharacterTeamType)).Cast(); - if (!enums.Contains(TeamTag)) + if (!enums.Contains(TeamID)) { - DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamTag}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}."); + DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamID}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}."); } } @@ -41,27 +42,34 @@ namespace Barotrauma foreach (var npc in affectedNpcs) { // characters will still remain on friendlyNPC team for rest of the tick - npc.SetOriginalTeam(TeamTag); - - if (AddToCrew && (TeamTag == CharacterTeamType.Team1 || TeamTag == CharacterTeamType.Team2)) + npc.SetOriginalTeam(TeamID); + foreach (Item item in npc.Inventory.AllItems) + { + var idCard = item.GetComponent(); + if (idCard != null) + { + idCard.TeamID = TeamID; + } + } + if (AddToCrew && (TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2)) { npc.Info.StartItemsGiven = true; GameMain.GameSession.CrewManager.AddCharacter(npc); ChangeItemTeam(Submarine.MainSub, true); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamTag, npc.Inventory.AllItems)); + 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 == TeamTag); + 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(TeamTag, npc.Inventory.AllItems)); + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.AllItems)); } } @@ -72,11 +80,10 @@ namespace Barotrauma item.AllowStealing = allowStealing; if (item.GetComponent() is { } wifiComponent) { - wifiComponent.TeamID = TeamTag; + wifiComponent.TeamID = TeamID; } if (item.GetComponent() is { } idCard) { - idCard.TeamID = TeamTag; idCard.SubmarineSpecificID = 0; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index cee49e531..e0d23114b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -39,13 +39,14 @@ namespace Barotrauma affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); foreach (var npc in affectedNpcs) { - if (!(npc.AIController is HumanAIController humanAiController)) { continue; } + if (npc.AIController is not HumanAIController humanAiController) { continue; } if (Follow) { var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true) { - OverridePriority = 100.0f + OverridePriority = 100.0f, + IsFollowOrderObjective = true }; humanAiController.ObjectiveManager.AddObjective(newObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index fa3b3d2f8..f05962579 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -19,8 +19,6 @@ namespace Barotrauma private IEnumerable affectedNpcs; - private AIObjectiveGoTo gotoObjective; - public override void Update(float deltaTime) { if (isFinished) { return; } @@ -33,19 +31,18 @@ namespace Barotrauma if (Wait) { - gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true) + var gotoObjective = new AIObjectiveGoTo( + AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true) { - OverridePriority = 100.0f + OverridePriority = 100.0f, + SourceEventAction = this }; humanAiController.ObjectiveManager.AddObjective(gotoObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; } else { - if (gotoObjective != null) - { - gotoObjective.Abandon = true; - } + AbandonGoToObjectives(humanAiController); } } isFinished = true; @@ -62,17 +59,25 @@ namespace Barotrauma { foreach (var npc in affectedNpcs) { - if (npc.Removed || npc.AIController is not HumanAIController) { continue; } - if (gotoObjective != null) - { - gotoObjective.Abandon = true; - } + if (npc.Removed || npc.AIController is not HumanAIController aiController) { continue; } + AbandonGoToObjectives(aiController); } affectedNpcs = null; } isFinished = false; } + private void AbandonGoToObjectives(HumanAIController aiController) + { + foreach (var objective in aiController.ObjectiveManager.Objectives) + { + if (objective is AIObjectiveGoTo gotoObjective && gotoObjective.SourceEventAction?.ParentEvent == ParentEvent) + { + gotoObjective.Abandon = true; + } + } + } + public override string ToDebugString() { return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(NPCWaitAction)} -> (NPCTag: {NPCTag.ColorizeObject()}, Wait: {Wait.ColorizeObject()})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index d39a5e666..348e856e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -1,7 +1,5 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using System.Collections.Immutable; namespace Barotrauma { @@ -11,17 +9,20 @@ namespace Barotrauma public Identifier TargetTag { get; set; } [Serialize("", IsPropertySaveable.Yes)] - public Identifier ItemIdentifier { get; set; } + public string ItemIdentifiers { get; set; } [Serialize(1, IsPropertySaveable.Yes)] public int Amount { get; set; } - public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) - { - if (ItemIdentifier.IsEmpty) + private readonly ImmutableHashSet itemIdentifierSplit; + + public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (string.IsNullOrEmpty(ItemIdentifiers)) { - ItemIdentifier = element.GetAttributeIdentifier("itemidentifiers", element.GetAttributeIdentifier("identifier", Identifier.Empty)); + ItemIdentifiers = element.GetAttributeString("itemidentifier", element.GetAttributeString("identifier", string.Empty)); } + itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers().ToImmutableHashSet(); } private bool isFinished = false; @@ -62,7 +63,7 @@ namespace Barotrauma var item = inventory.FindItem(it => it != null && !removedItems.Contains(it) && - (ItemIdentifier.IsEmpty || it.Prefab.Identifier == ItemIdentifier), recursive: true); + (itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(it.Prefab.Identifier)), recursive: true); if (item == null) { break; } Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); @@ -70,7 +71,7 @@ namespace Barotrauma } else if (target is Item item) { - if (ItemIdentifier.IsEmpty || item.Prefab.Identifier == ItemIdentifier) + if (itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(item.Prefab.Identifier)) { Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs index 14be7bdba..41c9391f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs @@ -46,43 +46,29 @@ namespace Barotrauma switch (TargetType) { case ReputationType.Faction: - { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier); - if (faction != null) { - faction.Reputation.AddReputation(Increase); - } - else - { - DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found."); - } - - break; - } - case ReputationType.Location: - { - Location location = campaign.Map.CurrentLocation; - if (location != null) - { - location.Reputation.AddReputation(Increase); - IEnumerable locations = location.Connections.SelectMany(c => c.Locations).Distinct().Where(l => l != null && l != location); - foreach (Location connectedLocation in locations) + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier); + if (faction != null) { - Debug.Assert(connectedLocation.Reputation != null, "connectedLocation.Reputation != null"); - if (connectedLocation.Reputation != null) - { - connectedLocation.Reputation.AddReputation(Increase / 4); - } + faction.Reputation.AddReputation(Increase); + } + else + { + DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found."); } - } - break; - } + break; + } + case ReputationType.Location: + { + campaign.Map.CurrentLocation?.Reputation?.AddReputation(Increase); + break; + } default: - { - DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified."); - break; - } + { + DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified."); + break; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index ea2851339..f015fc26b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -50,7 +50,7 @@ namespace Barotrauma public Identifier SpawnPointTag { get; set; } [Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes)] - public CharacterTeamType Team { get; protected set; } + public CharacterTeamType TeamID { get; protected set; } [Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")] public bool RequireSpawnPointTag { get; set; } @@ -92,6 +92,14 @@ namespace Barotrauma public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null; + //backwards compatibility + TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum("team", TeamID)); + if (element.GetAttribute("submarinetype") != null) + { + DebugConsole.ThrowError( + $"Error in even \"{(parentEvent.Prefab?.Identifier.ToString() ?? "unknown")}\". " + + $"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?"); + } } public override bool IsFinished(ref string goTo) @@ -118,7 +126,28 @@ namespace Barotrauma if (!NPCSetIdentifier.IsEmpty && !NPCIdentifier.IsEmpty) { - HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier); + HumanPrefab humanPrefab = null; + if (Level.Loaded?.StartLocation is Location startLocation) + { + humanPrefab = + TryFindHumanPrefab(startLocation.Faction) ?? + TryFindHumanPrefab(startLocation.SecondaryFaction); + } + HumanPrefab TryFindHumanPrefab(Faction faction) + { + if (faction == null) { return null; } + return + NPCSet.Get(NPCSetIdentifier, + NPCIdentifier.Replace("[faction]".ToIdentifier(), faction.Prefab.Identifier), + logError: false) ?? + //try to spawn a coalition NPC if a correct one can't be found + NPCSet.Get(NPCSetIdentifier, + NPCIdentifier.Replace("[faction]".ToIdentifier(), "coalition".ToIdentifier()), + logError: false); + } + + humanPrefab ??= NPCSet.Get(NPCSetIdentifier, NPCIdentifier, logError: true); + if (humanPrefab != null) { if (!AllowDuplicates && @@ -130,13 +159,13 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => { if (newCharacter == null) { return; } newCharacter.HumanPrefab = humanPrefab; - newCharacter.TeamID = Team; + newCharacter.TeamID = TeamID; newCharacter.EnableDespawn = false; - humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); + humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint); if (LootingIsStealing) { foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true)) @@ -151,6 +180,18 @@ namespace Barotrauma ParentEvent.AddTarget(TargetTag, newCharacter); } spawnedEntity = newCharacter; + if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier); + foreach (Identifier tag in humanPrefab.GetTags()) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag); + } + } +#if SERVER + newCharacter.LoadTalents(); + GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData()); +#endif }); } } @@ -165,7 +206,7 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter => { if (!TargetTag.IsEmpty && newCharacter != null) { @@ -211,7 +252,7 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawned: onSpawned); + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned); } } else @@ -239,10 +280,10 @@ namespace Barotrauma spawned = true; } - public static Vector2 OffsetSpawnPos(Vector2 pos, float offsetAmount) + public static Vector2 OffsetSpawnPos(Vector2 pos, float offset) { - Hull hull = Hull.FindHull(pos); - pos += Rand.Vector(offsetAmount); + Hull hull = Hull.FindHull(pos); + pos += Rand.Vector(offset); if (hull != null) { float margin = 50.0f; @@ -289,30 +330,24 @@ namespace Barotrauma public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) { bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost; - List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); - - potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed); - + List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); + potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && wp.IsTraversable); if (moduleFlags != null && moduleFlags.Any()) { - List spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Any(moduleFlags.Contains) ?? false).ToList(); + var spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull is Hull h && h.OutpostModuleTags.Any(moduleFlags.Contains)); if (spawnPoints.Any()) { - potentialSpawnPoints = spawnPoints; + potentialSpawnPoints = spawnPoints.ToList(); } } - if (spawnpointTags != null && spawnpointTags.Any()) { - var spawnPoints = potentialSpawnPoints - .Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && !wp.isObstructed)); - + var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable)); if (requireTaggedSpawnPoint || spawnPoints.Any()) { potentialSpawnPoints = spawnPoints.ToList(); } } - if (potentialSpawnPoints.None()) { if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs index 4dc8d6adc..f07c316cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma { @@ -7,7 +6,7 @@ namespace Barotrauma { private readonly List effects = new List(); - private int actionIndex; + private readonly int actionIndex; [Serialize("", IsPropertySaveable.Yes)] public Identifier TargetTag { get; set; } @@ -46,25 +45,40 @@ namespace Barotrauma public override void Update(float deltaTime) { if (isFinished) { return; } - var targets = ParentEvent.GetTargets(TargetTag); + var eventTargets = ParentEvent.GetTargets(TargetTag); foreach (StatusEffect effect in effects) { - foreach (var target in targets) + foreach (var target in eventTargets) { - if (target is Item targetItem) + if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects); - } - else - { - effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity); + List nearbyTargets = new List(); + effect.AddNearbyTargets(target.WorldPosition, nearbyTargets); + foreach (var nearbyTarget in nearbyTargets) + { + ApplyOnTarget(nearbyTarget as Entity, effect); + } + continue; } + ApplyOnTarget(target, effect); } } #if SERVER - ServerWrite(targets); + ServerWrite(eventTargets); #endif isFinished = true; + + void ApplyOnTarget(Entity target, StatusEffect effect) + { + if (target is Item targetItem) + { + effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects); + } + else + { + effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity); + } + } } public override string ToDebugString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 89eff4f39..ef66959e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -21,6 +21,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes)] public bool IgnoreIncapacitatedCharacters { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool AllowHiddenItems { get; set; } + private bool isFinished = false; public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) @@ -119,12 +122,12 @@ namespace Barotrauma private void TagItemsByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier == identifier); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); } private void TagItemsByTag(Identifier tag) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag)); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag)); } private void TagHullsByName(Identifier name) @@ -137,6 +140,11 @@ namespace Barotrauma ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); } + private bool IsValidItem(Item it) + { + return (!it.HiddenInGame || AllowHiddenItems) && SubmarineTypeMatches(it.Submarine); + } + private bool SubmarineTypeMatches(Submarine sub) { if (SubmarineType == SubType.Any) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs index c7253cc72..51e265f83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs @@ -1,12 +1,13 @@ -using System.Xml.Linq; - -namespace Barotrauma +namespace Barotrauma { class TriggerEventAction : EventAction { [Serialize("", IsPropertySaveable.Yes)] public Identifier Identifier { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool NextRound { get; set; } + private bool isFinished; public TriggerEventAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -26,17 +27,24 @@ namespace Barotrauma if (GameMain.GameSession?.EventManager != null) { - var eventPrefab = EventSet.GetEventPrefab(Identifier); - if (eventPrefab == null) + if (NextRound) { - DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}."); + GameMain.GameSession.EventManager.QueuedEventsForNextRound.Enqueue(Identifier); } else { - var ev = eventPrefab.CreateInstance(); - if (ev != null) + var eventPrefab = EventSet.GetEventPrefab(Identifier); + if (eventPrefab == null) { - GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev); + DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}."); + } + else + { + var ev = eventPrefab.CreateInstance(); + if (ev != null) + { + GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs index 6380f8f0d..202a8734d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs @@ -1,6 +1,3 @@ -using System; -using System.Xml.Linq; - namespace Barotrauma { class WaitAction : EventAction diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index dee7f7b0f..47d8f4ddf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -1,9 +1,11 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -12,6 +14,7 @@ namespace Barotrauma public enum NetworkEventType { CONVERSATION, + CONVERSATION_SELECTED_OPTION, STATUSEFFECT, MISSION, UNLOCKPATH @@ -72,8 +75,7 @@ namespace Barotrauma private readonly List activeEvents = new List(); private readonly HashSet finishedEvents = new HashSet(); - private readonly HashSet nonRepeatableEvents = new HashSet(); - private readonly HashSet usedUniqueSets = new HashSet(); + private readonly HashSet nonRepeatableEvents = new HashSet(); #if DEBUG && SERVER @@ -100,7 +102,9 @@ namespace Barotrauma public readonly Queue QueuedEvents = new Queue(); - private struct TimeStamp + public readonly Queue QueuedEventsForNextRound = new Queue(); + + private readonly struct TimeStamp { public readonly double Time; public readonly Event Event; @@ -122,7 +126,8 @@ namespace Barotrauma public bool Enabled = true; - private MTRandom rand; + private MTRandom random; + private int randomSeed; public void StartRound(Level level) { @@ -134,7 +139,9 @@ namespace Barotrauma pendingEventSets.Clear(); selectedEvents.Clear(); activeEvents.Clear(); - +#if SERVER + MissionAction.ResetMissionsUnlockedThisRound(); +#endif pathFinder = new PathFinder(WayPoint.WayPointList, false); totalPathLength = 0.0f; if (level != null) @@ -144,23 +151,22 @@ namespace Barotrauma } SelectSettings(); - - int seed = 0; + if (level != null) { - seed = ToolBox.StringToInt(level.Seed); + randomSeed = ToolBox.StringToInt(level.Seed); foreach (var previousEvent in level.LevelData.EventHistory) { - seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier); + randomSeed ^= ToolBox.IdentifierToInt(previousEvent); } } - rand = new MTRandom(seed); + random = new MTRandom(randomSeed); bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode; EventSet initialEventSet = SelectRandomEvents( EventSet.Prefabs.ToList(), requireCampaignSet: playingCampaign, - random: rand); + random: random); EventSet additiveSet = null; if (initialEventSet != null && initialEventSet.Additive) { @@ -168,7 +174,7 @@ namespace Barotrauma initialEventSet = SelectRandomEvents( EventSet.Prefabs.Where(e => !e.Additive).ToList(), requireCampaignSet: playingCampaign, - random: rand); + random: random); } if (initialEventSet != null) { @@ -188,14 +194,7 @@ namespace Barotrauma //if the outpost is connected to a locked connection, create an event to unlock it if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) { - var unlockPathPrefabs = EventPrefab.Prefabs.Where(e => e.UnlockPathEvent); - var unlockPathPrefabsForBiome = unlockPathPrefabs.Where(e => - e.BiomeIdentifier.IsEmpty || - e.BiomeIdentifier == level.LevelData.Biome.Identifier); - - var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ? - ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, b => b.Commonness, rand) : - ToolBox.SelectWeightedRandom(unlockPathPrefabs, b => b.Commonness, rand); + var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction); if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(); @@ -216,7 +215,7 @@ namespace Barotrauma { foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) { - nonRepeatableEvents.Add(ep); + nonRepeatableEvents.Add(ep.Identifier); } } foreach (EventSet childSet in eventSet.ChildSets) @@ -226,6 +225,21 @@ namespace Barotrauma } } + while (QueuedEventsForNextRound.Count > 0 && QueuedEventsForNextRound.Dequeue() is Identifier id) + { + var eventPrefab = EventSet.GetEventPrefab(id); + if (eventPrefab == null) + { + DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}."); + continue; + } + var ev = eventPrefab.CreateInstance(); + if (ev != null) + { + QueuedEvents.Enqueue(ev); + } + } + PreloadContent(GetFilesToPreload()); roundDuration = 0.0f; @@ -358,7 +372,6 @@ namespace Barotrauma QueuedEvents.Clear(); finishedEvents.Clear(); nonRepeatableEvents.Clear(); - usedUniqueSets.Clear(); preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); @@ -370,20 +383,49 @@ namespace Barotrauma /// /// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history /// - public void RegisterEventHistory() + public void RegisterEventHistory(bool registerFinishedOnly = false) { if (level?.LevelData == null) { return; } - level.LevelData.EventsExhausted = true; + level.LevelData.EventsExhausted = !registerFinishedOnly; + if (level.LevelData.Type == LevelData.LevelType.Outpost) { - level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e))); + if (registerFinishedOnly) + { + foreach (var finishedEvent in finishedEvents) + { + var key = finishedEvent.ParentSet; + if (key == null) { continue; } + if (level.LevelData.FinishedEvents.ContainsKey(key)) + { + level.LevelData.FinishedEvents[key] += 1; + } + else + { + level.LevelData.FinishedEvents.Add(key, 1); + } + } + } + + level.LevelData.EventHistory.AddRange(selectedEvents.Values + .SelectMany(v => v) + .Select(e => e.Prefab.Identifier) + .Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId))); + if (level.LevelData.EventHistory.Count > MaxEventHistory) { level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); } } - level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e))); + level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId))); + + if (!registerFinishedOnly) + { + level.LevelData.FinishedEvents.Clear(); + } + + bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId); } public void SkipEventCooldown() @@ -393,9 +435,9 @@ namespace Barotrauma private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness) { - if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab)) { return 0.0f; } + if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.Identifier)) { return 0.0f; } float retVal = baseCommonness; - if (level.LevelData.EventHistory.Contains(eventPrefab)) { retVal *= 0.1f; } + if (level.LevelData.EventHistory.Contains(eventPrefab.Identifier)) { retVal *= 0.1f; } return retVal; } @@ -436,9 +478,13 @@ namespace Barotrauma } } - bool isPrefabSuitable(EventPrefab e) - => (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && - !level.LevelData.NonRepeatableEvents.Contains(e); + bool isPrefabSuitable(EventPrefab e) => + (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && + !level.LevelData.NonRepeatableEvents.Contains(e.Identifier) && + isFactionSuitable(e.Faction); + + bool isFactionSuitable(Identifier factionId) => + factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier; foreach (var subEventPrefab in eventSet.EventPrefabs) { @@ -447,9 +493,9 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\"."); } } - + var suitablePrefabSubsets = eventSet.EventPrefabs.Where( - e => e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); + e => isFactionSuitable(e.Faction) && e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); for (int i = 0; i < applyCount; i++) { @@ -462,14 +508,14 @@ namespace Barotrauma for (int j = 0; j < eventCount; j++) { if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; } - EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), rand); + EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), random); (IEnumerable eventPrefabs, float commonness, float probability) = subEventPrefab; - if (eventPrefabs != null && rand.NextDouble() <= probability) + if (eventPrefabs != null && random.NextDouble() <= probability) { - var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); - + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } + newEvent.RandomSeed = randomSeed; if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) @@ -483,7 +529,7 @@ namespace Barotrauma } if (eventSet.ChildSets.Any()) { - var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand); + var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random); if (newEventSet != null) { CreateEvents(newEventSet); @@ -494,9 +540,9 @@ namespace Barotrauma { foreach ((IEnumerable eventPrefabs, float commonness, float probability) in suitablePrefabSubsets) { - if (rand.NextDouble() > probability) { continue; } + if (random.NextDouble() > probability) { continue; } - var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } if (!selectedEvents.ContainsKey(eventSet)) @@ -601,6 +647,10 @@ namespace Barotrauma private bool IsValidForLocation(EventSet eventSet, Location location) { if (location is null) { return true; } + if (!eventSet.Faction.IsEmpty) + { + if (eventSet.Faction != location.Faction?.Prefab.Identifier && eventSet.Faction != location.SecondaryFaction?.Prefab.Identifier) { return false; } + } var locationType = location.GetLocationType(); bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents; if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; } @@ -728,53 +778,50 @@ namespace Barotrauma calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval; } - if (currentIntensity < eventThreshold) + bool recheck = false; + do { - bool recheck = false; - do + recheck = false; + //activate pending event sets that can be activated + for (int i = pendingEventSets.Count - 1; i >= 0; i--) { - recheck = false; - //activate pending event sets that can be activated - for (int i = pendingEventSets.Count - 1; i >= 0; i--) + var eventSet = pendingEventSets[i]; + if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; } + if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; } + if (!CanStartEventSet(eventSet)) { continue; } + + pendingEventSets.RemoveAt(i); + + if (selectedEvents.ContainsKey(eventSet)) { - var eventSet = pendingEventSets[i]; - if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; } - - if (!CanStartEventSet(eventSet)) { continue; } - - pendingEventSets.RemoveAt(i); - - if (selectedEvents.ContainsKey(eventSet)) + //start events in this set + foreach (Event ev in selectedEvents[eventSet]) { - //start events in this set - foreach (Event ev in selectedEvents[eventSet]) + activeEvents.Add(ev); + eventThreshold = settings.DefaultEventThreshold; + if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown)) { - activeEvents.Add(ev); - eventThreshold = settings.DefaultEventThreshold; - if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown)) + eventCoolDown = settings.EventCooldown; + } + if (eventSet.ResetTime > 0) + { + ev.Finished += () => { - eventCoolDown = settings.EventCooldown; - } - if (eventSet.ResetTime > 0) - { - ev.Finished += () => - { - pendingEventSets.Add(eventSet); - CreateEvents(eventSet); - }; - } + pendingEventSets.Add(eventSet); + CreateEvents(eventSet); + }; } } - - //add child event sets to pending - foreach (EventSet childEventSet in eventSet.ChildSets) - { - pendingEventSets.Add(childEventSet); - recheck = true; - } } - } while (recheck); - } + + //add child event sets to pending + foreach (EventSet childEventSet in eventSet.ChildSets) + { + pendingEventSets.Add(childEventSet); + recheck = true; + } + } + } while (recheck); foreach (Event ev in activeEvents) { @@ -782,11 +829,11 @@ namespace Barotrauma { ev.Update(deltaTime); } - else if (!finishedEvents.Contains(ev)) + else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab)) { if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) { - if (!level.LevelData.EventHistory.Contains(ev.Prefab)) { level.LevelData.EventHistory.Add(ev.Prefab); } + if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); } } finishedEvents.Add(ev); } @@ -832,30 +879,43 @@ namespace Barotrauma monsterStrength = 0; foreach (Character character in Character.CharacterList) { - if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup(CharacterPrefab.HumanSpeciesName)) { continue; } + if (character.IsIncapacitated || !character.Enabled || character.IsPet) { continue; } - if (!(character.AIController is EnemyAIController enemyAI)) { continue; } + if (character.AIController is EnemyAIController enemyAI) + { + if (!enemyAI.AIParams.StayInAbyss) + { + // Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub. + monsterStrength += enemyAI.CombatStrength; + } - if (!enemyAI.AIParams.StayInAbyss) + 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) + { + // Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42 + enemyDanger += enemyAI.CombatStrength / 500.0f; + } + else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null) + { + // Enemy outside targeting the sub or something in it + // -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25. + enemyDanger += enemyAI.CombatStrength / 5000.0f; + } + } + else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1)) { - // Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub. - 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) - { - // Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42 - enemyDanger += enemyAI.CombatStrength / 500.0f; - } - else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null) - { - // Enemy outside targeting the sub or something in it - // -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25. - enemyDanger += enemyAI.CombatStrength / 5000.0f; + if (character.Submarine != null && + Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange) + { + //we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character), + //so let's just go with a fixed value. + //5 living enemy characters in an enemy sub in sonar range is enough to bump the intensity to max + enemyDanger += 0.2f; + } } } + // Add a portion of the total strength of active monsters to the enemy danger so that we don't spawn too many monsters around the sub. // On top of the existing value, so if 10 crawlers are targeting the sub simultaneously from outside, the final value would be: 0.02 x 10 + 0.2 = 0.4. // And if they get inside, we add 0.1 per crawler on that. @@ -1108,5 +1168,20 @@ namespace Barotrauma return false; } + + public void Load(XElement element) + { + foreach (var id in element.GetAttributeIdentifierArray(nameof(QueuedEventsForNextRound), Array.Empty())) + { + QueuedEventsForNextRound.Enqueue(id); + } + } + + public XElement Save() + { + return new XElement("eventmanager", + new XAttribute(nameof(QueuedEventsForNextRound), + string.Join(',', QueuedEventsForNextRound))); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index db5fe38b8..7bd5e7e01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -1,6 +1,7 @@ using System; +using System.Linq; using System.Reflection; -using System.Xml.Linq; +using System.Reflection.Emit; namespace Barotrauma { @@ -14,12 +15,12 @@ namespace Barotrauma public readonly bool TriggerEventCooldown; public readonly float Commonness; public readonly Identifier BiomeIdentifier; + public readonly Identifier Faction; public readonly float SpawnDistance; public readonly bool UnlockPathEvent; public readonly string UnlockPathTooltip; public readonly int UnlockPathReputation; - public readonly string UnlockPathFaction; public EventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) : base(file, element.GetAttributeIdentifier("identifier", fallbackIdentifier)) @@ -40,6 +41,7 @@ namespace Barotrauma } BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty); + Faction = ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent)); @@ -47,7 +49,6 @@ namespace Barotrauma UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false); UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip"); UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0); - UnlockPathFaction = element.GetAttributeString("unlockpathfaction", ""); SpawnDistance = element.GetAttributeFloat("spawndistance", 0); } @@ -80,5 +81,17 @@ namespace Barotrauma { return $"EventPrefab ({Identifier})"; } + + public static EventPrefab GetUnlockPathEvent(Identifier biomeIdentifier, Faction faction) + { + var unlockPathEvents = Prefabs.OrderBy(p => p.Identifier).Where(e => e.UnlockPathEvent); + if (faction != null && unlockPathEvents.Any(e => e.Faction == faction.Prefab.Identifier)) + { + unlockPathEvents = unlockPathEvents.Where(e => e.Faction == faction.Prefab.Identifier); + } + return + unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == biomeIdentifier) ?? + unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == Identifier.Empty); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index a5f16c350..b5409cc22 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -1,10 +1,9 @@ -using System; +using Barotrauma.Extensions; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -89,7 +88,9 @@ namespace Barotrauma public readonly LevelData.LevelType LevelType; public readonly ImmutableArray LocationTypeIdentifiers; - + + public readonly Identifier Faction; + public readonly bool ChooseRandom; private readonly int eventCount = 1; @@ -110,6 +111,8 @@ namespace Barotrauma public readonly bool IgnoreCoolDown; + public readonly bool IgnoreIntensity; + public readonly bool PerRuin, PerCave, PerWreck; public readonly bool DisableInHuntingGrounds; @@ -143,11 +146,12 @@ namespace Barotrauma public readonly struct SubEventPrefab { - public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability) + public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability, Identifier factionId) { PrefabOrIdentifier = prefabOrIdentifiers; SelfCommonness = commonness; SelfProbability = probability; + Faction = factionId; } public readonly Either PrefabOrIdentifier; @@ -178,6 +182,8 @@ namespace Barotrauma public readonly float? SelfProbability; public float Probability => SelfProbability ?? EventPrefabs.MaxOrNull(p => p.Probability) ?? 0.0f; + public readonly Identifier Faction; + public void Deconstruct(out IEnumerable eventPrefabs, out float commonness, out float probability) { eventPrefabs = EventPrefabs; @@ -260,6 +266,8 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); } + Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); + Identifier[] locationTypeStr = element.GetAttributeIdentifierArray("locationtype", null); if (locationTypeStr != null) { @@ -282,6 +290,7 @@ namespace Barotrauma PerWreck = element.GetAttributeBool("perwreck", false); DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false); IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck)); + IgnoreIntensity = element.GetAttributeBool("ignoreintensity", parentSet?.IgnoreIntensity ?? false); DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false)); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); @@ -332,15 +341,17 @@ namespace Barotrauma Identifier[] identifiers = subElement.GetAttributeIdentifierArray("identifier", Array.Empty()); float commonness = subElement.GetAttributeFloat("commonness", -1f); float probability = subElement.GetAttributeFloat("probability", -1f); + Identifier factionId = subElement.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); eventPrefabs.Add(new SubEventPrefab( identifiers, commonness >= 0f ? commonness : (float?)null, - probability >= 0f ? probability : (float?)null)); + probability >= 0f ? probability : (float?)null, + factionId)); } else { var prefab = new EventPrefab(subElement, file, $"{Identifier}-{subElement.ElementsBeforeSelf().Count()}".ToIdentifier()); - eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability)); + eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability, prefab.Faction)); } break; } @@ -365,14 +376,36 @@ namespace Barotrauma public float GetCommonness(Level level) { - Identifier key = level.GenerationParams?.Identifier ?? Identifier.Empty; - return OverrideCommonness.ContainsKey(key) ? OverrideCommonness[key] : DefaultCommonness; + if (level.GenerationParams?.Identifier != null && + OverrideCommonness.TryGetValue(level.GenerationParams.Identifier, out float generationParamsCommonness)) + { + return generationParamsCommonness; + } + else if (level.StartOutpost?.Info.OutpostGenerationParams?.Identifier != null && + OverrideCommonness.TryGetValue(level.StartOutpost.Info.OutpostGenerationParams.Identifier, out float startOutpostParamsCommonness)) + { + return startOutpostParamsCommonness; + } + else if (level.EndOutpost?.Info.OutpostGenerationParams?.Identifier != null && + OverrideCommonness.TryGetValue(level.EndOutpost.Info.OutpostGenerationParams.Identifier, out float endOutpostParamsCommonness)) + { + return endOutpostParamsCommonness; + } + return DefaultCommonness; } public int GetEventCount(Level level) { - if (level?.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count)) { return eventCount; } - return count; + int finishedEventCount = 0; + if (level is not null) + { + level.LevelData.FinishedEvents.TryGetValue(this, out finishedEventCount); + } + if (level.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count)) + { + return eventCount - finishedEventCount; + } + return count - finishedEventCount; } public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null, bool fullLog = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index ced3da5c8..58d243d43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -1,5 +1,4 @@ using Barotrauma.Extensions; -using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -28,6 +27,8 @@ namespace Barotrauma private const float EndDelay = 5.0f; private float endTimer; + private bool allowOrderingRescuees; + public override bool AllowRespawn => false; public override bool AllowUndocking @@ -39,17 +40,17 @@ namespace Barotrauma } } - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { - if (State > 0) + if (State == 0) { - return Enumerable.Empty(); + return Targets.Select(t => (Prefab.SonarLabel, t.WorldPosition)); } else { - return Targets.Select(t => t.WorldPosition); + return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } } } @@ -83,6 +84,8 @@ namespace Barotrauma { characterConfig = prefab.ConfigElement.GetChildElement("Characters"); + allowOrderingRescuees = prefab.ConfigElement.GetAttributeBool(nameof(allowOrderingRescuees), true); + string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", ""); hostagesKilledMessage = TextManager.Get(msgTag).Fallback(msgTag); @@ -144,10 +147,7 @@ namespace Barotrauma ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos( SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); - if (spawnPoint == null) - { - spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + spawnPoint ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); Vector2 spawnPos = spawnPoint.WorldPosition; if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100) { @@ -186,7 +186,12 @@ namespace Barotrauma if (element.Attribute("identifier") != null && element.Attribute("from") != null) { - HumanPrefab humanPrefab = GetHumanPrefabFromElement(element); + HumanPrefab humanPrefab = GetHumanPrefabFromElement(element); + if (humanPrefab == null) + { + DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found"); + continue; + } for (int i = 0; i < count; i++) { LoadHuman(humanPrefab, element, submarine); @@ -198,7 +203,7 @@ namespace Barotrauma var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (characterPrefab == null) { - DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found"); + DebugConsole.ThrowError($"Couldn't spawn a character for abandoned outpost mission: character prefab \"{speciesName}\" not found"); continue; } for (int i = 0; i < count; i++) @@ -214,19 +219,25 @@ namespace Barotrauma { Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); + var spawnPointType = element.GetAttributeEnum("spawnpointtype", SpawnType.Human); ISpatialEntity spawnPos = SpawnAction.GetSpawnPos( - SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, + SpawnAction.SpawnLocationType.Outpost, spawnPointType, moduleFlags ?? humanPrefab.GetModuleFlags(), spawnPointTags ?? humanPrefab.GetSpawnPointTags(), element.GetAttributeBool("asfaraspossible", false)); - if (spawnPos == null) - { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); bool requiresRescue = element.GetAttributeBool("requirerescue", false); - - Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos); + var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None); + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos); + if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier); + foreach (Identifier tag in humanPrefab.GetTags()) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag); + } + } if (spawnPos is WayPoint wp) { @@ -237,9 +248,19 @@ namespace Barotrauma { requireRescue.Add(spawnedCharacter); #if CLIENT - GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter); + if (allowOrderingRescuees) + { + GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter); + } #endif } + else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController humanAi) + { + var order = OrderPrefab.Prefabs["fightintruders"] + .CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter) + .WithManualPriority(CharacterInfo.HighestManualOrderPriority); + spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false); + } if (element.GetAttributeBool("requirekill", false)) { @@ -252,10 +273,7 @@ namespace Barotrauma Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); - if (spawnPos == null) - { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + 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)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs index 2bfb34391..1a5a8cb3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -18,17 +18,19 @@ namespace Barotrauma private Ruin TargetRuin { get; set; } - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { if (State == 0) { - return allTargets.Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c))).Select(t => t.WorldPosition); + return allTargets + .Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c))) + .Select(t => (Prefab.SonarLabel, t.WorldPosition)); } else { - return Enumerable.Empty(); + return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } } } @@ -164,7 +166,7 @@ namespace Barotrauma { bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ? campaign.GetAvailableTransition() != CampaignMode.TransitionType.None : - Submarine.MainSub is { } sub && (sub.AtEndExit || sub.AtStartExit); + Submarine.MainSub is { } sub && sub.AtEitherExit; return State > 0 && exitingLevel; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 634652ffa..2350443aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -69,15 +69,7 @@ namespace Barotrauma } } - public override LocalizedString SonarLabel - { - get - { - return base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel; - } - } - - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { @@ -85,7 +77,12 @@ namespace Barotrauma { yield break; } - yield return level.BeaconStation.WorldPosition; + else + { + yield return ( + Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel, + level.BeaconStation.WorldPosition); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs new file mode 100644 index 000000000..daa064131 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -0,0 +1,295 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + partial class EndMission : Mission + { + enum MissionPhase + { + Initial, + NoItemsDestroyed, + SomeItemsDestroyed, + AllItemsDestroyed, + BossKilled + } + + private readonly CharacterPrefab bossPrefab; + private readonly CharacterPrefab minionPrefab; + + private readonly Identifier spawnPointTag; + private readonly Identifier destructibleItemTag; + + private readonly string endCinematicSound; + + private ImmutableArray minions; + private readonly int minionCount; + private readonly float minionScatter; + + private Character boss; + + private readonly ItemPrefab projectilePrefab; + + private float projectileTimer = 30.0f; + + private readonly float startCinematicDistance = 30.0f; + + private float endCinematicTimer; + + private readonly List destructibleItems = new List(); + + protected readonly float wakeUpCinematicDelay = 5.0f; + protected readonly float bossWakeUpDelay = 7.0f; + protected readonly float cameraWaitDuration = 7.0f; + + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels + { + get { return destructibleItems.Where(it => it.Condition > 0.0f).Select(it => (Prefab.SonarLabel, it.WorldPosition)); } + } + + public override int State + { + get { return base.State; } + set + { + + if (state != value) + { + base.State = value; + OnStateChangedProjSpecific(); + if (Phase == MissionPhase.AllItemsDestroyed) + { + CoroutineManager.Invoke(() => + { + if (boss != null && !boss.Removed) + { + boss.AnimController.ColliderIndex = 1; + } + }, delay: wakeUpCinematicDelay + bossWakeUpDelay + 2); + } + } + } + } + + private MissionPhase Phase + { + get + { + //state 0: nothing happens yet, play a cinematic and skip to the next state when close enough to the boss + //state 1: start cinematic played + //state 2: first destructibleItems destroyed + //state 3: 2nd destructibleItems destroyed + //state 4: all destructibleItems destroyed + //state 5: boss killed + if (state == 0) { return MissionPhase.Initial; } + if (state == 1) { return MissionPhase.NoItemsDestroyed; } + if (state < destructibleItems.Count + 1) { return MissionPhase.SomeItemsDestroyed; } + if (state < destructibleItems.Count + 2) { return MissionPhase.AllItemsDestroyed; } + return MissionPhase.BossKilled; + } + } + + public EndMission(MissionPrefab prefab, Location[] locations, Submarine sub) + : base(prefab, locations, sub) + { + Identifier speciesName = prefab.ConfigElement.GetAttributeIdentifier("bossfile", Identifier.Empty); + if (!speciesName.IsEmpty) + { + bossPrefab = CharacterPrefab.FindBySpeciesName(speciesName); + if (bossPrefab == null) + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + } + } + else + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Monster file not set."); + } + + Identifier minionName = prefab.ConfigElement.GetAttributeIdentifier("minionfile", Identifier.Empty); + if (!minionName.IsEmpty) + { + minionPrefab = CharacterPrefab.FindBySpeciesName(minionName); + if (minionPrefab == null) + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + } + } + + minionCount = Math.Min(prefab.ConfigElement.GetAttributeInt(nameof(minionCount), 0), 255); + minionScatter = Math.Min(prefab.ConfigElement.GetAttributeFloat(nameof(minionScatter), 0), 10000); + + Identifier projectileId = prefab.ConfigElement.GetAttributeIdentifier("projectile", Identifier.Empty); + if (!projectileId.IsEmpty) + { + projectilePrefab = MapEntityPrefab.FindByIdentifier(projectileId) as ItemPrefab; + if (projectilePrefab == null) + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find an item prefab with the name \"{projectileId}\"."); + } + } + + spawnPointTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(spawnPointTag), Identifier.Empty); + destructibleItemTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(destructibleItemTag), Identifier.Empty); + endCinematicSound = prefab.ConfigElement.GetAttributeString(nameof(endCinematicSound), string.Empty); + startCinematicDistance = prefab.ConfigElement.GetAttributeFloat(nameof(startCinematicDistance), 0); + } + + protected override void StartMissionSpecific(Level level) + { + var spawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag)); + if (spawnPoint == null) + { + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\"."); + return; + } + if (!IsClient) + { + boss = Character.Create(bossPrefab.Identifier, spawnPoint.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); + var minionList = new List(); + float angle = 0; + float angleStep = MathHelper.TwoPi / Math.Max(minionCount, 1); + for (int i = 0; i < minionCount; i++) + { + minionList.Add(Character.Create(minionPrefab.Identifier, MathUtils.GetPointOnCircumference(spawnPoint.WorldPosition, minionScatter, angle), ToolBox.RandomSeed(8), createNetworkEvent: false)); + angle += angleStep; + } + SwarmBehavior.CreateSwarm(minionList.Cast()); + minions = minionList.ToImmutableArray(); + } + if (destructibleItemTag.IsEmpty) + { + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Destructible item tag not set."); + return; + } + destructibleItems.Clear(); + destructibleItems.AddRange(Item.ItemList.FindAll(it => it.HasTag(destructibleItemTag))); + if (destructibleItems.None()) + { + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\"."); + return; + } + } + + protected override void UpdateMissionSpecific(float deltaTime) + { + UpdateProjSpecific(); + + if (state == 0) + { + if (startCinematicDistance <= 0.0f || + boss == null || Submarine.MainSub == null || + Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, boss.WorldPosition) <= startCinematicDistance * startCinematicDistance) + { + State = 1; + } + return; + } + + if (!IsClient && State > 0) + { + State = Math.Max(State, destructibleItems.Count(it => it.Condition <= 0.0f) + 1); + } + + if (Phase == MissionPhase.AllItemsDestroyed) + { + if (projectilePrefab != null && boss != null && !boss.IsDead && !boss.Removed) + { + projectileTimer -= deltaTime; + if (projectileTimer <= 0.0f) + { + float dist = Vector2.Distance(Submarine.MainSub.WorldPosition, boss.WorldPosition); + float distanceFactor = Math.Min(dist / 10000.0f, 1.0f); + int projectileAmount = Rand.Range(3, 6); + //more concentrated shots the further the sub is + float spread = MathHelper.ToRadians(Rand.Range(20.0f, 180.0f)) * Math.Max(1.0f - distanceFactor, 0.2f); + for (int i = 0; i < projectileAmount; i++) + { + int index = i; + Entity.Spawner.AddItemToSpawnQueue(projectilePrefab, boss.WorldPosition, onSpawned: it => + { + var projectile = it.GetComponent(); + float angle = MathUtils.VectorToAngle(Submarine.MainSub.WorldPosition - boss.WorldPosition); + if (projectileAmount > 1) + { + angle += (index / (float)(projectileAmount - 1) - 0.5f) * spread; + } + it.body.SetTransform(it.SimPosition, angle); + it.UpdateTransform(); + //faster launch velocity the further the sub is + projectile.Use(launchImpulseModifier: MathHelper.Lerp(0, 5, distanceFactor)); + }); + } + + //the closer the sub is, more likely it is to shoot frequently + float shortIntervalProbability = MathHelper.Lerp(0.9f, 0.05f, distanceFactor); + if (Rand.Range(0.0f, 1.0f) < shortIntervalProbability) + { + projectileTimer = Rand.Range(3.0f, 5.0f); + } + else + { + projectileTimer = Rand.Range(15f, 30f); + } + } + } + else + { + State = Math.Max(destructibleItems.Count + 2, State); + } + } + else if (Phase == MissionPhase.BossKilled) + { + const float EndCinematicDuration = 20.0f; + + endCinematicTimer += deltaTime; +#if CLIENT + Screen.Selected.Cam.Shake = MathHelper.Clamp(MathF.Pow(endCinematicTimer, 3), 5.0f, 200.0f); + + + Screen.Selected.Cam.Rotation = + Math.Max((endCinematicTimer - 5.0f) * 0.05f, 0.0f) + + (PerlinNoise.GetPerlin(endCinematicTimer * 0.1f, endCinematicTimer * 0.05f) - 0.5f) * 0.5f * (endCinematicTimer / EndCinematicDuration); + if (Rand.Range(0.0f, 100.0f) < endCinematicTimer) + { + Level.Loaded.Renderer.Flash(); + } + Level.Loaded.Renderer.ChromaticAberrationStrength = endCinematicTimer * 5; + Level.Loaded.Renderer.CollapseEffectOrigin = boss.WorldPosition; + Level.Loaded.Renderer.CollapseEffectStrength = endCinematicTimer / EndCinematicDuration; +#endif + if (endCinematicTimer > 5 && !IsClient) + { + foreach (Character c in Character.CharacterList) + { + if (c.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior == null) + { + c.SetAllDamage(200.0f, 0.0f, 0.0f); + } + } + } + + if (endCinematicTimer > EndCinematicDuration && !IsClient) + { + //endCinematicTimer = 0; + GameMain.GameSession.Campaign?.LoadNewLevel(); + } + } + + } + + partial void UpdateProjSpecific(); + + partial void OnStateChangedProjSpecific(); + + protected override bool DetermineCompleted() + { + return Phase == MissionPhase.BossKilled; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index fe7c3ff84..51dd38d21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -10,11 +10,12 @@ namespace Barotrauma { partial class EscortMission : Mission { - private readonly XElement characterConfig; - private readonly XElement itemConfig; + private readonly ContentXElement characterConfig; + private readonly ContentXElement itemConfig; private readonly List characters = new List(); private readonly Dictionary> characterItems = new Dictionary>(); + private readonly Dictionary> characterStatusEffects = new Dictionary>(); private readonly int baseEscortedCharacters; private readonly float scalingEscortedCharacters; @@ -28,7 +29,8 @@ namespace Barotrauma private readonly List terroristCharacters = new List(); private bool terroristsShouldAct = false; private float terroristDistanceSquared; - private const string TerroristTeamChangeIdentifier = "terrorist"; + private const string TerroristTeamChangeIdentifier = "terrorist"; + private readonly string terroristAnnounceDialogTag = string.Empty; public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) @@ -39,6 +41,7 @@ namespace Barotrauma scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0); terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0); itemConfig = prefab.ConfigElement.GetChildElement("TerroristItems"); + terroristAnnounceDialogTag = prefab.ConfigElement.GetAttributeString("terroristannouncedialogtag", string.Empty); CalculateReward(); } @@ -96,14 +99,27 @@ namespace Barotrauma } List humanPrefabsToSpawn = new List(); - foreach (XElement element in characterConfig.Elements()) + foreach (ContentXElement characterElement in characterConfig.Elements()) { int count = CalculateScalingEscortedCharacterCount(inMission: true); - var humanPrefab = GetHumanPrefabFromElement(element); + var humanPrefab = GetHumanPrefabFromElement(characterElement); for (int i = 0; i < count; i++) { humanPrefabsToSpawn.Add(humanPrefab); } + foreach (var element in characterElement.Elements()) + { + if (element.NameAsIdentifier() == "statuseffect") + { + var newEffect = StatusEffect.Load(element, parentDebugName: Prefab.Name.Value); + if (newEffect == null) { continue; } + if (!characterStatusEffects.ContainsKey(humanPrefab)) + { + characterStatusEffects[humanPrefab] = new List { newEffect }; + } + characterStatusEffects[humanPrefab].Add(newEffect); + } + } } //if any of the escortees have a job defined, try to use a spawnpoint designated for that job @@ -128,6 +144,13 @@ namespace Barotrauma { humanAI.InitMentalStateManager(); } + if (characterStatusEffects.TryGetValue(humanPrefab, out var statusEffectList)) + { + foreach (var statusEffect in statusEffectList) + { + statusEffect.Apply(statusEffect.type, 1.0f, spawnedCharacter, spawnedCharacter); + } + } } @@ -162,7 +185,7 @@ namespace Barotrauma } int i = 0; - foreach (XElement element in characterConfig.Elements()) + foreach (ContentXElement element in characterConfig.Elements()) { string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty); string colorIdentifier = element.GetAttributeString("color", string.Empty); @@ -231,7 +254,10 @@ namespace Barotrauma if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands) { character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true)); - character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f)); + if (!string.IsNullOrEmpty(terroristAnnounceDialogTag)) + { + character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f)); + } XElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty); if (randomElement != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs index f0fa3a328..a1924db58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs @@ -1,4 +1,6 @@ -namespace Barotrauma +using System; + +namespace Barotrauma { partial class GoToMission : Mission { @@ -11,7 +13,7 @@ { if (Level.Loaded?.Type == LevelData.LevelType.Outpost) { - State = 1; + State = Math.Max(1, State); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 0d136f64f..90ac22989 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -11,20 +11,7 @@ namespace Barotrauma { partial class MineralMission : Mission { - private struct ResourceCluster - { - public int Amount; - public float Rotation; - - public ResourceCluster(int amount, float rotation) - { - Amount = amount; - Rotation = rotation; - } - - public static implicit operator ResourceCluster((int amount, float rotation) tuple) => new ResourceCluster(tuple.amount, tuple.rotation); - } - private readonly Dictionary resourceClusters = new Dictionary(); + private readonly Dictionary resourceAmounts = new Dictionary(); private readonly Dictionary> spawnedResources = new Dictionary>(); private readonly Dictionary relevantLevelResources = new Dictionary(); private readonly List<(Identifier Identifier, Vector2 Position)> missionClusterPositions = new List<(Identifier Identifier, Vector2 Position)>(); @@ -50,13 +37,13 @@ namespace Barotrauma /// private readonly float resourceHandoverAmount; - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { return missionClusterPositions - .Where(p => spawnedResources.ContainsKey(p.Item1) && AnyAreUncollected(spawnedResources[p.Item1])) - .Select(p => p.Item2); + .Where(p => spawnedResources.ContainsKey(p.Identifier) && AnyAreUncollected(spawnedResources[p.Identifier])) + .Select(p => (ModifyMessage(Prefab.SonarLabel, color: false), p.Position)); } } @@ -64,7 +51,6 @@ namespace Barotrauma public override LocalizedString FailureMessage => ModifyMessage(base.FailureMessage); public override LocalizedString Description => ModifyMessage(description); public override LocalizedString Name => ModifyMessage(base.Name, false); - public override LocalizedString SonarLabel => ModifyMessage(base.SonarLabel, false); public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { @@ -82,13 +68,13 @@ namespace Barotrauma { var identifier = c.GetAttributeIdentifier("identifier", Identifier.Empty); if (identifier.IsEmpty) { continue; } - if (resourceClusters.ContainsKey(identifier)) + if (resourceAmounts.ContainsKey(identifier)) { - resourceClusters[identifier] = (resourceClusters[identifier].Amount + 1, resourceClusters[identifier].Rotation); + resourceAmounts[identifier]++; } else { - resourceClusters.Add(identifier, (1, 0.0f)); + resourceAmounts.Add(identifier, 1); } } } @@ -129,7 +115,7 @@ namespace Barotrauma if (IsClient) { return; } - foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters) + foreach ((Identifier identifier, int amount) in resourceAmounts) { if (MapEntityPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { @@ -137,10 +123,10 @@ namespace Barotrauma continue; } - var spawnedResources = level.GenerateMissionResources(prefab, cluster.Amount, positionType, out float rotation, caves); - if (spawnedResources.Count < cluster.Amount) + var spawnedResources = level.GenerateMissionResources(prefab, amount, positionType, caves); + if (spawnedResources.Count < amount) { - DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{cluster.Amount} of {prefab.Name}"); + DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{amount} of {prefab.Name}"); } if (spawnedResources.None()) { continue; } @@ -175,7 +161,7 @@ namespace Barotrauma State = 1; break; case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } + if (!Submarine.MainSub.AtEitherExit) { return; } State = 2; break; } @@ -195,7 +181,7 @@ namespace Barotrauma { // When mission is completed successfully, half of the resources will be removed from the player (i.e. given to the outpost as a part of the mission) var handoverResources = new List(); - foreach (Identifier identifier in resourceClusters.Keys) + foreach (Identifier identifier in resourceAmounts.Keys) { if (relevantLevelResources.TryGetValue(identifier, out var availableResources)) { @@ -232,11 +218,11 @@ namespace Barotrauma private void FindRelevantLevelResources() { relevantLevelResources.Clear(); - foreach (var identifier in resourceClusters.Keys) + foreach (var identifier in resourceAmounts.Keys) { var items = Item.ItemList.Where(i => i.Prefab.Identifier == identifier && i.Submarine == null && i.ParentInventory == null && - (!(i.GetComponent() is Holdable h) || (h.Attachable && h.Attached))) + (i.GetComponent() is not Holdable h || (h.Attachable && h.Attached))) .ToArray(); relevantLevelResources.Add(identifier, items); } @@ -244,12 +230,12 @@ namespace Barotrauma private bool EnoughHaveBeenCollected() { - foreach (var kvp in resourceClusters) + foreach (var kvp in resourceAmounts) { if (relevantLevelResources.TryGetValue(kvp.Key, out var availableResources)) { var collected = availableResources.Count(HasBeenCollected); - var needed = kvp.Value.Amount; + var needed = kvp.Value; if (collected < needed) { return false; } } else @@ -300,10 +286,10 @@ namespace Barotrauma protected override LocalizedString ModifyMessage(LocalizedString message, bool color = true) { int i = 1; - foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters) + foreach ((Identifier identifier, int amount) in resourceAmounts) { Replace($"[resourcename{i}]", ItemPrefab.FindByIdentifier(identifier)?.Name.Value ?? ""); - Replace($"[resourcequantity{i}]", cluster.Amount.ToString()); + Replace($"[resourcequantity{i}]", amount.ToString()); i++; } Replace("[handoverpercentage]", ToolBox.GetFormattedPercentage(resourceHandoverAmount)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 0ccb6fd13..c0f125362 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -22,7 +22,7 @@ namespace Barotrauma public virtual int State { get { return state; } - protected set + set { if (state != value) { @@ -30,6 +30,11 @@ namespace Barotrauma TryTriggerEvents(state); #if SERVER GameMain.Server?.UpdateMissionState(this); +#elif CLIENT + if (Prefab.ShowProgressBar) + { + CharacterHUD.ShowMissionProgressBar(this); + } #endif ShowMessage(State); OnMissionStateChanged?.Invoke(this); @@ -37,6 +42,8 @@ namespace Barotrauma } } + public int TimesAttempted { get; set; } + protected static bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; private readonly CheckDataAction completeCheckDataAction; @@ -44,6 +51,12 @@ namespace Barotrauma public readonly ImmutableArray Headers; public readonly ImmutableArray Messages; + /// + /// The reward that was actually given from completing the mission, taking any talent bonuses into account + /// (some of which may not be possible to determine in advance) + /// + private int? finalReward; + public virtual LocalizedString Name => Prefab.Name; private readonly LocalizedString successMessage; @@ -113,15 +126,19 @@ namespace Barotrauma get { return null; } } - public virtual IEnumerable SonarPositions + public virtual IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { - get { return Enumerable.Empty(); } + get { return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } } - public virtual LocalizedString SonarLabel => Prefab.SonarLabel; - public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier; + /// + /// Where was this mission received from? Affects which faction we give reputation for if the mission is configured to give reputation for the faction that gave the mission. + /// Defaults to Locations[0] + /// + public Location OriginLocation; + public readonly Location[] Locations; public int? Difficulty @@ -141,7 +158,7 @@ namespace Barotrauma } } - private List delayedTriggerEvents = new List(); + private readonly List delayedTriggerEvents = new List(); public Action OnMissionStateChanged; @@ -157,12 +174,13 @@ namespace Barotrauma Headers = prefab.Headers; var messages = prefab.Messages.ToArray(); + OriginLocation = locations[0]; Locations = locations; var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction)); if (endConditionElement != null) { - completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier.ToString()})"); + completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})"); } for (int n = 0; n < 2; n++) @@ -307,7 +325,7 @@ namespace Barotrauma private void TryTriggerEvent(MissionPrefab.TriggerEvent trigger) { if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; } - if (trigger.Delay > 0) + if (trigger.Delay > 0 || trigger.State == 0) { if (!delayedTriggerEvents.Any(t => t.TriggerEvent == trigger)) { @@ -357,6 +375,8 @@ namespace Barotrauma GiveReward(); } + TimesAttempted++; + EndMissionSpecific(completed); } @@ -364,6 +384,27 @@ namespace Barotrauma protected virtual void EndMissionSpecific(bool completed) { } + /// + /// Get the final reward, taking talent bonuses into account if the mission has concluded and the talents modified the reward accordingly. + /// + public int GetFinalReward(Submarine sub) + { + return finalReward ?? GetReward(sub); + } + + /// + /// Calculates the final reward after talent bonuses have been applied. Note that this triggers talent effects of the type OnGainMissionMoney, + /// and should only be called once when the mission is completed! + /// + private void CalculateFinalReward(Submarine sub) + { + int reward = GetReward(sub); + IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); + crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); + crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); + finalReward = (int)(reward * missionMoneyGainMultiplier.Value); + } private void GiveReward() { @@ -407,39 +448,35 @@ namespace Barotrauma info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value)); } - // apply money gains afterwards to prevent them from affecting XP gains - var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); - crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); - crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); - - int totalReward = (int)(reward * missionMoneyGainMultiplier.Value); - GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value); - + CalculateFinalReward(Submarine.MainSub); #if SERVER - totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward); + finalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), finalReward.Value); #endif bool isSingleplayerOrServer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; - if (isSingleplayerOrServer && totalReward > 0) + if (isSingleplayerOrServer) { - campaign.Bank.Give(totalReward); - } - - foreach (Character character in crewCharacters) - { - character.Info.MissionsCompletedSinceDeath++; - } - - foreach (KeyValuePair reputationReward in ReputationRewards) - { - if (reputationReward.Key == "location") + if (finalReward > 0) { - Locations[0].Reputation.AddReputation(reputationReward.Value); - Locations[1].Reputation.AddReputation(reputationReward.Value); + campaign.Bank.Give(finalReward.Value); } - else + + foreach (Character character in crewCharacters) { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); - if (faction != null) { faction.Reputation.AddReputation(reputationReward.Value); } + character.Info.MissionsCompletedSinceDeath++; + } + + foreach (KeyValuePair reputationReward in ReputationRewards) + { + if (reputationReward.Key == "location") + { + OriginLocation.Reputation?.AddReputation(reputationReward.Value); + } + else + { + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); + float prevValue = faction.Reputation.Value; + faction?.Reputation.AddReputation(reputationReward.Value); + } } } @@ -484,18 +521,15 @@ namespace Barotrauma float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; int rewardPercentage = (int)(rewardWeight * 100); - return reward switch - { - Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage, sum), - None _ => (0, rewardPercentage, sum), - _ => throw new ArgumentOutOfRangeException() - }; + int amount = reward.TryUnwrap(out var a) ? a : 0; + + return ((int)(amount * rewardWeight), rewardPercentage, sum); } protected void ChangeLocationType(LocationTypeChange change) { if (change == null) { throw new ArgumentException(); } - if (GameMain.GameSession.GameMode is CampaignMode && !IsClient) + if (GameMain.GameSession.GameMode is CampaignMode campaign && !IsClient) { int srcIndex = -1; for (int i = 0; i < Locations.Length; i++) @@ -509,13 +543,15 @@ namespace Barotrauma if (srcIndex == -1) { return; } var location = Locations[srcIndex]; + if (location.LocationTypeChangesBlocked) { return; } + if (change.RequiredDurationRange.X > 0) { location.PendingLocationTypeChange = (change, Rand.Range(change.RequiredDurationRange.X, change.RequiredDurationRange.Y), Prefab); } else { - location.ChangeType(LocationType.Prefabs[change.ChangeToType]); + location.ChangeType(campaign, LocationType.Prefabs[change.ChangeToType]); location.LocationTypeChangeCooldown = change.CooldownAfterChange; } } @@ -529,7 +565,6 @@ namespace Barotrauma if (element.Attribute("name") != null) { DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters."); - return null; } @@ -538,7 +573,7 @@ namespace Barotrauma HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); if (humanPrefab == null) { - DebugConsole.ThrowError("Couldn't spawn character for mission: character prefab \"" + characterIdentifier + "\" not found"); + DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\"."); return null; } @@ -557,8 +592,7 @@ namespace Barotrauma Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); spawnedCharacter.HumanPrefab = humanPrefab; humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn); - humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.ServerAndClient, createNetworkEvents: false); - + humanPrefab.GiveItems(spawnedCharacter, submarine, positionToStayIn as WayPoint, Rand.RandSync.ServerAndClient, createNetworkEvents: false); characters.Add(spawnedCharacter); characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 92f08baec..90e4d007b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -25,7 +25,8 @@ namespace Barotrauma GoTo = 0x400, ScanAlienRuins = 0x800, ClearAlienRuins = 0x1000, - All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins + End = 0x2000, + All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins | End } partial class MissionPrefab : PrefabWithUintIdentifier @@ -45,14 +46,15 @@ namespace Barotrauma { MissionType.Pirate, typeof(PirateMission) }, { MissionType.GoTo, typeof(GoToMission) }, { MissionType.ScanAlienRuins, typeof(ScanMission) }, - { MissionType.ClearAlienRuins, typeof(AlienRuinMission) } + { MissionType.ClearAlienRuins, typeof(AlienRuinMission) }, + { MissionType.End, typeof(EndMission) } }; public static readonly Dictionary PvPMissionClasses = new Dictionary() { { MissionType.Combat, typeof(CombatMission) } }; - public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo }; + public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo, MissionType.End }; private readonly ConstructorInfo constructor; @@ -62,11 +64,7 @@ namespace Barotrauma public readonly Identifier TextIdentifier; - private readonly string[] tags; - public IEnumerable Tags - { - get { return tags; } - } + public readonly ImmutableHashSet Tags; public readonly LocalizedString Name; public readonly LocalizedString Description; @@ -93,10 +91,24 @@ namespace Barotrauma public readonly bool AllowRetry; + public readonly bool ShowInMenus, ShowStartMessage; + public readonly bool IsSideObjective; + public readonly bool AllowOtherMissionsInLevel; + public readonly bool RequireWreck, RequireRuin; + /// + /// If enabled, locations this mission takes place in cannot change their type + /// + public readonly bool BlockLocationTypeChanges; + + public readonly bool ShowProgressBar; + public readonly bool ShowProgressInNumbers; + public readonly int MaxProgressState; + public readonly LocalizedString ProgressBarLabel; + /// /// The mission can only be received when travelling from a location of the first type to a location of the second type /// @@ -144,7 +156,7 @@ namespace Barotrauma TextIdentifier = element.GetAttributeIdentifier("textidentifier", Identifier); - tags = element.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + Tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet(); string nameTag = element.GetAttributeString("name", ""); Name = TextManager.Get($"MissionName.{TextIdentifier}"); @@ -167,16 +179,26 @@ namespace Barotrauma Reward = element.GetAttributeInt("reward", 1); AllowRetry = element.GetAttributeBool("allowretry", false); + 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); + BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false); Commonness = element.GetAttributeInt("commonness", 1); + AllowOtherMissionsInLevel = element.GetAttributeBool("allowothermissionsinlevel", true); if (element.GetAttribute("difficulty") != null) { int difficulty = element.GetAttributeInt("difficulty", MinDifficulty); Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } + ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false); + ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false); + MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1); + string progressBarLabel = element.GetAttributeString(nameof(ProgressBarLabel), ""); + ProgressBarLabel = TextManager.Get(progressBarLabel).Fallback(progressBarLabel); + string successMessageTag = element.GetAttributeString("successmessage", ""); SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}"); if (!string.IsNullOrEmpty(successMessageTag)) @@ -350,6 +372,7 @@ namespace Barotrauma { return AllowedLocationTypes.Any(lt => lt == "any") || + AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) || AllowedLocationTypes.Any(lt => lt == from.Type.Identifier); } @@ -357,11 +380,11 @@ namespace Barotrauma { if (fromType == "any" || fromType == from.Type.Identifier || - (fromType == "anyoutpost" && from.HasOutpost())) + (fromType == "anyoutpost" && from.HasOutpost() && from.Type.Identifier != "abandoned")) { if (toType == "any" || toType == to.Type.Identifier || - (toType == "anyoutpost" && to.HasOutpost())) + (toType == "anyoutpost" && to.HasOutpost() && to.Type.Identifier != "abandoned")) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 9b3641502..a9ab792ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -16,17 +16,20 @@ namespace Barotrauma private readonly Level.PositionType spawnPosType; private Vector2? spawnPos = null; - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { if (State > 0) { - return Enumerable.Empty(); + yield break; } else { - return sonarPositions; + foreach (Vector2 sonarPos in sonarPositions) + { + yield return (Prefab.SonarLabel, sonarPos); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index bc71d49dc..e849a5499 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -31,17 +31,17 @@ namespace Barotrauma private Vector2 nestPosition; - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { if (State > 0) { - Enumerable.Empty(); + yield break; } else { - yield return nestPosition; + yield return (Prefab.SonarLabel, nestPosition); } } } @@ -260,9 +260,25 @@ namespace Barotrauma int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1); for (int i = 0; i < amount; i++) { - Character.Create(monster.Item1.Identifier, nestPosition + Rand.Vector(100.0f), ToolBox.RandomSeed(8), createNetworkEvent: true); + Vector2 offsetPosition; + int tries = 0; + do + { + offsetPosition = nestPosition + Rand.Vector(100.0f); + tries++; + if (tries > 10) + { + offsetPosition = nestPosition; + break; + } + } while (Level.Loaded.IsPositionInsideWall(offsetPosition)); + Character.Create(monster.Item1.Identifier, offsetPosition, ToolBox.RandomSeed(8), createNetworkEvent: true); } } + if (Level.Loaded.IsPositionInsideWall(nestPosition)) + { + DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition})."); + } monsterPrefabs.Clear(); break; } @@ -274,7 +290,7 @@ namespace Barotrauma break; case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } + if (!Submarine.MainSub.AtEitherExit) { return; } State = 2; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index ff33e7ec9..ecc29fcb6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -36,23 +36,32 @@ namespace Barotrauma private readonly List patrolPositions = new List(); - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { - var empty = Enumerable.Empty(); - if (outsideOfSonarRange) + if (!outsideOfSonarRange || state > 1) { - return State switch - { - 0 => patrolPositions, - 1 => lastSighting.HasValue ? lastSighting.Value.ToEnumerable() : empty, - _ => empty, - }; + yield break; + } - else + else if (state == 0) { - return empty; + foreach (Vector2 patrolPos in patrolPositions) + { + yield return (Prefab.SonarLabel, patrolPos); + } + } + else if (state == 1) + { + if (lastSighting.HasValue) + { + yield return (Prefab.SonarLabel, lastSighting.Value); + } + else + { + yield break; + } } } } @@ -85,6 +94,31 @@ namespace Barotrauma characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes"); addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0); + //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); + if (characterTypeElement == null) + { + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\"."); + } + } + //make sure all defined character types can be found from human prefabs + foreach (XElement characterTypeElement in characterTypeConfig.Elements()) + { + foreach (XElement characterElement in characterTypeElement.Elements()) + { + Identifier characterIdentifier = characterElement.GetAttributeIdentifier("identifier", Identifier.Empty); + Identifier characterFrom = characterElement.GetAttributeIdentifier("from", Identifier.Empty); + HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); + if (humanPrefab == null) + { + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\"."); + } + } + } + // for campaign missions, set level at construction LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData; if (levelData != null) @@ -100,6 +134,7 @@ namespace Barotrauma //level already set return; } + submarineInfo = null; levelData = level; missionDifficulty = level?.Difficulty ?? 0; @@ -117,8 +152,15 @@ namespace Barotrauma DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!"); return; } - // maybe a little redundant - var contentFile = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()).FirstOrDefault(x => x.Path == submarinePath); + + 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}!"); @@ -241,9 +283,10 @@ namespace Barotrauma // 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); for (int i = 0; i < amountCreated; i++) { - XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == element.GetAttributeString("typeidentifier", string.Empty)).FirstOrDefault(); + XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId).FirstOrDefault(); if (characterType == null) { @@ -253,7 +296,10 @@ namespace Barotrauma XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier); - Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(variantElement), characters, characterItems, enemySub, CharacterTeamType.None, null); + var humanPrefab = GetHumanPrefabFromElement(variantElement); + if (humanPrefab == null) { continue; } + + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, enemySub, CharacterTeamType.None, null); if (!commanderAssigned) { bool isCommander = variantElement.GetAttributeBool("iscommander", false); @@ -305,8 +351,9 @@ namespace Barotrauma if (enemySub == null) { - DebugConsole.ThrowError($"Enemy Submarine was not created. SubmarineInfo is likely not defined."); - // TODO: should we set the state to something here? + DebugConsole.ThrowError(submarineInfo == null ? + $"Error in PirateMission: enemy sub was not created (submarineInfo == null)." : + $"Error in PirateMission: enemy sub was not created."); return; } @@ -345,10 +392,11 @@ namespace Barotrauma protected override void UpdateMissionSpecific(float deltaTime) { - if (state >= 2) { return; } + if (state >= 2 || enemySub == null) { return; } float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange); outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange; + if (CheckWinState()) { State = 2; @@ -411,6 +459,7 @@ namespace Barotrauma characters.Clear(); characterItems.Clear(); failed = !completed; + submarineInfo = null; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 0d1b41b98..3a0b83978 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -5,40 +5,182 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { partial class SalvageMission : Mission { - private readonly ItemPrefab itemPrefab; - private Item item; - - private readonly Level.PositionType spawnPositionType; - - private readonly string containerTag; - - private readonly string existingItemTag; - - private readonly bool showMessageWhenPickedUp; - - /// - /// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list. - /// - private readonly List> statusEffects = new List>(); - - public override IEnumerable SonarPositions + private class Target { - get + public Item Item; + + /// + /// Note that the integer values matter here: the state of the target can't go back to a smaller value, + /// and a larger or equal value than the RequiredRetrievalState means the item counts as retrieved + /// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub) + /// + public enum RetrievalState + { + None = 0, + Interact = 1, + PickedUp = 2, + RetrievedToSub = 3 + } + + public readonly ItemPrefab ItemPrefab; + public readonly Level.PositionType SpawnPositionType; + public readonly string ContainerTag; + public readonly string ExistingItemTag; + + public readonly bool RemoveItem; + + public readonly LocalizedString SonarLabel; + + public readonly bool AllowContinueBeforeRetrieved; + + /// + /// Does the target need to be picked up or brought to the sub for mission to be considered successful. + /// If None, the target has no effect on the completion of the mission. + /// + public readonly RetrievalState RequiredRetrievalState; + + public readonly bool HideLabelAfterRetrieved; + + public bool Retrieved { - if (item == null) + get { - Enumerable.Empty(); + return RequiredRetrievalState switch + { + RetrievalState.None => true, + RetrievalState.Interact or RetrievalState.PickedUp => State >= RequiredRetrievalState, + RetrievalState.RetrievedToSub => State == RetrievalState.RetrievedToSub, + _ => throw new NotImplementedException(), + }; + } + } + + private RetrievalState state; + public RetrievalState State + { + get { return state; } + set + { + if (value == state) { return; } + state = value; +#if SERVER + GameMain.Server?.UpdateMissionState(mission); +#endif + } + } + + public bool Interacted; + + private readonly SalvageMission mission; + + /// + /// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list. + /// + public readonly List> StatusEffects = new List>(); + + public Target(ContentXElement element, SalvageMission mission) + { + this.mission = mission; + ContainerTag = element.GetAttributeString("containertag", ""); + RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", RetrievalState.RetrievedToSub); + AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", false); + HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", false); + + string sonarLabelTag = element.GetAttributeString("sonarlabel", ""); + if (!string.IsNullOrEmpty(sonarLabelTag)) + { + SonarLabel = + TextManager.Get($"MissionSonarLabel.{sonarLabelTag}") + .Fallback(TextManager.Get(sonarLabelTag)) + .Fallback(element.GetAttributeString("sonarlabel", "")); + } + ExistingItemTag = element.GetAttributeString("existingitemtag", ""); + + RemoveItem = element.GetAttributeBool("removeitem", true); + + if (element.GetAttribute("itemname") != null) + { + DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); + string itemName = element.GetAttributeString("itemname", ""); + ItemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; + if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\""); + } } else { - yield return item.GetRootInventoryOwner()?.WorldPosition ?? item.WorldPosition; + Identifier itemIdentifier = element.GetAttributeIdentifier("itemidentifier", Identifier.Empty); + if (!itemIdentifier.IsEmpty) + { + ItemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; + } + if (ItemPrefab == null) + { + string itemTag = element.GetAttributeString("itemtag", ""); + ItemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; + } + if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\""); + } + } + + SpawnPositionType = element.GetAttributeEnum("spawntype", Level.PositionType.Cave | Level.PositionType.Ruin); + + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "statuseffect": + { + var newEffect = StatusEffect.Load(subElement, parentDebugName: mission.Prefab.Name.Value); + if (newEffect == null) { continue; } + StatusEffects.Add(new List { newEffect }); + break; + } + case "chooserandom": + StatusEffects.Add(new List()); + foreach (var effectElement in subElement.Elements()) + { + var newEffect = StatusEffect.Load(effectElement, parentDebugName: mission.Prefab.Name.Value); + if (newEffect == null) { continue; } + StatusEffects.Last().Add(newEffect); + } + break; + } + } + } + + public void Reset() + { + state = RetrievalState.None; + Item = null; + } + } + + private readonly List targets = new List(); + + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels + { + get + { + foreach (var target in targets) + { + if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; } + if (target.Item != null) + { + yield return ( + target.SonarLabel ?? Prefab.SonarLabel, + target.Item.GetRootInventoryOwner()?.WorldPosition ?? target.Item.WorldPosition); + } + if (!target.AllowContinueBeforeRetrieved && !target.Retrieved) { break; } } } } @@ -46,225 +188,254 @@ namespace Barotrauma public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - containerTag = prefab.ConfigElement.GetAttributeString("containertag", ""); - - if (prefab.ConfigElement.GetAttribute("itemname") != null) + foreach (ContentXElement subElement in prefab.ConfigElement.Elements()) { - DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); - string itemName = prefab.ConfigElement.GetAttributeString("itemname", ""); - itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; - if (itemPrefab == null) + if (subElement.NameAsIdentifier() == "target") { - DebugConsole.ThrowError("Error in SalvageMission: couldn't find an item prefab with the name " + itemName); + targets.Add(new Target(subElement, this)); } } - else + if (!targets.Any()) { - string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", null); - if (itemIdentifier != null) - { - itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; - } - if (itemPrefab == null) - { - string itemTag = prefab.ConfigElement.GetAttributeString("itemtag", ""); - itemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; - } - if (itemPrefab == null) - { - DebugConsole.ThrowError("Error in SalvageMission - couldn't find an item prefab with the identifier " + itemIdentifier); - } - } - - existingItemTag = prefab.ConfigElement.GetAttributeString("existingitemtag", ""); - showMessageWhenPickedUp = prefab.ConfigElement.GetAttributeBool("showmessagewhenpickedup", false); - - string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); - if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) || - !Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType)) - { - spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin; - } - - foreach (var element in prefab.ConfigElement.Elements()) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "statuseffect": - { - var newEffect = StatusEffect.Load(element, parentDebugName: prefab.Name.Value); - if (newEffect == null) { continue; } - statusEffects.Add(new List { newEffect }); - break; - } - case "chooserandom": - statusEffects.Add(new List()); - foreach (var subElement in element.Elements()) - { - var newEffect = StatusEffect.Load(subElement, parentDebugName: prefab.Name.Value); - if (newEffect == null) { continue; } - statusEffects.Last().Add(newEffect); - } - break; - } + targets.Add(new Target(prefab.ConfigElement, this)); } } protected override void StartMissionSpecific(Level level) { #if SERVER - originalInventoryID = Entity.NullEntityID; + spawnInfo.Clear(); #endif - item = null; - if (!IsClient) + foreach (var target in targets) { - //ruin/cave/wreck items are allowed to spawn close to the sub - float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Cave || spawnPositionType == Level.PositionType.Wreck ? - 0.0f : Level.Loaded.Size.X * 0.3f; - Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f); - - if (!string.IsNullOrEmpty(existingItemTag)) + bool usedExistingItem = false; + UInt16 originalInventoryID = 0; + byte originalItemContainerIndex = 0; + int originalSlotIndex = 0; + var executedEffectIndices = new List<(int listIndex, int effectIndex)>(); + + target.Reset(); + if (!IsClient) { - var suitableItems = Item.ItemList.Where(it => it.HasTag(existingItemTag)); - switch (spawnPositionType) + //ruin/cave/wreck items are allowed to spawn close to the sub + float minDistance = target.SpawnPositionType switch { - case Level.PositionType.Cave: - case Level.PositionType.MainPath: - case Level.PositionType.SidePath: - item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f); - break; - case Level.PositionType.Ruin: - case Level.PositionType.Wreck: - foreach (Item it in suitableItems) - { - if (it.Submarine?.Info == null) { continue; } - if (spawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; } - if (spawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } - Rectangle worldBorders = it.Submarine.Borders; - worldBorders.Location += it.Submarine.WorldPosition.ToPoint(); - if (Submarine.RectContains(worldBorders, it.WorldPosition)) - { - item = it; -#if SERVER - usedExistingItem = true; -#endif - break; - } - } - break; - } - } + Level.PositionType.Ruin or + Level.PositionType.Cave or + Level.PositionType.Wreck or + Level.PositionType.Outpost => 0.0f, + _ => Level.Loaded.Size.X * 0.3f, + }; + Vector2 position = + target.SpawnPositionType == Level.PositionType.None ? + Vector2.Zero : + Level.Loaded.GetRandomItemPos(target.SpawnPositionType, 100.0f, minDistance, 30.0f); - if (item == null) - { - item = new Item(itemPrefab, position, null); - item.body.SetTransformIgnoreContacts(item.body.SimPosition, item.body.Rotation); - item.body.FarseerBody.BodyType = BodyType.Kinematic; - } - - for (int i = 0; i < statusEffects.Count; i++) - { - List effectList = statusEffects[i]; - if (effectList.Count == 0) { continue; } - int effectIndex = Rand.Int(effectList.Count); - var selectedEffect = effectList[effectIndex]; - item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position); -#if SERVER - executedEffectIndices.Add(new Pair(i, effectIndex)); -#endif - } - - //try to find a container and place the item inside it - if (!string.IsNullOrEmpty(containerTag) && item.ParentInventory == null) - { - List validContainers = new List(); - foreach (Item it in Item.ItemList) + if (!string.IsNullOrEmpty(target.ExistingItemTag)) { - if (!it.HasTag(containerTag)) { continue; } - if (!it.IsPlayerTeamInteractable) { continue; } - switch (spawnPositionType) + var suitableItems = Item.ItemList.Where(it => it.HasTag(target.ExistingItemTag)); + if (GameMain.GameSession?.Missions != null) + { + //don't choose an item that was already chosen as the target for another salvage mission + suitableItems = suitableItems.Where(it => + GameMain.GameSession.Missions.None(m => m != this && m is SalvageMission salvageMission && salvageMission.targets.Any(t => t.Item == it))); + } + switch (target.SpawnPositionType) { case Level.PositionType.Cave: case Level.PositionType.MainPath: - if (it.Submarine != null) { continue; } + case Level.PositionType.SidePath: + target.Item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f); +#if SERVER + usedExistingItem = target.Item != null; +#endif break; case Level.PositionType.Ruin: - if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; } - break; case Level.PositionType.Wreck: - if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } + case Level.PositionType.Outpost: + foreach (Item it in suitableItems) + { + if (it.Submarine?.Info == null) { continue; } + if (target.SpawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; } + if (target.SpawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } + if (target.SpawnPositionType == Level.PositionType.Outpost && it.Submarine.Info.Type != SubmarineType.Outpost) { continue; } + Rectangle worldBorders = it.Submarine.Borders; + worldBorders.Location += it.Submarine.WorldPosition.ToPoint(); + if (Submarine.RectContains(worldBorders, it.WorldPosition)) + { + target.Item = it; +#if SERVER + usedExistingItem = true; +#endif + break; + } + } + break; + default: + target.Item = suitableItems.FirstOrDefault(); +#if SERVER + usedExistingItem = target.Item != null; +#endif break; } - var itemContainer = it.GetComponent(); - if (itemContainer != null && itemContainer.Inventory.CanBePut(item)) { validContainers.Add(itemContainer); } } - if (validContainers.Any()) + + if (target.Item == null) { - var selectedContainer = validContainers.GetRandomUnsynced(); - if (selectedContainer.Combine(item, user: null)) + if (target.ItemPrefab == null && string.IsNullOrEmpty(target.ContainerTag)) { + DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag ?? "null"}"); + continue; + } + target.Item = new Item(target.ItemPrefab, position, null); + target.Item.body.SetTransformIgnoreContacts(target.Item.body.SimPosition, target.Item.body.Rotation); + target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; + } + else if (target.RequiredRetrievalState == Target.RetrievalState.Interact) + { + target.Item.OnInteract += () => + { + target.Interacted = true; + }; + } + for (int i = 0; i < target.StatusEffects.Count; i++) + { + List effectList = target.StatusEffects[i]; + if (effectList.Count == 0) { continue; } + int effectIndex = Rand.Int(effectList.Count); + var selectedEffect = effectList[effectIndex]; + target.Item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: target.Item.Position); #if SERVER - originalInventoryID = selectedContainer.Item.ID; - originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer); - originalSlotIndex = item.ParentInventory?.FindIndex(item) ?? -1; + executedEffectIndices.Add((i, effectIndex)); #endif - } // Placement successful + } + + //try to find a container and place the item inside it + if (!string.IsNullOrEmpty(target.ContainerTag) && target.Item.ParentInventory == null) + { + List validContainers = new List(); + foreach (Item it in Item.ItemList) + { + if (!it.HasTag(target.ContainerTag)) { continue; } + if (!it.IsPlayerTeamInteractable) { continue; } + switch (target.SpawnPositionType) + { + case Level.PositionType.Cave: + case Level.PositionType.MainPath: + if (it.Submarine != null) { continue; } + break; + case Level.PositionType.Ruin: + if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; } + break; + case Level.PositionType.Wreck: + if (it.Submarine?.Info == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } + break; + } + var itemContainer = it.GetComponent(); + if (itemContainer != null && itemContainer.Inventory.CanBePut(target.Item)) { validContainers.Add(itemContainer); } + } + if (validContainers.Any()) + { + var selectedContainer = validContainers.GetRandomUnsynced(); + if (selectedContainer.Combine(target.Item, user: null)) + { +#if SERVER + originalInventoryID = selectedContainer.Item.ID; + originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer); + originalSlotIndex = target.Item.ParentInventory?.FindIndex(target.Item) ?? -1; +#endif + } // Placement successful + } } } +#if SERVER + spawnInfo.Add( + target, + new SpawnInfo(usedExistingItem, originalInventoryID, originalItemContainerIndex, originalSlotIndex, executedEffectIndices)); +#endif } } protected override void UpdateMissionSpecific(float deltaTime) { - if (item == null) + //make body dynamic when picked up + foreach (var target in targets) { -#if DEBUG - DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)"); -#endif - return; + var root = target.Item?.GetRootContainer() ?? target.Item; + if (root == null) { continue; } + if (target.Item.ParentInventory != null && target.Item.body != null) { target.Item.body.FarseerBody.BodyType = BodyType.Dynamic; } } - if (IsClient) + if (IsClient) { return; } + + for (int i = 0; i < targets.Count; i++) { - if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } - return; - } - switch (State) - { - case 0: - if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } - if (showMessageWhenPickedUp) - { - if (!(item.GetRootInventoryOwner() is Character)) { return; } - } - else - { - Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine; - if (parentSub == null || parentSub.Info.Type != SubmarineType.Player) + var target = targets[i]; + if (i > 0 && !targets[i - 1].AllowContinueBeforeRetrieved && !targets[i - 1].Retrieved) { break; } + if (target.Item == null) + { +#if DEBUG + DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)"); +#endif + return; + } + switch (target.State) + { + case Target.RetrievalState.None: + if (target.Interacted) { - return; + TrySetRetrievalState(Target.RetrievalState.Interact); } - } - State = 1; - break; - case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } - State = 2; - break; + var root = target.Item?.GetRootContainer() ?? target.Item; + if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1) + { + TrySetRetrievalState(Target.RetrievalState.PickedUp); + } + break; + case Target.RetrievalState.PickedUp: + Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine; + if (parentSub != null) + { + if (parentSub.Info.Type == SubmarineType.Player || Level.IsLoadedFriendlyOutpost) + { + TrySetRetrievalState(Target.RetrievalState.RetrievedToSub); + } + } + break; + } + + void TrySetRetrievalState(Target.RetrievalState retrievalState) + { + if (retrievalState < target.State) { return; } + bool wasRetrieved = false; + target.State = retrievalState; + //increment the mission state if the target became retrieved + if (!wasRetrieved && target.Retrieved) { State = i + 1; } + } + } + if (targets.All(t => t.Retrieved)) + { + State = targets.Count + 1; } } protected override bool DetermineCompleted() { - var root = item?.GetRootContainer() ?? item; - return root?.CurrentHull?.Submarine != null && (root.CurrentHull.Submarine.AtEndExit || root.CurrentHull.Submarine.AtStartExit) && !item.Removed; + return targets.All(t => t.State >= t.RequiredRetrievalState); } protected override void EndMissionSpecific(bool completed) { - item?.Remove(); - item = null; - failed = !completed && state > 0; + //consider failed (can't attempt again) if we picked up any of the items but failed to bring them out of the level + failed = !completed && targets.Any(t => t.State >= Target.RetrievalState.PickedUp); + foreach (var target in targets) + { + if (target.RemoveItem) + { + target.Item?.Remove(); + target.Reset(); + } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index a59c69f8d..b8a2f0936 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -32,25 +32,20 @@ namespace Barotrauma } } - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { - if (State > 0) + if (State > 0 || scanTargets.None()) { - return Enumerable.Empty(); - } - else if (scanTargets.Any()) - { - return scanTargets - .Where(kvp => !kvp.Value) - .Select(kvp => kvp.Key.WorldPosition); + return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } else { - return Enumerable.Empty(); - } - + return scanTargets + .Where(kvp => !kvp.Value) + .Select(kvp => (Prefab.SonarLabel, kvp.Key.WorldPosition)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index d4e57fc47..e45b7b4cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -244,7 +244,12 @@ namespace Barotrauma float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition); foreach (Submarine sub in Submarine.Loaded) { - if (sub.Info.Type != SubmarineType.Player && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; } + if (sub.Info.Type != SubmarineType.Player && + sub.Info.Type != SubmarineType.EnemySubmarine && + sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) + { + continue; + } float minDistToSub = GetMinDistanceToSub(sub); if (dist < minDistToSub * minDistToSub) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index cbb841a31..703aa19ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -116,7 +116,7 @@ namespace Barotrauma { foreach (Entity entity in Entity.GetEntities()) { - if (targetPredicates[tag].Any(p => p(entity))) + if (targetPredicates[tag].Any(p => p(entity)) && !targetsToReturn.Contains(entity)) { targetsToReturn.Add(entity); } @@ -131,7 +131,7 @@ namespace Barotrauma { foreach (Character npc in outpostNPCs) { - if (npc.Removed) { continue; } + if (npc.Removed || targetsToReturn.Contains(npc)) { continue; } targetsToReturn.Add(npc); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 7aa700475..272d38833 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -82,7 +82,13 @@ namespace Barotrauma.Extensions return count == 0 ? default : source.ElementAt(Rand.Range(0, count, Rand.RandSync.Unsynced)); } } - + + public static T GetRandom(this IEnumerable source, Random rand) + where T : PrefabWithUintIdentifier + { + return source.OrderBy(p => p.UintIdentifier).ToArray().GetRandom(rand); + } + public static T GetRandom(this IEnumerable source, Rand.RandSync randSync) where T : PrefabWithUintIdentifier { @@ -305,11 +311,14 @@ namespace Barotrauma.Extensions => source .Where(nullable => nullable.HasValue) .Select(nullable => nullable.Value); - + public static IEnumerable NotNone(this IEnumerable> source) - => source - .OfType>() - .Select(some => some.Value); + { + foreach (var o in source) + { + if (o.TryUnwrap(out var v)) { yield return v; } + } + } public static IEnumerable Successes( this IEnumerable> source) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs index 3b56fd1aa..7322cb985 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs @@ -19,5 +19,12 @@ namespace Barotrauma public static string RemoveFromEnd(this string s, string substr, StringComparison stringComparison = StringComparison.Ordinal) => s.EndsWith(substr, stringComparison) ? s.Substring(0, s.Length - substr.Length) : s; + + public static bool IsTrueString(this string s) + => s.Length == 4 + && s[0] is 'T' or 't' + && s[1] is 'R' or 'r' + && s[2] is 'U' or 'u' + && s[3] is 'E' or 'e'; } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 1a741f3b7..0560da516 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -260,10 +260,12 @@ namespace Barotrauma } bool success = false; bool isCampaign = GameMain.GameSession?.GameMode is CampaignMode; + float levelDifficulty = Level.Loaded?.Difficulty ?? 0.0f; foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers) { if (preferredContainer.CampaignOnly && !isCampaign) { continue; } if (preferredContainer.NotCampaign && isCampaign) { continue; } + if (levelDifficulty < preferredContainer.MinLevelDifficulty || levelDifficulty > preferredContainer.MaxLevelDifficulty) { continue; } if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0 && preferredContainer.Amount <= 0) { continue; } validContainers = GetValidContainers(preferredContainer, containers, validContainers, primary: true); if (validContainers.None()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index edcf1532a..1f3535788 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -8,6 +8,8 @@ using System.Linq; using System.Text; using System.Xml.Linq; using Barotrauma.Networking; +using System.Collections; +using System.Collections.Immutable; #if SERVER using Barotrauma.Networking; #endif @@ -414,14 +416,31 @@ namespace Barotrauma } } #endif - return Submarine.MainSub.GetItems(true).FindAll(item => + return FindAllSellableItems().Where(it => IsItemSellable(it, confirmedSoldEntities)); + } + + public static IReadOnlyCollection FindAllItemsOnPlayerAndSub(Character character) + { + List allItems = new(); + if (character?.Inventory is { } inv) + { + allItems.AddRange(inv.FindAllItems(recursive: true)); + } + allItems.AddRange(FindAllSellableItems()); + return allItems; + } + + public static IEnumerable FindAllSellableItems() + { + if (Submarine.MainSub is null) { return Enumerable.Empty(); } + + return Submarine.MainSub.GetItems(true).FindAll(static item => { - if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } if (item.GetRootInventoryOwner() is Character) { return false; } - if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } - if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } + if (!item.Components.All(static c => c is not Holdable { Attachable: true, Attached: true })) { return false; } + if (!item.Components.All(static c => c is not Wire w || w.Connections.All(static c => c is null))) { return false; } if (!ItemAndAllContainersInteractable(item)) { return false; } - if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } + if (item.GetRootContainer() is { } rootContainer && rootContainer.HasTag("dontsellitems")) { return false; } return true; }).Distinct(); @@ -471,6 +490,21 @@ namespace Barotrauma return true; } + public static IEnumerable FindCargoRooms(IEnumerable subs) => subs.SelectMany(s => FindCargoRooms(s)); + + public static IEnumerable FindCargoRooms(Submarine sub) => WayPoint.WayPointList + .Where(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Cargo) + .Select(wp => wp.CurrentHull) + .Distinct(); + + public static IEnumerable FilterCargoCrates(IEnumerable items, Func conditional = null) + => items.Where(it => it.HasTag("crate") && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it))); + + public static IEnumerable FindReusableCargoContainers(IEnumerable subs, IEnumerable cargoRooms = null) => + FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull))) + .Select(it => it.GetComponent()) + .Where(c => c != null); + public static ItemContainer GetOrCreateCargoContainerFor(ItemPrefab item, ISpatialEntity cargoRoomOrSpawnPoint, ref List availableContainers) { ItemContainer itemContainer = null; @@ -553,8 +587,8 @@ namespace Barotrauma } #endif } - - List availableContainers = new List(); + var connectedSubs = sub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); + List availableContainers = FindReusableCargoContainers(connectedSubs, FindCargoRooms(connectedSubs)).ToList(); foreach (PurchasedItem pi in itemsToSpawn) { Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 06ab5566e..a397da922 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -248,11 +248,11 @@ namespace Barotrauma List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub).ToList(); - if (Level.IsLoadedOutpost && Submarine.Loaded.Any(s => s.Info.Type == SubmarineType.Outpost && (s.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false))) + if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost()) { - spawnWaypoints = WayPoint.WayPointList.FindAll(wp => + spawnWaypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && - wp.Submarine == Level.Loaded.StartOutpost && + wp.Submarine == Level.Loaded.StartOutpost && wp.CurrentHull != null && wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); while (spawnWaypoints.Count > characterInfos.Count) @@ -262,9 +262,8 @@ namespace Barotrauma while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count) { spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]); - } + } } - if (spawnWaypoints == null || !spawnWaypoints.Any()) { spawnWaypoints = mainSubWaypoints; @@ -290,6 +289,16 @@ namespace Barotrauma else if (!character.Info.StartItemsGiven) { character.GiveJobItems(mainSubWaypoints[i]); + foreach (Item item in character.Inventory.AllItems) + { + //if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in + //we don't want that in this case, the crew's cards shouldn't be submarine-specific + var idCard = item.GetComponent(); + if (idCard != null) + { + idCard.SubmarineSpecificID = 0; + } + } } if (character.Info.HealthData != null) { @@ -298,6 +307,7 @@ namespace Barotrauma character.LoadTalents(); + character.GiveIdCardTags(mainSubWaypoints[i]); character.GiveIdCardTags(spawnWaypoints[i]); character.Info.StartItemsGiven = true; if (character.Info.OrderData != null) @@ -410,20 +420,18 @@ namespace Barotrauma { List availableSpeakers = new List() { npc, player }; List dialogFlags = new List() { "OutpostNPC".ToIdentifier(), "EnterOutpost".ToIdentifier() }; + if (npc.HumanPrefab != null) + { + foreach (var tag in npc.HumanPrefab.GetTags()) + { + dialogFlags.Add(tag); + } + } if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) { if (campaignMode.Map?.CurrentLocation?.Type?.Identifier == "abandoned") { - if (npc.TeamID == CharacterTeamType.None) - { - dialogFlags.Remove("OutpostNPC".ToIdentifier()); - dialogFlags.Add("Bandit".ToIdentifier()); - } - else if (npc.TeamID == CharacterTeamType.FriendlyNPC) - { - dialogFlags.Remove("OutpostNPC".ToIdentifier()); - dialogFlags.Add("Hostage".ToIdentifier()); - } + dialogFlags.Remove("OutpostNPC".ToIdentifier()); } else if (campaignMode.Map?.CurrentLocation?.Reputation != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index 7f78f853d..004c007bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -8,19 +8,15 @@ namespace Barotrauma { internal partial class CampaignMetadata { - public CampaignMode Campaign { get; } - private readonly Dictionary data = new Dictionary(); - public CampaignMetadata(CampaignMode campaign) + public CampaignMetadata() { - Campaign = campaign; } - public CampaignMetadata(CampaignMode campaign, XElement element) + public void Load(XElement element) { - Campaign = campaign; - + data.Clear(); foreach (var subElement in element.Elements()) { if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase)) @@ -59,10 +55,11 @@ namespace Barotrauma { DebugConsole.Log($"Set the value \"{identifier}\" to {value}"); + SteamAchievementManager.OnCampaignMetadataSet(identifier, value, unlockClients: true); + if (!data.ContainsKey(identifier)) { data.Add(identifier, value); - SteamAchievementManager.OnCampaignMetadataSet(identifier, value); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index 32dc12d72..b08da08a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -1,13 +1,16 @@ #nullable enable using Microsoft.Xna.Framework; -using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { public enum FactionAffiliation { - Affiliated, - Neutral + Positive, + Neutral, + Negative } class Faction @@ -25,21 +28,25 @@ namespace Barotrauma /// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents /// /// - public FactionAffiliation GetPlayerAffiliationStatus() + public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction) { - float affiliation = 1f; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) - { - if (character.Info is not { } info) { continue; } + if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return FactionAffiliation.Neutral; } - affiliation *= 1f + info.GetSavedStatValue(StatTypes.Affiliation, Prefab.Identifier); + bool isHighest = true; + foreach (Faction otherFaction in factions) + { + if (otherFaction == faction || otherFaction.Reputation.Value < faction.Reputation.Value) { continue; } + + isHighest = false; + break; } - return affiliation switch - { - >= 1f => FactionAffiliation.Affiliated, - _ => FactionAffiliation.Neutral - }; + return isHighest ? FactionAffiliation.Positive : FactionAffiliation.Negative; + } + + public override string ToString() + { + return $"{base.ToString()} ({Prefab?.Identifier.ToString() ?? "null"})"; } } @@ -52,6 +59,54 @@ namespace Barotrauma public LocalizedString Description { get; } public LocalizedString ShortDescription { get; } + public class HireableCharacter + { + public readonly Identifier NPCSetIdentifier; + public readonly Identifier NPCIdentifier; + public readonly float MinReputation; + + public HireableCharacter(ContentXElement element) + { + NPCSetIdentifier = element.GetAttributeIdentifier("from", element.GetAttributeIdentifier("npcsetidentifier", Identifier.Empty)); + NPCIdentifier = element.GetAttributeIdentifier("identifier", element.GetAttributeIdentifier("npcidentifier", Identifier.Empty)); + MinReputation = element.GetAttributeFloat("minreputation", 0.0f); + } + } + + public ImmutableArray HireableCharacters; + + public class AutomaticMission + { + public readonly Identifier MissionTag; + public readonly LevelData.LevelType LevelType; + public readonly float MinReputation, MaxReputation; + public readonly float MinProbability, MaxProbability; + public readonly int MaxDistanceFromFactionOutpost; + public readonly bool DisallowBetweenOtherFactionOutposts; + + public AutomaticMission(ContentXElement element, string parentDebugName) + { + MissionTag = element.GetAttributeIdentifier("missiontag", Identifier.Empty); + LevelType = element.GetAttributeEnum("leveltype", LevelData.LevelType.LocationConnection); + MinReputation = element.GetAttributeFloat("minreputation", 0.0f); + MaxReputation = element.GetAttributeFloat("maxreputation", 0.0f); + if (MinReputation > MaxReputation) + { + DebugConsole.ThrowError($"Error in faction prefab \"{parentDebugName}\": MinReputation cannot be larger than MaxReputation."); + } + float probability = element.GetAttributeFloat("probability", 0.0f); + MinProbability = element.GetAttributeFloat("minprobability", probability); + MaxProbability = element.GetAttributeFloat("maxprobability", probability); + MaxDistanceFromFactionOutpost = element.GetAttributeInt(nameof(MaxDistanceFromFactionOutpost), int.MaxValue); + DisallowBetweenOtherFactionOutposts = element.GetAttributeBool(nameof(DisallowBetweenOtherFactionOutposts), false); + } + } + + public ImmutableArray AutomaticMissions; + + public bool StartOutpost { get; } + + public int MenuOrder { get; } /// @@ -69,38 +124,73 @@ namespace Barotrauma /// public int InitialReputation { get; } + public float ControlledOutpostPercentage { get; } + + public float SecondaryControlledOutpostPercentage { get; } + #if CLIENT public Sprite? Icon { get; private set; } + public Sprite? IconSmall { get; private set; } + public Sprite? BackgroundPortrait { get; private set; } +#endif public Color IconColor { get; } -#endif public FactionPrefab(ContentXElement element, FactionsFile file) : base(file, element.GetAttributeIdentifier("identifier", string.Empty)) { MenuOrder = element.GetAttributeInt("menuorder", 0); + StartOutpost = element.GetAttributeBool("startoutpost", false); MinReputation = element.GetAttributeInt("minreputation", -100); MaxReputation = element.GetAttributeInt("maxreputation", 100); InitialReputation = element.GetAttributeInt("initialreputation", 0); + ControlledOutpostPercentage = element.GetAttributeFloat("controlledoutpostpercentage", 0); + SecondaryControlledOutpostPercentage = element.GetAttributeFloat("secondarycontrolledoutpostpercentage", 0); Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}").Fallback("Unnamed"); Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description").Fallback(""); ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription").Fallback(""); -#if CLIENT + + List hireableCharacters = new List(); + List automaticMissions = new List(); foreach (var subElement in element.Elements()) { - - if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) + var subElementId = subElement.NameAsIdentifier(); + if (subElementId == "icon") { IconColor = subElement.GetAttributeColor("color", Color.White); +#if CLIENT Icon = new Sprite(subElement); +#endif } - else if (subElement.Name.ToString().Equals("portrait", StringComparison.OrdinalIgnoreCase)) + else if (subElementId == "iconsmall") { +#if CLIENT + IconSmall = new Sprite(subElement); +#endif + } + else if (subElementId == "portrait") + { +#if CLIENT BackgroundPortrait = new Sprite(subElement); +#endif + } + else if (subElementId == "hireable") + { + hireableCharacters.Add(new HireableCharacter(subElement)); + } + else if (subElementId == "mission" || subElementId == "automaticmission") + { + automaticMissions.Add(new AutomaticMission(subElement, Identifier.ToString())); } } -#endif + HireableCharacters = hireableCharacters.ToImmutableArray(); + AutomaticMissions = automaticMissions.ToImmutableArray(); + } + + public override string ToString() + { + return $"{base.ToString()} ({Identifier})"; } public override void Dispose() diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index ed89cf0c7..7205df632 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -1,17 +1,25 @@ using Microsoft.Xna.Framework; using System; -using System.Linq; namespace Barotrauma { class Reputation { public const float HostileThreshold = 0.2f; - public const float ReputationLossPerNPCDamage = 0.1f; - public const float ReputationLossPerStolenItemPrice = 0.01f; - public const float ReputationLossPerWallDamage = 0.1f; - public const float MinReputationLossPerStolenItem = 0.5f; - public const float MaxReputationLossPerStolenItem = 10.0f; + public const float ReputationLossPerNPCDamage = 0.025f; + public const float ReputationLossPerWallDamage = 0.025f; + public const float ReputationLossPerStolenItemPrice = 0.0025f; + public const float MinReputationLossPerStolenItem = 0.025f; + public const float MaxReputationLossPerStolenItem = 0.5f; + + /// + /// Maximum amount of reputation loss you can get from damaging outpost NPCs per round + /// + public const float MaxReputationLossFromNPCDamage = 10.0f; + /// + /// Maximum amount of reputation loss you can get from damaging outpost walls per round + /// + public const float MaxReputationLossFromWallDamage = 10.0f; public Identifier Identifier { get; } public int MinReputation { get; } @@ -19,6 +27,8 @@ namespace Barotrauma public int InitialReputation { get; } public CampaignMetadata Metadata { get; } + public float ReputationAtRoundStart { get; set; } + private readonly Identifier metaDataIdentifier; /// @@ -59,27 +69,47 @@ namespace Barotrauma Value = newReputation; } - public void AddReputation(float reputationChange) + public float GetReputationChangeMultiplier(float reputationChange) { if (reputationChange > 0f) { float reputationGainMultiplier = 1f; foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - reputationGainMultiplier += character.GetStatValue(StatTypes.ReputationGainMultiplier); + reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier, includeSaved: false); + reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0; } - reputationChange *= reputationGainMultiplier; + return reputationGainMultiplier; } else if (reputationChange < 0f) { float reputationLossMultiplier = 1f; foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - reputationLossMultiplier += character.GetStatValue(StatTypes.ReputationLossMultiplier); + reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier, includeSaved: false); + reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0; } - reputationChange *= reputationLossMultiplier; + return reputationLossMultiplier; } - Value += reputationChange; + return 1.0f; + } + + public void AddReputation(float reputationChange, float maxReputationChangePerRound = float.MaxValue) + { + float currentValue = Value; + float currentReputationChange = currentValue - ReputationAtRoundStart; + if (Math.Abs(currentReputationChange) >= maxReputationChangePerRound && + Math.Sign(currentReputationChange) == Math.Sign(reputationChange)) + { + return; + } + float newValue = Value + reputationChange * GetReputationChangeMultiplier(reputationChange); + if (Math.Abs(newValue - ReputationAtRoundStart) > maxReputationChangePerRound && + Math.Sign(newValue - currentValue) == Math.Sign(newValue - ReputationAtRoundStart)) + { + newValue = ReputationAtRoundStart + maxReputationChangePerRound * Math.Sign(reputationChange); + } + Value = newValue; } public readonly NamedEvent OnReputationValueChanged = new NamedEvent(); @@ -108,6 +138,7 @@ namespace Barotrauma metaDataIdentifier = $"reputation.{Identifier}".ToIdentifier(); MinReputation = minReputation; MaxReputation = maxReputation; + ReputationAtRoundStart = initialReputation; InitialReputation = initialReputation; Faction = faction; Location = location; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs index 795cb4680..98a2ebc39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -69,7 +69,7 @@ namespace Barotrauma public Option RewardDistributionChanged; public Option BalanceChanged; - public WalletChangedData MergeInto(WalletChangedData other) + public readonly WalletChangedData MergeInto(WalletChangedData other) { other.BalanceChanged = AddOptionalInt(other.BalanceChanged, BalanceChanged); other.RewardDistributionChanged = AddOptionalInt(other.RewardDistributionChanged, RewardDistributionChanged); @@ -80,32 +80,20 @@ namespace Barotrauma static Option AddOptionalInt(Option a, Option b) { - return a switch - { - Some some1 => b switch - { - Some some2 => Option.Some(some1.Value + some2.Value), - None _ => Option.Some(some1.Value), - _ => throw new ArgumentOutOfRangeException(nameof(b)) - }, - None _ => b switch - { - Some some1 => Option.Some(some1.Value), - None _ => Option.None(), - _ => throw new ArgumentOutOfRangeException(nameof(b)) - }, - _ => throw new ArgumentOutOfRangeException(nameof(a)) - }; + bool hasValue1 = a.TryUnwrap(out var value1); + bool hasValue2 = b.TryUnwrap(out var value2); + return hasValue1 + ? hasValue2 + ? Option.Some(value1 + value2) + : Option.Some(value1) + : hasValue2 + ? Option.Some(value2) + : Option.None; } static Option TurnToNoneIfZero(Option option) { - return option switch - { - Some s => s.Value == 0 ? Option.None() : option, - None _ => option, - _ => throw new ArgumentOutOfRangeException(nameof(option)) - }; + return option.Bind(i => i == 0 ? Option.None : Option.Some(i)); } } } @@ -223,12 +211,8 @@ namespace Barotrauma }; } - public string GetOwnerLogName() => Owner switch - { - Some { Value: var character } => character.Name, - None _ => "the bank", - _ => throw new ArgumentOutOfRangeException(nameof(Owner)) - }; + public string GetOwnerLogName() + => Owner.TryUnwrap(out var character) ? character.Name : "the bank"; partial void SettingsChanged(Option balanceChanged, Option rewardChanged); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index ddfcd55e0..81b26d81f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -23,8 +23,6 @@ namespace Barotrauma public const int MaxMoney = int.MaxValue / 2; //about 1 billion public const int InitialMoney = 8500; - //duration of the cinematic + credits at the end of the campaign - protected const float EndCinematicDuration = 240.0f; //duration of the camera transition at the end of a round protected const float EndTransitionDuration = 5.0f; //there can be no events before this time has passed during the 1st campaign round @@ -44,9 +42,10 @@ namespace Barotrauma public UpgradeManager UpgradeManager; public MedicalClinic MedicalClinic; - public List Factions; + private List factions; + public IReadOnlyList Factions => factions; - public CampaignMetadata CampaignMetadata; + public readonly CampaignMetadata CampaignMetadata; protected XElement petsElement; @@ -84,9 +83,9 @@ namespace Barotrauma public bool CheatsEnabled; - public const float HullRepairCostPerDamage = 0.5f, ItemRepairCostPerRepairDuration = 1.0f; + public const float HullRepairCostPerDamage = 0.1f, ItemRepairCostPerRepairDuration = 1.0f; public const int ShuttleReplaceCost = 1000; - public const int MaxHullRepairCost = 2000, MaxItemRepairCost = 2000; + public const int MaxHullRepairCost = 600, MaxItemRepairCost = 2000; protected bool wasDocked; @@ -96,6 +95,8 @@ namespace Barotrauma public SubmarineInfo PendingSubmarineSwitch; public bool TransferItemsOnSubSwitch { get; set; } + public bool SwitchedSubsThisRound { get; private set; } + protected Map map; public Map Map { @@ -106,20 +107,24 @@ namespace Barotrauma { get { - if (Map.CurrentLocation != null) + //map can be null if we're in the process of loading the save + if (Map != null) { - foreach (Mission mission in map.CurrentLocation.SelectedMissions) + if (Map.CurrentLocation != null) { - if (mission.Locations[0] == mission.Locations[1] || - mission.Locations.Contains(Map.SelectedLocation)) + foreach (Mission mission in map.CurrentLocation.SelectedMissions) { - yield return mission; + if (mission.Locations[0] == mission.Locations[1] || + mission.Locations.Contains(Map.SelectedLocation)) + { + yield return mission; + } } } - } - foreach (Mission mission in extraMissions) - { - yield return mission; + foreach (Mission mission in extraMissions) + { + yield return mission; + } } } } @@ -141,10 +146,19 @@ namespace Barotrauma private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions) { if (GameMain.NetworkMember == null) { return true; } - //allow managing if no-one with permissions is alive - return - GameMain.NetworkMember.ConnectedClients.Count == 1 || - GameMain.NetworkMember.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (IsOwner(c) || c.HasPermission(permissions))); + if (GameMain.NetworkMember.ConnectedClients.Count == 1) { return true; } + + if (GameMain.NetworkMember.GameStarted) + { + //allow managing if no-one with permissions is alive and in-game + return GameMain.NetworkMember.ConnectedClients.None(c => + c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && + (IsOwner(c) || c.HasPermission(permissions))); + } + else + { + return GameMain.NetworkMember.ConnectedClients.None(c => IsOwner(c) || c.HasPermission(permissions)); + } } protected CampaignMode(GameModePreset preset, CampaignSettings settings) @@ -157,26 +171,26 @@ namespace Barotrauma CargoManager = new CargoManager(this); MedicalClinic = new MedicalClinic(this); + CampaignMetadata = new CampaignMetadata(); Identifier messageIdentifier = new Identifier("money"); #if CLIENT OnMoneyChanged.RegisterOverwriteExisting(new Identifier("CampaignMoneyChangeNotification"), e => { - if (!(e.ChangedData.BalanceChanged is Some { Value: var changed })) { return; } + if (!e.ChangedData.BalanceChanged.TryUnwrap(out var changed)) { return; } if (changed == 0) { return; } bool isGain = changed > 0; Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red; - switch (e.Owner) + if (e.Owner.TryUnwrap(out var owner)) { - case Some { Value: var owner}: - owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed); - break; - case None _ when IsSinglePlayer: - Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed); - break; + owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed); + } + else if (IsSinglePlayer) + { + Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed); } string FormatMessage() => TextManager.GetWithVariable(isGain ? "moneygainformat" : "moneyloseformat", "[money]", TextManager.FormatCurrency(Math.Abs(changed))).ToString(); @@ -239,6 +253,11 @@ namespace Barotrauma prevCampaignUIAutoOpenType = TransitionType.None; #endif + foreach (var faction in factions) + { + faction.Reputation.ReputationAtRoundStart = faction.Reputation.Value; + } + if (PurchasedHullRepairsInLatestSave) { foreach (Structure wall in Structure.WallList) @@ -272,6 +291,7 @@ namespace Barotrauma PurchasedLostShuttlesInLatestSave = PurchasedLostShuttles = false; var connectedSubs = Submarine.MainSub.GetConnectedSubs(); wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); + SwitchedSubsThisRound = false; } public static int GetHullRepairCost() @@ -307,12 +327,12 @@ namespace Barotrauma return (int)Math.Min(totalRepairDuration * ItemRepairCostPerRepairDuration, MaxItemRepairCost); } - public void InitCampaignData() + public void InitFactions() { - Factions = new List(); + factions = new List(); foreach (FactionPrefab factionPrefab in FactionPrefab.Prefabs) { - Factions.Add(new Faction(CampaignMetadata, factionPrefab)); + factions.Add(new Faction(CampaignMetadata, factionPrefab)); } } @@ -358,10 +378,9 @@ namespace Barotrauma currentLocation.DeselectMission(mission); } } - if (levelData.HasBeaconStation && !levelData.IsBeaconActive) { - var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase))).OrderBy(m => m.UintIdentifier); + var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("beaconnoreward")).OrderBy(m => m.UintIdentifier); if (beaconMissionPrefabs.Any()) { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); @@ -374,7 +393,7 @@ namespace Barotrauma } if (levelData.HasHuntingGrounds) { - var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))).OrderBy(m => m.UintIdentifier); + var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("huntinggrounds")).OrderBy(m => m.UintIdentifier); if (!huntingGroundsMissionPrefabs.Any()) { DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found."); @@ -400,15 +419,108 @@ namespace Barotrauma weights[i] = weight; } var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(prefabs, weights, rand); - if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))) + if (!Missions.Any(m => m.Prefab.Tags.Contains("huntinggrounds"))) { extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); } } } + foreach (Faction faction in factions.OrderBy(f => f.Prefab.MenuOrder)) + { + foreach (var automaticMission in faction.Prefab.AutomaticMissions) + { + if (faction.Reputation.Value < automaticMission.MinReputation || faction.Reputation.Value > automaticMission.MaxReputation) { continue; } + + if (automaticMission.DisallowBetweenOtherFactionOutposts && levelData.Type == LevelData.LevelType.LocationConnection) + { + if (Map.SelectedConnection.Locations.All(l => l.Faction != null && l.Faction != faction)) + { + continue; + } + } + if (automaticMission.MaxDistanceFromFactionOutpost < int.MaxValue) + { + if (!Map.LocationOrConnectionWithinDistance( + currentLocation, + automaticMission.MaxDistanceFromFactionOutpost, + loc => loc.Faction == faction)) + { + continue; + } + } + Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed + TotalPassedLevels)); + if (levelData.Type != automaticMission.LevelType) { continue; } + float probability = + MathHelper.Lerp( + automaticMission.MinProbability, + automaticMission.MaxProbability, + MathUtils.InverseLerp(automaticMission.MinReputation, automaticMission.MaxReputation, faction.Reputation.Value)); + if (rand.NextDouble() < probability) + { + var missionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t == automaticMission.MissionTag)).OrderBy(m => m.UintIdentifier); + if (missionPrefabs.Any()) + { + var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => (float)p.Commonness, rand); + if (missionPrefab.Type == MissionType.Pirate && Missions.Any(m => m.Prefab.Type == MissionType.Pirate)) + { + continue; + } + if (automaticMission.LevelType == LevelData.LevelType.Outpost) + { + extraMissions.Add(missionPrefab.Instantiate(new Location[] { currentLocation, currentLocation }, Submarine.MainSub)); + } + else + { + extraMissions.Add(missionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); + } + } + } + } + } + } + if (levelData.Biome.IsEndBiome) + { + Identifier endMissionTag = Identifier.Empty; + if (levelData.Type == LevelData.LevelType.LocationConnection) + { + int locationIndex = map.EndLocations.IndexOf(map.SelectedLocation); + if (locationIndex > -1) + { + endMissionTag = ("endlevel_locationconnection_" + locationIndex).ToIdentifier(); + } + } + else + { + int locationIndex = map.EndLocations.IndexOf(map.CurrentLocation); + if (locationIndex > -1) + { + endMissionTag = ("endlevel_location_" + locationIndex).ToIdentifier(); + } + } + if (!endMissionTag.IsEmpty) + { + var endLevelMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains(endMissionTag)).OrderBy(m => m.UintIdentifier); + if (endLevelMissionPrefabs.Any()) + { + Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); + var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => (float)p.Commonness, rand); + if (!Missions.Any(m => m.Prefab.Type == endLevelMissionPrefab.Type)) + { + if (levelData.Type == LevelData.LevelType.LocationConnection) + { + extraMissions.Add(endLevelMissionPrefab.Instantiate(map.SelectedConnection.Locations, Submarine.MainSub)); + } + else + { + extraMissions.Add(endLevelMissionPrefab.Instantiate(new Location[] { map.CurrentLocation, map.CurrentLocation }, Submarine.MainSub)); + } + } + } + } } } + public void LoadNewLevel() { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) @@ -503,13 +615,6 @@ namespace Barotrauma { if (leavingSub.AtEndExit) { - if (Map.EndLocation != null && - map.SelectedLocation == Map.EndLocation && - Map.EndLocation.Connections.Any(c => c.LevelData == Level.Loaded.LevelData)) - { - nextLevel = map.StartLocation.LevelData; - return TransitionType.End; - } if (Level.Loaded.EndLocation != null && Level.Loaded.EndLocation.Type.HasOutpost && Level.Loaded.EndOutpost != null) { nextLevel = Level.Loaded.EndLocation.LevelData; @@ -554,8 +659,32 @@ namespace Barotrauma } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) { - nextLevel = map.SelectedLocation == null ? null : map.SelectedConnection?.LevelData; - return nextLevel == null ? TransitionType.None : TransitionType.LeaveLocation; + int currentEndLocationIndex = map.EndLocations.IndexOf(map.CurrentLocation); + if (currentEndLocationIndex > -1) + { + if (currentEndLocationIndex == map.EndLocations.Count - 1) + { + //at the last end location, end of campaign + nextLevel = map.StartLocation?.LevelData; + return TransitionType.End; + } + else if (leavingSub.AtEndExit && currentEndLocationIndex < map.EndLocations.Count - 1) + { + //more end locations to go, progress to the next one + nextLevel = map.EndLocations[currentEndLocationIndex + 1]?.LevelData; + return TransitionType.ProgressToNextLocation; + } + else + { + nextLevel = null; + return TransitionType.None; + } + } + else + { + nextLevel = map.SelectedLocation == null ? null : map.SelectedConnection?.LevelData; + return nextLevel == null ? TransitionType.None : TransitionType.LeaveLocation; + } } else { @@ -579,9 +708,11 @@ namespace Barotrauma //TODO: ignore players who don't have the permission to trigger a transition between levels? var leavingPlayers = Character.CharacterList.Where(c => !c.IsDead && (c == Character.Controlled || c.IsRemotePlayer)); + CharacterTeamType submarineTeam = leavingPlayers.FirstOrDefault()?.TeamID ?? CharacterTeamType.Team1; + //allow leaving if inside an outpost, and the submarine is either docked to it or close enough - Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers); - Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers); + Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers, submarineTeam); + Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers, submarineTeam); int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 : leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost)); @@ -595,11 +726,11 @@ namespace Barotrauma return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd; - static Submarine GetLeavingSubAtStart(IEnumerable leavingPlayers) + static Submarine GetLeavingSubAtStart(IEnumerable leavingPlayers, CharacterTeamType submarineTeam) { if (Level.Loaded.StartOutpost == null) { - Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -609,26 +740,35 @@ namespace Barotrauma if (Level.Loaded.StartOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; } - Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null || !closestSub.AtStartExit) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } } - static Submarine GetLeavingSubAtEnd(IEnumerable leavingPlayers) + static Submarine GetLeavingSubAtEnd(IEnumerable leavingPlayers, CharacterTeamType submarineTeam) { + if (Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.ExitPoints.Any()) + { + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); + if (closestSub == null || !closestSub.AtEndExit) { return null; } + return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; + } //no "end" in outpost levels - if (Level.Loaded.Type == LevelData.LevelType.Outpost) { return null; } + if (Level.Loaded.Type == LevelData.LevelType.Outpost) + { + return null; + } if (Level.Loaded.EndOutpost == null) { - Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -638,13 +778,13 @@ namespace Barotrauma if (Level.Loaded.EndOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; } - Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null || !closestSub.AtEndExit) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -765,8 +905,8 @@ namespace Barotrauma } foreach (Location location in Map.Locations) { - location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty); - location.Reset(); + location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty); + location.Reset(this); } Map.ClearLocationHistory(); Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); @@ -779,6 +919,10 @@ namespace Barotrauma { location.TurnsInRadiation = 0; } + foreach (var faction in Factions) + { + faction.Reputation.SetReputation(faction.Prefab.InitialReputation); + } EndCampaignProjSpecific(); if (CampaignMetadata != null) @@ -800,11 +944,56 @@ namespace Barotrauma protected virtual void EndCampaignProjSpecific() { } + /// + /// Returns a random faction based on their ControlledOutpostPercentage + /// + /// If true, the method can return null if the sum of the factions ControlledOutpostPercentage is less than 100% + public Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty = true) + { + return GetRandomFaction(Factions, randSync, secondary: false, allowEmpty); + } + + /// + /// Returns a random faction based on their SecondaryControlledOutpostPercentage + /// + /// If true, the method can return null if the sum of the factions SecondaryControlledOutpostPercentage is less than 100% + public Faction GetRandomSecondaryFaction(Rand.RandSync randSync, bool allowEmpty = true) + { + return GetRandomFaction(Factions, randSync, secondary: true, allowEmpty); + } + + public static Faction GetRandomFaction(IEnumerable factions, Rand.RandSync randSync, bool secondary = false, bool allowEmpty = true) + { + return GetRandomFaction(factions, Rand.GetRNG(randSync), secondary, allowEmpty); + } + + public static Faction GetRandomFaction(IEnumerable factions, Random random, bool secondary = false, bool allowEmpty = true) + { + List factionsList = factions.OrderBy(f => f.Prefab.Identifier).ToList(); + List weights = factionsList.Select(f => secondary ? f.Prefab.SecondaryControlledOutpostPercentage : f.Prefab.ControlledOutpostPercentage).ToList(); + float percentageSum = weights.Sum(); + if (percentageSum < 100.0f && allowEmpty) + { + //chance of non-faction-specific outposts if percentage of controlled outposts is <100 + factionsList.Add(null); + weights.Add(100.0f - percentageSum); + } + return ToolBox.SelectWeightedRandom(factionsList, weights, random); + } + public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null) { if (characterInfo == null) { return false; } + if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) + { + if (GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation) + { + return false; + } + } if (!TryPurchase(client, characterInfo.Salary)) { return false; } characterInfo.IsNewHire = true; + characterInfo.Title = null; location.RemoveHireableCharacter(characterInfo); CrewManager.AddCharacterInfo(characterInfo); GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown"); @@ -859,11 +1048,14 @@ namespace Barotrauma public void AssignNPCMenuInteraction(Character character, InteractionType interactionType) { character.CampaignInteractionType = interactionType; + if (character.CampaignInteractionType == InteractionType.Store && character.HumanPrefab is { Identifier: var merchantId }) { character.MerchantIdentifier = merchantId; + map.CurrentLocation?.GetStore(merchantId)?.SetMerchantFaction(character.Faction); } + character.DisableHealthWindow = interactionType != InteractionType.None && interactionType != InteractionType.Examine && @@ -975,11 +1167,36 @@ namespace Barotrauma if (npc == null || attacker == null || npc.IsDead || npc.IsInstigator) { return; } if (npc.TeamID != CharacterTeamType.FriendlyNPC) { return; } if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; } - Location location = Map?.CurrentLocation; - if (location != null) + + if (npc.Faction != null && Factions.FirstOrDefault(f => f.Prefab.Identifier == npc.Faction) is Faction faction) { - location.Reputation.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage); + faction.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage, Reputation.MaxReputationLossFromNPCDamage); } + else + { + Location location = Map?.CurrentLocation; + location?.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage, Reputation.MaxReputationLossFromNPCDamage); + } + } + + public Faction GetFaction(Identifier identifier) + { + return factions.Find(f => f.Prefab.Identifier == identifier); + } + + public float GetReputation(Identifier factionIdentifier) + { + var faction = + factionIdentifier == "location".ToIdentifier() ? + factions.Find(f => f == Map?.CurrentLocation?.Faction) : + factions.Find(f => f.Prefab.Identifier == factionIdentifier); + return faction?.Reputation?.Value ?? 0.0f; + } + + public FactionAffiliation GetFactionAffiliation(Identifier factionIdentifier) + { + var faction = GetFaction(factionIdentifier); + return Faction.GetPlayerAffiliationStatus(faction); } public abstract void Save(XElement element); @@ -996,7 +1213,20 @@ namespace Barotrauma new XAttribute(nameof(TotalPlayTime).ToLowerInvariant(), TotalPlayTime), new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels)); } - + + protected void LoadEvents(XElement element) + { + TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0); + TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0); + } + + protected XElement SaveEvents() + { + return new XElement("events", + new XAttribute(nameof(EventManager.QueuedEventsForNextRound).ToLowerInvariant(), + string.Join(',', GameMain.GameSession.EventManager.QueuedEventsForNextRound))); + } + public void LogState() { DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White); @@ -1080,6 +1310,7 @@ namespace Barotrauma TransferItemsBetweenSubs(); } RefreshOwnedSubmarines(); + SwitchedSubsThisRound = true; PendingSubmarineSwitch = null; } @@ -1097,7 +1328,7 @@ namespace Barotrauma var itemsToTransfer = new List<(Item item, Item container)>(); if (PendingSubmarineSwitch != null) { - var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); + var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); // Remove items from the old sub foreach (Item item in Item.ItemList) { @@ -1132,15 +1363,29 @@ namespace Barotrauma { // Load the new sub var newSub = new Submarine(PendingSubmarineSwitch); - var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); - // Move the transferred items - List availableContainers = Item.ItemList - .Where(it => connectedSubs.Contains(it.Submarine) && it.HasTag("crate") && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed) - .Select(it => it.GetComponent()) - .Where(c => c != null) - .ToList(); + var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); + WayPoint wp = WayPoint.WayPointList.FirstOrDefault(wp => wp.SpawnType == SpawnType.Cargo && connectedSubs.Contains(wp.Submarine)); + Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.FirstOrDefault(h => connectedSubs.Contains(h.Submarine) && !h.IsWetRoom); + if (spawnHull == null) + { + DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub."); + return; + } + // First move the cargo containers, so that we can reuse them + var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag("crate")); + foreach (var (item, oldContainer) in cargoContainers) + { + Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); + item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); + item.CurrentHull = spawnHull; + item.Submarine = spawnHull.Submarine; + } + // Then move the other items + var cargoRooms = CargoManager.FindCargoRooms(newSub); + List availableContainers = CargoManager.FindReusableCargoContainers(connectedSubs).ToList(); foreach (var (item, oldContainer) in itemsToTransfer) { + if (cargoContainers.Contains((item, oldContainer))) { continue; } Item newContainer = null; item.Submarine = newSub; if (item.Container == null) @@ -1149,25 +1394,16 @@ namespace Barotrauma } if (item.Container == null && (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null, createNetworkEvent: false))) { - WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, newSub); - Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.Where(h => h.Submarine == newSub && !h.IsWetRoom).GetRandomUnsynced(); - if (spawnHull == null) + var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); + if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) { - DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub."); - return; + Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); + item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); } - if (spawnHull != null) + else if (cargoContainer.Item.Submarine is Submarine containerSub) { - var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); - if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) - { - Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); - item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); - } - } - else - { - DebugConsole.AddWarning($"Failed to transfer item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!"); + // Use the item's sub in case the sub consists of multiple linked subs. + item.Submarine = containerSub; } } string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 2377b386c..31aa8aa9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -11,11 +11,14 @@ namespace Barotrauma { public static CampaignSettings Empty => new CampaignSettings(element: null); +#if CLIENT + public static CampaignSettings CurrentSettings = new CampaignSettings(GameSettings.CurrentConfig.SavedCampaignSettings); +#endif public string Name => "CampaignSettings"; public const string LowerCaseSaveElementName = "campaignsettings"; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("Normal", IsPropertySaveable.Yes)] public string PresetName { get; set; } = string.Empty; [Serialize(true, IsPropertySaveable.Yes)] @@ -53,7 +56,6 @@ namespace Barotrauma return definition.GetInt(StartingBalanceAmount.ToIdentifier()); } return 8000; - } } @@ -65,7 +67,7 @@ namespace Barotrauma { return definition.GetFloat(Difficulty.ToIdentifier()); } - return 0; + return 0; } } @@ -82,7 +84,7 @@ namespace Barotrauma } public const int DefaultMaxMissionCount = 2; - public const int MaxMissionCountLimit = 3; + public const int MaxMissionCountLimit = 10; public const int MinMissionCountLimit = 1; public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index a84b1a7c5..2f5d725da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1,4 +1,5 @@ -using Barotrauma.IO; +using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -53,7 +54,7 @@ namespace Barotrauma } } - private bool ValidateFlag(NetFlags flag) + private static bool ValidateFlag(NetFlags flag) { if (MathHelper.IsPowerOfTwo((int)flag)) { return true; } #if DEBUG @@ -105,9 +106,8 @@ namespace Barotrauma #endif } CampaignID = currentCampaignID; - CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); - InitCampaignData(); + InitFactions(); } public static MultiPlayerCampaign StartNew(string mapSeed, CampaignSettings settings) @@ -190,11 +190,20 @@ namespace Barotrauma //map already created, update it //if we're not downloading the initial save file (LastSaveID > 0), //show notifications about location type changes - map.LoadState(subElement, LastSaveID > 0); + map.LoadState(this, subElement, LastSaveID > 0); } break; case "metadata": - CampaignMetadata = new CampaignMetadata(this, subElement); + var prevReputations = Factions.ToDictionary(k => k, v => v.Reputation.Value); + CampaignMetadata.Load(subElement); + foreach (var faction in Factions) + { + if (!MathUtils.NearlyEqual(prevReputations[faction], faction.Reputation.Value)) + { + faction.Reputation.OnReputationValueChanged?.Invoke(faction.Reputation); + Reputation.OnAnyReputationValueChanged.Invoke(faction.Reputation); + } + } break; case "upgrademanager": case "pendingupgrades": @@ -214,6 +223,9 @@ namespace Barotrauma case "stats": LoadStats(subElement); break; + case "eventmanager": + GameMain.GameSession.EventManager.Load(subElement); + break; case Wallet.LowerCaseSaveElementName: Bank = new Wallet(Option.None(), subElement); break; @@ -237,10 +249,8 @@ namespace Barotrauma }; } - CampaignMetadata ??= new CampaignMetadata(this); UpgradeManager ??= new UpgradeManager(this); - InitCampaignData(); #if SERVER characterData.Clear(); string characterDataPath = GetCharacterDataSavePath(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index d902e245a..e376e4e22 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -9,6 +9,7 @@ using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Barotrauma.Networking; +using Barotrauma.Extensions; namespace Barotrauma { @@ -75,7 +76,10 @@ namespace Barotrauma get { if (Map != null) { return Map.CurrentLocation; } - if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } + if (dummyLocations == null) + { + dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData); + } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[0]; } @@ -86,7 +90,10 @@ namespace Barotrauma get { if (Map != null) { return Map.SelectedLocation; } - if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } + if (dummyLocations == null) + { + dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData); + } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[1]; } @@ -248,13 +255,44 @@ namespace Barotrauma } } + public static Location[] CreateDummyLocations(LevelData levelData, LocationType? forceLocationType = null) + { + MTRandom rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); + var forceParams = levelData?.ForceOutpostGenerationParams; + if (forceLocationType == null && + forceParams != null && forceParams.AllowedLocationTypes.Any() && !forceParams.AllowedLocationTypes.Contains("Any".ToIdentifier())) + { + forceLocationType = + LocationType.Prefabs.Where(lt => forceParams.AllowedLocationTypes.Contains(lt.Identifier)).GetRandom(rand); + } + var dummyLocations = CreateDummyLocations(rand, forceLocationType); + List factions = new List(); + foreach (var factionPrefab in FactionPrefab.Prefabs) + { + factions.Add(new Faction(new CampaignMetadata(), factionPrefab)); + } + foreach (var location in dummyLocations) + { + if (location.Type.HasOutpost) + { + location.Faction = CampaignMode.GetRandomFaction(factions, rand, secondary: false); + location.SecondaryFaction = CampaignMode.GetRandomFaction(factions, rand, secondary: true); + } + } + return dummyLocations; + } + public static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType = null) + { + return CreateDummyLocations(new MTRandom(ToolBox.StringToInt(seed)), forceLocationType); + } + + private static Location[] CreateDummyLocations(Random rand, LocationType? forceLocationType = null) { var dummyLocations = new Location[2]; - MTRandom rand = new MTRandom(ToolBox.StringToInt(seed)); for (int i = 0; i < 2; i++) { - dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType); + dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType); } return dummyLocations; } @@ -268,7 +306,7 @@ namespace Barotrauma /// /// Switch to another submarine. The sub is loaded when the next round starts. /// - public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, int cost, Client? client = null) + public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client = null) { if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { @@ -286,11 +324,6 @@ namespace Barotrauma } } } - if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0) - { - Campaign!.TryPurchase(client, cost); - } - GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); Campaign!.PendingSubmarineSwitch = newSubmarine; Campaign!.TransferItemsOnSubSwitch = transferItems; } @@ -298,10 +331,11 @@ namespace Barotrauma public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) { if (Campaign is null) { return; } - if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, newSubmarine.Price)) { return; } + int price = newSubmarine.GetPrice(); + if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return; } if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { - GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); + GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); OwnedSubmarines.Add(newSubmarine); #if SERVER (Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList); @@ -395,6 +429,7 @@ namespace Barotrauma } } + GameMode!.AddExtraMissions(LevelData); foreach (Mission mission in GameMode!.Missions) { // setting level for missions that may involve difficulty-related submarine creation @@ -505,6 +540,8 @@ namespace Barotrauma existingRoundSummary.ContinueButton.Visible = true; } + CharacterHUD.ClearBossProgressBars(); + RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation); if (GameMode is not TutorialMode && GameMode is not TestGameMode) @@ -514,14 +551,15 @@ namespace Barotrauma { GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false); - if (missions.Count > 1) + var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage); + if (missionsToShow.Count() > 1) { string joinedMissionNames = string.Join(", ", missions.Select(m => m.Name)); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false); } else { - var mission = missions.FirstOrDefault(); + var mission = missionsToShow.FirstOrDefault(); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), mission?.Name ?? TextManager.Get("None")), Color.CadetBlue, playSound: false); } } @@ -568,7 +606,6 @@ namespace Barotrauma if (GameMode != null && Submarine != null) { missions.Clear(); - GameMode.AddExtraMissions(LevelData); missions.AddRange(GameMode.Missions); GameMode.Start(); foreach (Mission mission in missions) @@ -611,7 +648,7 @@ namespace Barotrauma } } - CreatureMetrics.Instance.RecentlyEncountered.Clear(); + CreatureMetrics.RecentlyEncountered.Clear(); GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition; RoundDuration = 0.0f; @@ -627,7 +664,16 @@ namespace Barotrauma return; } - if (level.StartOutpost != null) + var originalSubPos = Submarine.WorldPosition; + var spawnPoint = WayPoint.WayPointList.Find(wp => wp.SpawnType.HasFlag(SpawnType.Submarine) && wp.Submarine == level.StartOutpost); + if (spawnPoint != null) + { + //pre-determine spawnpoint, just use it directly + Submarine.SetPosition(spawnPoint.WorldPosition); + Submarine.NeutralizeBallast(); + Submarine.EnableMaintainPosition(); + } + else if (level.StartOutpost != null) { //start by placing the sub below the outpost Rectangle outpostBorders = Level.Loaded.StartOutpost.GetDockedBorders(); @@ -682,7 +728,7 @@ namespace Barotrauma else { Submarine.SetPosition(spawnPos - Vector2.UnitY * 100.0f); - Submarine.NeutralizeBallast(); + Submarine.NeutralizeBallast(); Submarine.EnableMaintainPosition(); } } @@ -691,6 +737,7 @@ namespace Barotrauma Submarine.NeutralizeBallast(); Submarine.EnableMaintainPosition(); } + } else { @@ -841,7 +888,7 @@ namespace Barotrauma GUI.PreventPauseMenuToggle = true; - if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null) + if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null && transitionType != CampaignMode.TransitionType.End) { GUI.ClearMessages(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); @@ -854,6 +901,7 @@ namespace Barotrauma TabMenu.OnRoundEnded(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); ObjectiveManager.ResetUI(); + CharacterHUD.ClearBossProgressBars(); #endif SteamAchievementManager.OnRoundEnded(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs index 43baec7c3..9e76c344f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs @@ -8,7 +8,7 @@ namespace Barotrauma public List AvailableCharacters { get; set; } public List PendingHires = new List(); - public const int MaxAvailableCharacters = 10; + public const int MaxAvailableCharacters = 6; public HireManager() { @@ -32,6 +32,24 @@ namespace Barotrauma var variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient); AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant)); } + if (location.Faction != null) { GenerateFactionCharacters(location.Faction.Prefab); } + if (location.SecondaryFaction != null) { GenerateFactionCharacters(location.SecondaryFaction.Prefab); } + } + + private void GenerateFactionCharacters(FactionPrefab faction) + { + foreach (var character in faction.HireableCharacters) + { + HumanPrefab humanPrefab = NPCSet.Get(character.NPCSetIdentifier, character.NPCIdentifier); + if (humanPrefab == null) + { + DebugConsole.ThrowError($"Couldn't create a hireable for the location: character prefab \"{character.NPCIdentifier}\" not found in the NPC set \"{character.NPCSetIdentifier}\"."); + continue; + } + var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient); + characterInfo.MinReputationToHire = (faction.Identifier, character.MinReputation); + AvailableCharacters.Add(characterInfo); + } } public void Remove() diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 2b40e6304..4d0a5186a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -14,6 +14,8 @@ namespace Barotrauma public enum NetworkHeader { REQUEST_AFFLICTIONS, + AFFLICTION_UPDATE, + UNSUBSCRIBE_ME, REQUEST_PENDING, ADD_PENDING, REMOVE_PENDING, @@ -295,6 +297,42 @@ namespace Barotrauma static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength)); } + public static void OnAfflictionCountChanged(Character character) => + GameMain.GameSession?.Campaign?.MedicalClinic?.OnAfflictionCountChangedPrivate(character); + + private void OnAfflictionCountChangedPrivate(Character character) + { + if (character is not { CharacterHealth: { } health, Info: { } info }) { return; } + + ImmutableArray afflictions = GetAllAfflictions(health); + +#if CLIENT + if (GameMain.NetworkMember is null) + { + ui?.UpdateAfflictions(new NetCrewMember(info, afflictions)); + } + + ui?.UpdateCrewPanel(); +#elif SERVER + foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) + { + if (sub.Expiry < DateTimeOffset.Now) + { + afflictionSubscribers.Remove(sub); + continue; + } + + if (sub.Target == info) + { + ServerSend(new NetCrewMember(info, afflictions), + header: NetworkHeader.AFFLICTION_UPDATE, + deliveryMethod: DeliveryMethod.Unreliable, + targetClient: sub.Subscriber); + } + } +#endif + } + public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price); private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue; @@ -330,7 +368,7 @@ namespace Barotrauma new NetAffliction { Identifier = "internaldamage".ToIdentifier(), Strength = 80, Price = 10 }, new NetAffliction { Identifier = "blunttrauma".ToIdentifier(), Strength = 50, Price = 10 }, new NetAffliction { Identifier = "lacerations".ToIdentifier(), Strength = 20, Price = 10 }, - new NetAffliction { Identifier = "burn".ToIdentifier(), Strength = 10, Price = 10 } + new NetAffliction { Identifier = AfflictionPrefab.DamageType, Strength = 10, Price = 10 } }; #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/SlideshowPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/SlideshowPrefab.cs new file mode 100644 index 000000000..386a1836b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/SlideshowPrefab.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + class SlideshowPrefab : Prefab + { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + public class Slide + { + public readonly LocalizedString Text; + public readonly Sprite Portrait; + + public readonly float FadeInDelay, FadeInDuration, FadeOutDuration; + public readonly float TextFadeInDelay, TextFadeInDuration; + + public Slide(ContentXElement element) + { + string text = element.GetAttributeString(nameof(Text), string.Empty); + Text = TextManager.Get(text).Fallback(text); + + FadeInDelay = element.GetAttributeFloat(nameof(FadeInDelay), 0.0f); + FadeInDuration = element.GetAttributeFloat(nameof(FadeInDuration), 2.0f); + FadeOutDuration = element.GetAttributeFloat(nameof(FadeOutDuration), 2.0f); + TextFadeInDelay = element.GetAttributeFloat(nameof(TextFadeInDelay), 2.0f); + TextFadeInDuration = element.GetAttributeFloat(nameof(TextFadeInDuration), 3.0f); + + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "portrait": + Portrait = new Sprite(subElement, lazyLoad: true); + break; + } + } + } + } + + public readonly ImmutableArray Slides; + + public SlideshowPrefab(ContentFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", "")) + { + List slides = new List(); + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "slide": + slides.Add(new Slide(subElement)); + break; + } + } + Slides = slides.ToImmutableArray(); + } + + public override void Dispose() { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index f627284a2..d644c8267 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -105,15 +105,21 @@ namespace Barotrauma string slotString = subElement.GetAttributeString("slot", "None"); InvSlotType slot = Enum.TryParse(slotString, ignoreCase: true, out InvSlotType s) ? s : InvSlotType.None; - Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false), slot: slot, onSpawned: (Item item) => + + bool forceToSlot = subElement.GetAttributeBool("forcetoslot", false); + int amount = subElement.GetAttributeInt("amount", 1); + for (int i = 0; i < amount; i++) { - if (item != null && item.ParentInventory != this) + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, this, ignoreLimbSlots: forceToSlot, slot: slot, onSpawned: (Item item) => { - string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\"."; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - } - }); + if (item != null && item.ParentInventory != this) + { + string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\"."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } + }); + } } } @@ -172,21 +178,6 @@ namespace Barotrauma (SlotTypes[i] == InvSlotType.Any || slots[i].Items.Count < 1); } - public bool CanBeAutoMovedToCorrectSlots(Item item) - { - if (item == null) { return false; } - foreach (var allowedSlot in item.AllowedSlots) - { - InvSlotType slotsFree = InvSlotType.None; - for (int i = 0; i < slots.Length; i++) - { - if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Empty()) { slotsFree |= SlotTypes[i]; } - } - if (allowedSlot == slotsFree) { return true; } - } - return false; - } - public override void RemoveItem(Item item) { RemoveItem(item, tryEquipFromSameStack: false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 3ef615e8a..3c26d393f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -507,9 +507,9 @@ namespace Barotrauma.Items.Components wire.RemoveConnection(DockingTarget.item); powerConnection.TryAddLink(wire); - wire.Connect(powerConnection, false, false); + wire.TryConnect(powerConnection, addNode: false); recipient.TryAddLink(wire); - wire.Connect(recipient, false, false); + wire.TryConnect(recipient, addNode: false); //Flag connections to be updated Powered.ChangedConnections.Add(powerConnection); @@ -543,8 +543,8 @@ namespace Barotrauma.Items.Components System.Diagnostics.Debug.Assert(doorBody == null); doorBody = GameMain.World.CreateRectangle( - DockingTarget.Door.Body.width, - DockingTarget.Door.Body.height, + DockingTarget.Door.Body.Width, + DockingTarget.Door.Body.Height, 1.0f, position); doorBody.UserData = DockingTarget.Door; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 96d89ddc3..517c3d8f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using FarseerPhysics.Dynamics; #if CLIENT using Barotrauma.Lights; #endif @@ -13,6 +14,10 @@ namespace Barotrauma.Items.Components { partial class Door : Pickable, IDrawableComponent, IServerSerializable { + private static readonly HashSet doorList = new HashSet(); + + public static IReadOnlyCollection DoorList { get { return doorList; } } + private Gap linkedGap; private bool isOpen; @@ -89,6 +94,9 @@ namespace Barotrauma.Items.Components public PhysicsBody Body { get; private set; } + //the fixture that's part of the submarine's collider (= fixture that things outside the sub can collide with if the door is outside hulls) + public Fixture OutsideSubmarineFixture; + private float RepairThreshold { get { return item.GetComponent() == null ? 0.0f : item.MaxCondition; } @@ -162,9 +170,14 @@ namespace Barotrauma.Items.Components set { isOpen = value; - OpenState = (isOpen) ? 1.0f : 0.0f; + OpenState = isOpen ? 1.0f : 0.0f; } } + public bool IsClosed => !IsOpen; + + public bool IsFullyOpen => IsOpen && OpenState >= 1.0f; + + public bool IsFullyClosed => IsClosed && OpenState <= 0f; [Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")] public bool HasIntegratedButtons { get; private set; } @@ -226,6 +239,7 @@ namespace Barotrauma.Items.Components } IsActive = true; + doorList.Add(this); } public override void OnItemLoaded() @@ -394,11 +408,20 @@ namespace Barotrauma.Items.Components if (isClosing) { if (OpenState < 0.9f) { PushCharactersAway(); } + if (CheckSubmarinesInDoorWay()) + { + PredictedState = null; + isOpen = true; + } } else { bool wasEnabled = Body.Enabled; Body.Enabled = Impassable || openState < 1.0f; + if (OutsideSubmarineFixture != null) + { + OutsideSubmarineFixture.CollidesWith = Body.Enabled ? SubmarineBody.CollidesWith : Category.None; + } if (wasEnabled && !Body.Enabled && IsHorizontal) { //when opening a hatch, force characters above it to refresh the floor position @@ -442,6 +465,10 @@ namespace Barotrauma.Items.Components } PushCharactersAway(); } + if (OutsideSubmarineFixture != null && Body.Enabled) + { + OutsideSubmarineFixture.CollidesWith = SubmarineBody.CollidesWith; + } #if CLIENT UpdateConvexHulls(); #endif @@ -462,10 +489,16 @@ namespace Barotrauma.Items.Components ce = ce.Next; } } + + if (OutsideSubmarineFixture != null) + { + OutsideSubmarineFixture.CollidesWith = Category.None; + } if (linkedGap != null) { linkedGap.Open = 1.0f; } + IsOpen = false; #if CLIENT if (convexHull != null) { convexHull.Enabled = false; } @@ -488,11 +521,8 @@ namespace Barotrauma.Items.Components { RefreshLinkedGap(); #if CLIENT - Vector2[] corners = GetConvexHullCorners(Rectangle.Empty); - - convexHull = new ConvexHull(corners, Color.Black, item); - if (Window != Rectangle.Empty) convexHull2 = new ConvexHull(corners, Color.Black, item); - + convexHull = new ConvexHull(Rectangle.Empty, !IsHorizontal, item); + if (Window != Rectangle.Empty) { convexHull2 = new ConvexHull(Rectangle.Empty, !IsHorizontal, item); } UpdateConvexHulls(); #endif } @@ -540,6 +570,36 @@ namespace Barotrauma.Items.Components convexHull?.Remove(); convexHull2?.Remove(); #endif + + doorList.Remove(this); + } + + private bool CheckSubmarinesInDoorWay() + { + if (linkedGap != null && linkedGap.IsRoomToRoom) { return false; } + + Rectangle doorRect = item.WorldRect; + if (IsHorizontal) + { + doorRect.Width = (int)(item.Rect.Width * (1.0f - openState)); + } + else + { + doorRect.Height = (int)(item.Rect.Height * (1.0f - openState)); + } + + foreach (Submarine sub in Submarine.Loaded) + { + if (sub == item.Submarine || sub.DockedTo.Contains(item.Submarine)) { continue; } + Rectangle worldBorders = sub.Borders; + worldBorders.Location += sub.WorldPosition.ToPoint(); + if (!Submarine.RectsOverlap(worldBorders, doorRect)) { continue; } + foreach (Hull hull in sub.GetHulls(alsoFromConnectedSubs: false)) + { + if (Submarine.RectsOverlap(hull.WorldRect, doorRect)) { return true; } + } + } + return false; } bool itemPosErrorShown; @@ -563,7 +623,6 @@ namespace Barotrauma.Items.Components Vector2 currSize = IsHorizontal ? new Vector2(item.Rect.Width * (1.0f - openState), doorSprite.size.Y * item.Scale) : new Vector2(doorSprite.size.X * item.Scale, item.Rect.Height * (1.0f - openState)); - Vector2 simSize = ConvertUnits.ToSimUnits(currSize); foreach (Character c in Character.CharacterList) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index ff4f8b21b..b039709e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -600,7 +600,7 @@ namespace Barotrauma.Items.Components public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - //no further data needed, the event just triggers the discharge + msg.WriteUInt16(user?.ID ?? Entity.NullEntityID); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs index c77aa4a7c..28264f432 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs @@ -67,11 +67,18 @@ namespace Barotrauma.Items.Components [Serialize(true, IsPropertySaveable.Yes, "")] public bool CanSpawn { get; set; } = true; + [Editable, Serialize(false, IsPropertySaveable.Yes, "")] + public bool PreloadCharacter { get; set; } + private float spawnTimer; private float? spawnTimerGoal; private int spawnedAmount = 0; + private Character? preloadedCharacter; + + private bool preloadInitiated; + public EntitySpawnerComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -103,12 +110,21 @@ namespace Barotrauma.Items.Components } } } - - base.OnItemLoaded(); } public override void Update(float deltaTime, Camera cam) { + if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated) + { + SpawnCharacter(Vector2.Zero, onSpawn: (Character c) => + { + preloadedCharacter = c; + c.DisabledByEvent = true; + }); + preloadInitiated = true; + return; + } + base.Update(deltaTime, cam); item.SendSignal(CanSpawn ? "1" : "0", "state_out"); @@ -269,10 +285,18 @@ namespace Barotrauma.Items.Components { if (!string.IsNullOrWhiteSpace(SpeciesName)) { - Identifier[] allSpecies = SpeciesName.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); - Identifier species = allSpecies.GetRandomUnsynced(); - Entity.Spawner?.AddCharacterToSpawnQueue(species, pos); - spawnedAmount++; + if (preloadedCharacter != null) + { + preloadedCharacter.DisabledByEvent = false; + preloadedCharacter.TeleportTo(pos); + preloadedCharacter = null; + spawnedAmount++; + } + else + { + SpawnCharacter(pos); + spawnedAmount++; + } } else if (!string.IsNullOrWhiteSpace(ItemIdentifier)) { @@ -291,5 +315,15 @@ namespace Barotrauma.Items.Components } } } + + private void SpawnCharacter(Vector2 pos, Action? onSpawn = null) + { + if (!string.IsNullOrWhiteSpace(SpeciesName)) + { + Identifier[] allSpecies = SpeciesName.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); + Identifier species = allSpecies.GetRandomUnsynced(); + Entity.Spawner?.AddCharacterToSpawnQueue(species, pos, onSpawn); + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index b59784eeb..6620a203c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -226,7 +226,7 @@ namespace Barotrauma.Items.Components Pusher = null; if (element.GetAttributeBool("blocksplayers", false)) { - Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, + Pusher = new PhysicsBody(item.body.Width, item.body.Height, item.body.Radius, item.body.Density, BodyType.Dynamic, Physics.CollisionItemBlocking, @@ -427,10 +427,11 @@ namespace Barotrauma.Items.Components return; } + //cannot hold and wear an item at the same time + //(unless the slot in which it's held and worn are equal - e.g. a suit with built-in tool or weapon on one hand) var wearable = item.GetComponent(); - if (wearable != null) + if (wearable != null && !wearable.AllowedSlots.SequenceEqual(allowedSlots)) { - //cannot hold and wear an item at the same time wearable.Unequip(character); } @@ -558,10 +559,16 @@ namespace Barotrauma.Items.Components public override bool OnPicked(Character picker) { +#if CLIENT if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { + if (!picker.Inventory.CanBeAutoMovedToCorrectSlots(item)) + { + picker.Inventory.FlashAllowedSlots(item, Color.Red); + } return false; } +#endif bool wasAttached = IsAttached; if (base.OnPicked(picker)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 4ad251c17..816ff3c9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -128,7 +128,7 @@ namespace Barotrauma.Items.Components if (body != null) { - trigger = new PhysicsBody(body.width, body.height, body.radius, + trigger = new PhysicsBody(body.Width, body.Height, body.Radius, body.Density, BodyType.Static, Physics.CollisionWall, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 548147bfc..da760edf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -445,7 +445,7 @@ namespace Barotrauma.Items.Components targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, fullColor: GUIStyle.HealthBarColorHigh, - textTag: targetItem.Name); + textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 774bf603b..9751820c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -5,9 +5,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -98,6 +96,9 @@ namespace Barotrauma.Items.Components private set; } + private readonly IReadOnlySet suitableProjectiles; + + private enum ChargingState { Inactive, @@ -130,12 +131,11 @@ namespace Barotrauma.Items.Components // TODO: should define this in xml if we have ranged weapons that don't require aim to use item.RequireAimToUse = true; characterUsable = true; - + suitableProjectiles = element.GetAttributeIdentifierArray(nameof(suitableProjectiles), Array.Empty()).ToHashSet(); if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload) { DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement."); } - InitProjSpecific(element); } @@ -143,7 +143,8 @@ namespace Barotrauma.Items.Components public override void Equip(Character character) { - ReloadTimer = Math.Min(reload, 1.0f); + //clamp above 1 to prevent rapid-firing by swapping weapons + ReloadTimer = Math.Max(Math.Min(reload, 1.0f), ReloadTimer); IsActive = true; } @@ -259,7 +260,8 @@ namespace Barotrauma.Items.Components { Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi; - float spread = GetSpread(character) * Rand.Range(-0.5f, 0.5f); + float spread = GetSpread(character) * Projectile.GetSpreadFromPool(); + var lastProjectile = LastProjectile; if (lastProjectile != projectile) { @@ -275,7 +277,7 @@ namespace Barotrauma.Items.Components { Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } - projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); + projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool()); } Item.RemoveContained(projectile.Item); } @@ -294,39 +296,41 @@ namespace Barotrauma.Items.Components public Projectile FindProjectile(bool triggerOnUseOnContainers = false) { - var containedItems = item.OwnInventory?.AllItemsMod; - if (containedItems == null) { return null; } - - foreach (Item item in containedItems) + foreach (ItemContainer container in item.GetComponents()) { - if (item == null) { continue; } - Projectile projectile = item.GetComponent(); - if (projectile != null) { return projectile; } - } - - //projectile not found, see if one of the contained items contains projectiles - foreach (Item it in containedItems) - { - if (it == null) { continue; } - var containedSubItems = it.OwnInventory?.AllItemsMod; - if (containedSubItems == null) { continue; } - foreach (Item subItem in containedSubItems) + foreach (Item containedItem in container.Inventory.AllItemsMod) { - if (subItem == null) { continue; } - Projectile projectile = subItem.GetComponent(); - //apply OnUse statuseffects to the container in case it has to react to it somehow - //(play a sound, spawn more projectiles, reduce condition...) - if (triggerOnUseOnContainers && subItem.Condition > 0.0f) + if (containedItem == null) { continue; } + Projectile projectile = containedItem.GetComponent(); + if (IsSuitableProjectile(projectile)) { return projectile; } + + //projectile not found, see if the contained item contains projectiles + var containedSubItems = containedItem.OwnInventory?.AllItemsMod; + if (containedSubItems == null) { continue; } + foreach (Item subItem in containedSubItems) { - subItem.GetComponent()?.Item.ApplyStatusEffects(ActionType.OnUse, 1.0f); - } - if (projectile != null) { return projectile; } + if (subItem == null) { continue; } + Projectile subProjectile = subItem.GetComponent(); + //apply OnUse statuseffects to the container in case it has to react to it somehow + //(play a sound, spawn more projectiles, reduce condition...) + if (triggerOnUseOnContainers && subItem.Condition > 0.0f) + { + subItem.GetComponent()?.Item.ApplyStatusEffects(ActionType.OnUse, 1.0f); + } + if (IsSuitableProjectile(subProjectile)) { return subProjectile; } + } } } - return null; } + private bool IsSuitableProjectile(Projectile projectile) + { + if (projectile?.Item == null) { return false; } + if (!suitableProjectiles.Any()) { return true; } + return suitableProjectiles.Any(s => projectile.Item.Prefab.Identifier == s || projectile.Item.HasTag(s)); + } + partial void LaunchProjSpecific(); } class AbilityRangedWeapon : AbilityObject, IAbilityItem diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 1451570d3..10143e307 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -100,6 +100,9 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No, description: "Can the item hit broken doors.")] public bool HitBrokenDoors { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "Should the tool ignore characters? Enabled e.g. for fire extinguisher.")] + public bool IgnoreCharacters { get; set; } + [Serialize(0.0f, IsPropertySaveable.No, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")] public float FireProbability { get; set; } @@ -313,7 +316,11 @@ namespace Barotrauma.Items.Components private readonly List fireSourcesInRange = new List(); private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List ignoredBodies) { - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; + var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; + if (!IgnoreCharacters) + { + collisionCategories |= Physics.CollisionCharacter; + } //if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them if (statusEffectLists != null) @@ -703,7 +710,7 @@ namespace Barotrauma.Items.Components private float repairTimer; private Gap previousGap; private readonly float repairTimeOut = 5; - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (!(objective.OperateTarget is Gap leak)) { @@ -901,17 +908,16 @@ namespace Barotrauma.Items.Components // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. foreach (ISerializableEntity target in currentTargets) { - if (!(target is Door door)) { continue; } + if (target is not Door door) { continue; } if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } - for (int i = 0; i < effect.propertyNames.Length; i++) + foreach (var propertyEffect in effect.PropertyEffects) { - Identifier propertyName = effect.propertyNames[i]; - if (propertyName != "stuck") { continue; } - if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } + if (propertyEffect.propertyName != "stuck") { continue; } + if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyEffect.propertyName, out SerializableProperty property)) { continue; } object value = property.GetValue(target); if (door.Stuck > 0) { - bool isCutting = effect.propertyEffects[i].GetType() == typeof(float) && (float)effect.propertyEffects[i] < 0; + bool isCutting = propertyEffect.value is float and < 0; var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White, textTag: isCutting ? "progressbar.cutting" : "progressbar.welding"); if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 8b8ff6601..422152d7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -42,6 +42,8 @@ namespace Barotrauma.Items.Components } } + public const float WaterDragCoefficient = 0.5f; + public override bool Use(float deltaTime, Character character = null) { //actual throwing logic is handled in Update @@ -59,6 +61,7 @@ namespace Barotrauma.Items.Components base.Drop(dropper); throwState = ThrowState.None; throwAngle = ThrowAngleStart; + Item.ResetWaterDragCoefficient(); } public override void UpdateBroken(float deltaTime, Camera cam) @@ -97,6 +100,7 @@ namespace Barotrauma.Items.Components } item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; midAir = false; + Item.ResetWaterDragCoefficient(); } return; } @@ -188,6 +192,7 @@ namespace Barotrauma.Items.Components } item.Drop(CurrentThrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer); + item.WaterDragCoefficient = WaterDragCoefficient; item.body.ApplyLinearImpulse(throwVector * ThrowForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); //disable platform collisions until the item comes back to rest again diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 9d8b2e08e..4f0491cde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -111,8 +111,9 @@ namespace Barotrauma.Items.Components private bool drawable = true; - [Serialize(PropertyConditional.Comparison.And, IsPropertySaveable.No)] - public PropertyConditional.Comparison IsActiveConditionalComparison + #warning TODO: misnomer - should be IsActiveConditionalLogicalOperator + [Serialize(PropertyConditional.LogicalOperatorType.And, IsPropertySaveable.No)] + public PropertyConditional.LogicalOperatorType IsActiveConditionalComparison { get; set; @@ -245,17 +246,10 @@ namespace Barotrauma.Items.Components [Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public int ManuallySelectedSound { get; private set; } - /// /// Can be used by status effects or conditionals to the speed of the item /// - public float Speed - { - get - { - return item.Speed; - } - } + public float Speed => item.Speed; public readonly bool InheritStatusEffects; @@ -346,14 +340,8 @@ namespace Barotrauma.Items.Components { case "activeconditional": case "isactive": - IsActiveConditionals = IsActiveConditionals ?? new List(); - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - IsActiveConditionals.Add(new PropertyConditional(attribute)); - } - } + IsActiveConditionals ??= new List(); + IsActiveConditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; case "requireditem": case "requireditems": @@ -450,7 +438,7 @@ namespace Barotrauma.Items.Components public virtual void Drop(Character dropper) { } /// true if the operation was completed - public virtual bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public virtual bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { return false; } @@ -1033,7 +1021,7 @@ namespace Barotrauma.Items.Components prevRequiredItems[newRequiredItem.Type].Find(ri => ri.JoinedIdentifiers == newRequiredItem.JoinedIdentifiers) : null; if (prevRequiredItem != null) { - newRequiredItem.statusEffects = prevRequiredItem.statusEffects; + newRequiredItem.StatusEffects = prevRequiredItem.StatusEffects; newRequiredItem.Msg = prevRequiredItem.Msg; newRequiredItem.IsOptional = prevRequiredItem.IsOptional; newRequiredItem.IgnoreInEditor = prevRequiredItem.IgnoreInEditor; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 68c59aaee..a16f4572e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -12,20 +12,9 @@ namespace Barotrauma.Items.Components { partial class ItemContainer : ItemComponent, IDrawableComponent { - class ActiveContainedItem - { - public readonly Item Item; - public readonly StatusEffect StatusEffect; - public readonly bool ExcludeBroken; - public readonly bool ExcludeFullCondition; - public ActiveContainedItem(Item item, StatusEffect statusEffect, bool excludeBroken, bool excludeFullCondition) - { - Item = item; - StatusEffect = statusEffect; - ExcludeBroken = excludeBroken; - ExcludeFullCondition = excludeFullCondition; - } - } + readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition); + + readonly record struct DrawableContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation); class SlotRestrictions { @@ -63,7 +52,9 @@ namespace Barotrauma.Items.Components public readonly ItemInventory Inventory; private readonly List activeContainedItems = new List(); - + + private readonly List drawableContainedItems = new List(); + private List[] itemIds; //how many items can be contained @@ -351,8 +342,6 @@ namespace Barotrauma.Items.Components public void OnItemContained(Item containedItem) { - item.SetContainedItemPositions(); - int index = Inventory.FindIndex(containedItem); if (index >= 0 && index < slotRestrictions.Length) { @@ -362,7 +351,7 @@ namespace Barotrauma.Items.Components foreach (var containableItem in slotRestrictions[index].ContainableItems) { if (!containableItem.MatchesItem(containedItem)) { continue; } - foreach (StatusEffect effect in containableItem.statusEffects) + foreach (StatusEffect effect in containableItem.StatusEffects) { activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition)); } @@ -370,6 +359,14 @@ namespace Barotrauma.Items.Components } } + var relatedItem = FindContainableItem(containedItem); + drawableContainedItems.RemoveAll(d => d.Item == containedItem); + drawableContainedItems.Add(new DrawableContainedItem(containedItem, + Hide: relatedItem?.Hide ?? false, + ItemPos: relatedItem?.ItemPos, + Rotation: relatedItem?.Rotation ?? 0.0f)); + drawableContainedItems.Sort((DrawableContainedItem it1, DrawableContainedItem it2) => Inventory.FindIndex(it1.Item).CompareTo(Inventory.FindIndex(it2.Item))); + if (item.GetComponent() != null) { GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningPlanted:" + containedItem.Prefab.Identifier); @@ -384,6 +381,7 @@ namespace Barotrauma.Items.Components // Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it). SetContainedActive(true); } + item.SetContainedItemPositions(); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); OnContainedItemsChanged.Invoke(this); } @@ -396,6 +394,7 @@ namespace Barotrauma.Items.Components public void OnItemRemoved(Item containedItem) { activeContainedItems.RemoveAll(i => i.Item == containedItem); + drawableContainedItems.RemoveAll(i => i.Item == containedItem); //deactivate if the inventory is empty IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); @@ -483,11 +482,11 @@ namespace Barotrauma.Items.Components { foreach (Item item in Inventory.AllItemsMod) { - item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter); - item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter); + item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter, useTarget: ownerCharacter); + item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter, useTarget: ownerCharacter); item.GetComponent()?.Equip(ownerCharacter); - autoInjectCooldown = AutoInjectInterval; } + autoInjectCooldown = AutoInjectInterval; } } @@ -512,10 +511,18 @@ namespace Barotrauma.Items.Components if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; } StatusEffect effect = activeContainedItem.StatusEffect; - if (effect.HasTargetType(StatusEffect.TargetType.This)) + if (effect.HasTargetType(StatusEffect.TargetType.This)) + { effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects); - if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + } + if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + { effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects); + } + if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character) + { + effect.Apply(ActionType.OnContaining, deltaTime, item, character); + } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { @@ -759,54 +766,50 @@ namespace Barotrauma.Items.Components int i = 0; Vector2 currentItemPos = transformedItemPos; - foreach (Item contained in Inventory.AllItems) + foreach (DrawableContainedItem contained in drawableContainedItems) { Vector2 itemPos = currentItemPos; - var relatedItem = FindContainableItem(contained); - if (relatedItem != null) + if (contained.ItemPos.HasValue) { - if (relatedItem.ItemPos.HasValue) + Vector2 pos = contained.ItemPos.Value; + if (item.body != null) { - Vector2 pos = relatedItem.ItemPos.Value; - if (item.body != null) + Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.Position; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) { - Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); - pos.X *= item.body.Dir; - itemPos = Vector2.Transform(pos, transform) + item.body.Position; + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; } - else + if (item.FlippedY) { - itemPos = pos; - // This code is aped based on above. Not tested. - if (item.FlippedX) - { - itemPos.X = -itemPos.X; - itemPos.X += item.Rect.Width; - } - if (item.FlippedY) - { - itemPos.Y = -itemPos.Y; - itemPos.Y -= item.Rect.Height; - } - itemPos += new Vector2(item.Rect.X, item.Rect.Y); - if (Math.Abs(item.RotationRad) > 0.01f) - { - Matrix transform = Matrix.CreateRotationZ(item.RotationRad); - itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position; - } + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position; } } - } + } - if (contained.body != null) + if (contained.Item.body != null) { try { Vector2 simPos = ConvertUnits.ToSimUnits(itemPos); float rotation = itemRotation; - if (relatedItem != null && relatedItem.Rotation != 0) + if (contained.Rotation != 0) { - rotation = MathHelper.ToRadians(relatedItem.Rotation); + rotation = MathHelper.ToRadians(contained.Rotation); } if (item.body != null) { @@ -817,29 +820,29 @@ namespace Barotrauma.Items.Components { rotation += -item.RotationRad; } - contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); - contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation); - contained.body.UpdateDrawPosition(); + contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); + contained.Item.body.SetPrevTransform(contained.Item.body.SimPosition, contained.Item.body.Rotation); + contained.Item.body.UpdateDrawPosition(); } catch (Exception e) { DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); - GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Name, + GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Item.Name, GameAnalyticsManager.ErrorSeverity.Error, "SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); } - contained.body.Submarine = item.Submarine; + contained.Item.body.Submarine = item.Submarine; } - contained.Rect = + contained.Item.Rect = new Rectangle( - (int)(itemPos.X - contained.Rect.Width / 2.0f), - (int)(itemPos.Y + contained.Rect.Height / 2.0f), - contained.Rect.Width, contained.Rect.Height); + (int)(itemPos.X - contained.Item.Rect.Width / 2.0f), + (int)(itemPos.Y + contained.Item.Rect.Height / 2.0f), + contained.Item.Rect.Width, contained.Item.Rect.Height); - contained.Submarine = item.Submarine; - contained.CurrentHull = item.CurrentHull; - contained.SetContainedItemPositions(); + contained.Item.Submarine = item.Submarine; + contained.Item.CurrentHull = item.CurrentHull; + contained.Item.SetContainedItemPositions(); i++; if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index d6ae48ec5..e3d29d09b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -116,7 +116,7 @@ namespace Barotrauma.Items.Components { float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor); float currForce = force * voltageFactor; - float condition = item.Condition / item.MaxCondition; + float condition = item.MaxCondition <= 0.0f ? 0.0f : item.Condition / item.MaxCondition; // Broken engine makes more noise. float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition); UpdateAITargets(noise); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 0c41133e9..f8f3df86e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -267,7 +267,7 @@ namespace Barotrauma.Items.Components itemList.Enabled = true; if (amountInput != null) { - amountInput.Enabled = true; + amountInput.Enabled = amountTextMax.Enabled; } RefreshActivateButtonText(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 923623a3e..0401bda5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -235,7 +235,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { #if CLIENT if (GameMain.Client != null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index cdf2c9298..3ecd2752f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -671,7 +671,7 @@ namespace Barotrauma.Items.Components return picker != null; } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } character.AIController.SteeringManager.Reset(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 807c13d23..1beb6ee19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -272,7 +272,7 @@ namespace Barotrauma.Items.Components private static readonly Dictionary> targetGroups = new Dictionary>(); - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 90922ec50..cc73b26fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -720,7 +720,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { character.AIController.SteeringManager.Reset(); if (objective.Override) @@ -813,7 +813,7 @@ namespace Barotrauma.Items.Components } } - sonar?.AIOperate(deltaTime, character, objective); + sonar?.CrewAIOperate(deltaTime, character, objective); if (!MaintainPos && showIceSpireWarning && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("dialogicespirespottedsonar").Value, null, 0.0f, "icespirespottedsonar".ToIdentifier(), 60.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 4c90945ba..45d2af075 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -303,7 +303,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 23c9487a8..50dce4efd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -6,6 +6,7 @@ using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Voronoi2; @@ -13,6 +14,20 @@ namespace Barotrauma.Items.Components { partial class Projectile : ItemComponent, IServerSerializable { + const int SpreadCounterWrapAround = 256; + + private static readonly ImmutableArray spreadPool; + static Projectile() + { + MTRandom random = new MTRandom(0); + spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray(); + } + + public static float GetSpreadFromPool() + { + return spreadPool[SpreadCounter]; + } + struct HitscanResult { public Fixture Fixture; @@ -41,10 +56,14 @@ namespace Barotrauma.Items.Components } } + public const float WaterDragCoefficient = 0.1f; + private readonly Queue impactQueue = new Queue(); private bool removePending; + public static byte SpreadCounter { get; private set; } + //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; @@ -192,7 +211,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; hitscan are launched with an equal amount of angle between them. Only applies when firing multiple hitscan.")] + [Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; projectiles are launched with an equal amount of angle between them. Only applies when firing multiple projectiles.")] public bool StaticSpread { get; @@ -280,6 +299,8 @@ namespace Barotrauma.Items.Components return; } + SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround); + InitProjSpecific(element); } partial void InitProjSpecific(ContentXElement element); @@ -292,13 +313,13 @@ namespace Barotrauma.Items.Components switch (item.body.BodyShape) { case PhysicsBody.Shape.Circle: - Attack.DamageRange = item.body.radius; + Attack.DamageRange = item.body.Radius; break; case PhysicsBody.Shape.Capsule: - Attack.DamageRange = item.body.height / 2 + item.body.radius; + Attack.DamageRange = item.body.Height / 2 + item.body.Radius; break; case PhysicsBody.Shape.Rectangle: - Attack.DamageRange = new Vector2(item.body.width / 2.0f, item.body.height / 2.0f).Length(); + Attack.DamageRange = new Vector2(item.body.Width / 2.0f, item.body.Height / 2.0f).Length(); break; } Attack.DamageRange = ConvertUnits.ToDisplayUnits(Attack.DamageRange); @@ -358,8 +379,8 @@ namespace Barotrauma.Items.Components if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { #if SERVER - launchRot = rotation; - Item.CreateServerEvent(this, new EventData(launch: true)); + launchRot = rotation; + Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1))); #endif } } @@ -368,23 +389,23 @@ namespace Barotrauma.Items.Components { if (character != null && !characterUsable) { return false; } if (item.body == null) { return false; } + //can't launch if already launched + if (StickTarget != null || IsActive) { return false; } + float initialRotation = item.body.Rotation; for (int i = 0; i < HitScanCount; i++) { float launchAngle; - + if (StaticSpread) { - float staticSpread = Spread / (HitScanCount - 1); - // because the position of the item changes as hitscan are fired, we will set an - // initial offset on the first hitscan and then increase the item's angle by a set amount as hitscan are fired - float offset = i == 0 ? -staticSpread * (HitScanCount -1) : 0f; - launchAngle = item.body.Rotation + MathHelper.ToRadians(staticSpread + offset); + launchAngle = initialRotation + MathHelper.ToRadians(i - ((float)(HitScanCount - 1) / 2)) * Spread; } else { - launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * Rand.Range(-0.5f, 0.5f)); + launchAngle = initialRotation + MathHelper.ToRadians(Spread * GetSpreadFromPool()); } + SpreadCounter++; Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle)); if (Hitscan) @@ -401,8 +422,7 @@ namespace Barotrauma.Items.Components { item.body.SetTransform(item.body.SimPosition, launchAngle); float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread)); - DoLaunch(launchDir * modifiedLaunchImpulse * item.body.Mass); - System.Diagnostics.Debug.WriteLine("launch: " + modifiedLaunchImpulse + " - " + item.body.LinearVelocity); + DoLaunch(launchDir * modifiedLaunchImpulse); } } User = character; @@ -423,15 +443,26 @@ namespace Barotrauma.Items.Components } item.Drop(null, createNetworkEvent: false); + Item.WaterDragCoefficient = WaterDragCoefficient; launchPos = item.SimPosition; item.body.Enabled = true; - item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f); + if (item.body.BodyType == BodyType.Kinematic) + { + item.body.LinearVelocity = impulse; + } + else + { + impulse *= item.body.Mass; + item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f); + } item.body.FarseerBody.OnCollision += OnProjectileCollision; item.body.FarseerBody.IsBullet = true; + EnableProjectileCollisions(); + IsActive = true; if (stickJoint == null) { return; } @@ -447,6 +478,7 @@ namespace Barotrauma.Items.Components Vector2 simPositon = item.SimPosition; Vector2 rayStartWorld = item.WorldPosition; item.Drop(null); + Item.WaterDragCoefficient = WaterDragCoefficient; item.body.Enabled = true; //set the velocity of the body because the OnProjectileCollision method @@ -505,6 +537,7 @@ namespace Barotrauma.Items.Components { var h = hits[i]; item.SetTransform(h.Point, rotation); + item.UpdateTransform(); if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero)) { hitCount++; @@ -560,6 +593,8 @@ namespace Barotrauma.Items.Components return true; } if (fixture.Body.UserData is VineTile) { return true; } + if (fixture.CollidesWith == Category.None) { return true; } + if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; } //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub @@ -611,6 +646,7 @@ namespace Barotrauma.Items.Components return -1; } if (fixture.Body.UserData is VineTile) { return -1; } + if (fixture.CollidesWith == Category.None) { return -1; } if (fixture.Body.UserData is Item item) { if (item.Condition <= 0) { return -1; } @@ -669,6 +705,7 @@ namespace Barotrauma.Items.Components public override void Drop(Character dropper) { + Item.ResetWaterDragCoefficient(); if (dropper != null) { DisableProjectileCollisions(); @@ -755,6 +792,7 @@ namespace Barotrauma.Items.Components { if (User != null && User.Removed) { User = null; return false; } if (IgnoredBodies != null && IgnoredBodies.Contains(target.Body)) { return false; } + if (originalCollisionCategories == Category.None && originalCollisionTargets == Category.None) { return false; } //ignore character colliders (the projectile only hits limbs) if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character) { @@ -840,8 +878,24 @@ namespace Barotrauma.Items.Components } if (target.Body.UserData is Submarine sub) { - Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? - contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity); + //hit an item in a different sub -> no need to ignore, we can process the impact with this info + //(if it wasn't, we'll move the projectile to that sub's coordinate space and let it hit what it hits there) + if (Launcher?.Submarine != sub && target.UserData is Item) + { + return false; + } + + Vector2 normalizedVel; + Vector2 dir; + if (item.body.LinearVelocity.LengthSquared() < 0.001f) + { + normalizedVel = Vector2.Zero; + dir = contact.Manifold.LocalNormal; + } + else + { + normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity); + } //do a raycast in the sub's coordinate space to see if it hit a structure var wallBody = Submarine.PickBody( @@ -850,7 +904,7 @@ namespace Barotrauma.Items.Components collisionCategory: Physics.CollisionWall); if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) && //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction - Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) + Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0) { target = wallBody.FixtureList.First(); if (hits.Contains(target.Body)) @@ -886,7 +940,7 @@ namespace Barotrauma.Items.Components AttackResult attackResult = new AttackResult(); Character character = null; - if (target.Body.UserData is Submarine submarine) + if (target.Body.UserData is Submarine submarine && target.UserData is not Barotrauma.Item) { item.Move(-submarine.Position); item.Submarine = submarine; @@ -911,9 +965,11 @@ namespace Barotrauma.Items.Components if (Attack != null) { attackResult = Attack.DoDamageToLimb(User ?? Attacker, limb, item.WorldPosition, 1.0f); } if (limb.character != null) { character = limb.character; } } - else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item) is Item targetItem) + else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item ?? target.UserData as Item) is Item targetItem) { if (targetItem.Removed) { return false; } + //hit the external collider of an item (turret?) of the same sub -> ignore + if (target.UserData is Item && targetItem.Submarine != null && targetItem.Submarine == Launcher?.Submarine) { return false; } if (Attack != null && (targetItem.Prefab.DamagedByProjectiles || DamageDoors && targetItem.GetComponent() != null) && targetItem.Condition > 0) { attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); @@ -925,7 +981,7 @@ namespace Barotrauma.Items.Components targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, fullColor: GUIStyle.HealthBarColorHigh, - textTag: targetItem.Name); + textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty); } #endif } @@ -1091,10 +1147,14 @@ namespace Barotrauma.Items.Components private void EnableProjectileCollisions() { - item.body.CollisionCategories = Physics.CollisionProjectile; - item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; - if (!IgnoreProjectilesWhileActive) + if (item.body.CollisionCategories != Category.None) { + item.body.CollisionCategories = Physics.CollisionProjectile; + item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; + } + if (item.Prefab.DamagedByProjectiles && !IgnoreProjectilesWhileActive) + { + if (item.body.CollisionCategories == Category.None) { item.body.CollisionCategories = Physics.CollisionCharacter; } item.body.CollidesWith |= Physics.CollisionProjectile; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index e5604f449..66e6e93e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -1,12 +1,11 @@ -using Barotrauma.Extensions; +using Barotrauma.Abilities; +using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Abilities; namespace Barotrauma.Items.Components { @@ -17,6 +16,9 @@ namespace Barotrauma.Items.Components private float deteriorationTimer; private float deteriorateAlwaysResetTimer; + private int updateDeteriorationCounter; + private const int UpdateDeteriorationInterval = 10; + private int prevSentConditionValue; private string conditionSignal; @@ -232,6 +234,7 @@ namespace Barotrauma.Items.Components public float RepairDegreeOfSuccess(Character character, List skills) { if (skills.Count == 0) { return 1.0f; } + if (character == null) { return 0.0f; } float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum(); float average = skillSum / skills.Count; @@ -241,6 +244,7 @@ namespace Barotrauma.Items.Components public void RepairBoost(bool qteSuccess) { + if (CurrentFixer == null) { return; } if (qteSuccess) { item.Condition += RepairDegreeOfSuccess(CurrentFixer, requiredSkills) * 3 * (currentFixerAction == FixActions.Repair ? 1.0f : -1.0f); @@ -404,26 +408,11 @@ namespace Barotrauma.Items.Components #endif } } - if (!ShouldDeteriorate()) { return; } - if (item.Condition > 0.0f) + updateDeteriorationCounter++; + if (updateDeteriorationCounter >= UpdateDeteriorationInterval) { - if (deteriorationTimer > 0.0f) - { - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) - { - deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier(); -#if SERVER - if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); } -#endif - } - return; - } - - if (item.ConditionPercentage > MinDeteriorationCondition) - { - float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); - item.Condition -= deteriorationSpeed * deltaTime; - } + UpdateDeterioration(deltaTime * UpdateDeteriorationInterval); + updateDeteriorationCounter = 0; } return; } @@ -559,6 +548,30 @@ namespace Barotrauma.Items.Components } } + private void UpdateDeterioration(float deltaTime) + { + if (item.Condition <= 0.0f) { return; } + if (!ShouldDeteriorate()) { return; } + + if (deteriorationTimer > 0.0f) + { + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + { + deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier(); +#if SERVER + if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); } +#endif + } + return; + } + + if (item.ConditionPercentage > MinDeteriorationCondition) + { + float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); + item.Condition -= deteriorationSpeed * deltaTime; + } + } + private float GetMaxRepairConditionMultiplier(Character character) { if (character == null) { return 1.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 224d1882f..b73b7cbf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components public readonly List Effects; - public readonly List LoadedWireIds; + public readonly List<(ushort wireId, int? connectionIndex)> LoadedWires; //The grid the connection is a part of public GridInfo Grid; @@ -151,16 +151,20 @@ namespace Barotrauma.Items.Components IsPower = Name == "power_in" || Name == "power" || Name == "power_out"; - LoadedWireIds = new List(); + LoadedWires = new List<(ushort wireId, int? connectionIndex)>(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "link": int id = subElement.GetAttributeInt("w", 0); + int? i = null; + if (subElement.GetAttribute("i") != null) + { + i = subElement.GetAttributeInt("i", 0); + } if (id < 0) { id = 0; } - if (LoadedWireIds.Count < MaxWires) { LoadedWireIds.Add(idRemap.GetOffsetId(id)); } - + if (LoadedWires.Count < MaxWires) { LoadedWires.Add((idRemap.GetOffsetId(id), i)); } break; case "statuseffect": Effects ??= new List(); @@ -351,22 +355,29 @@ namespace Barotrauma.Items.Components public void InitializeFromLoaded() { - if (LoadedWireIds.Count == 0) { return; } + if (LoadedWires.Count == 0) { return; } - for (int i = 0; i < LoadedWireIds.Count; i++) + foreach ((ushort wireId, int? connectionIndex) in LoadedWires) { - if (!(Entity.FindEntityByID(LoadedWireIds[i]) is Item wireItem)) { continue; } + if (Entity.FindEntityByID(wireId) is not Item wireItem) { continue; } var wire = wireItem.GetComponent(); if (wire != null && TryAddLink(wire)) { - if (wire.Item.body != null) wire.Item.body.Enabled = false; - wire.Connect(this, false, false); + if (wire.Item.body != null) { wire.Item.body.Enabled = false; } + if (connectionIndex.HasValue) + { + wire.Connect(this, connectionIndex.Value, addNode: false, sendNetworkEvent: false); + } + else + { + wire.TryConnect(this, addNode: false, sendNetworkEvent: false); + } wire.FixNodeEnds(); recipientsDirty = true; } } - LoadedWireIds.Clear(); + LoadedWires.Clear(); } @@ -377,7 +388,8 @@ namespace Barotrauma.Items.Components foreach (var wire in wires.OrderBy(w => w.Item.ID)) { newElement.Add(new XElement("link", - new XAttribute("w", wire.Item.ID.ToString()))); + new XAttribute("w", wire.Item.ID.ToString()), + new XAttribute("i", wire.Connections[0] == this ? 0 : 1))); } parentElement.Add(newElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 3954c06ed..7c9fd3a1a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -295,8 +295,8 @@ namespace Barotrauma.Items.Components for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++) { - Connections[i].LoadedWireIds.Clear(); - Connections[i].LoadedWireIds.AddRange(loadedConnections[i].LoadedWireIds); + Connections[i].LoadedWires.Clear(); + Connections[i].LoadedWires.AddRange(loadedConnections[i].LoadedWires); } disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", Array.Empty()).ToList(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index dc2ae516c..eb76f17ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -243,14 +243,7 @@ namespace Barotrauma.Items.Components for (int i = 0; i < labels.Length; i++) { labels[i] = i < newLabels.Length ? newLabels[i] : customInterfaceElementList[i].Label; - if (Screen.Selected != GameMain.SubEditorScreen) - { - customInterfaceElementList[i].Label = TextManager.Get(labels[i]).Fallback(labels[i]).Value; - } - else - { - customInterfaceElementList[i].Label = labels[i]; - } + customInterfaceElementList[i].Label = labels[i]; } UpdateLabelsProjSpecific(); } @@ -304,9 +297,12 @@ namespace Barotrauma.Items.Components } #if SERVER //make sure the clients know about the states of the checkboxes and text fields - if (item.Submarine == null || !item.Submarine.Loading) + if (customInterfaceElementList.Any()) { - item.CreateServerEvent(this); + if (item.Submarine == null || !item.Submarine.Loading) + { + item.CreateServerEvent(this); + } } #endif } @@ -326,7 +322,7 @@ namespace Barotrauma.Items.Components } foreach (StatusEffect effect in btnElement.StatusEffects) { - item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f); + item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f, character: item.ParentInventory?.Owner as Character); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index da887ef93..097d23817 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; using Barotrauma.Networking; using Barotrauma.Extensions; #if CLIENT @@ -13,6 +12,9 @@ namespace Barotrauma.Items.Components partial class LightComponent : Powered, IServerSerializable, IDrawableComponent { private Color lightColor; + /// + /// The current brightness of the light source, affected by powerconsumption/voltage + /// private float lightBrightness; private float blinkFrequency; private float pulseFrequency, pulseAmount; @@ -94,7 +96,7 @@ namespace Barotrauma.Items.Components if (isOn == value && IsActive == value) { return; } IsActive = isOn = value; - SetLightSourceState(value); + SetLightSourceState(value, value ? lightBrightness : 0.0f); OnStateChanged(); } } @@ -174,7 +176,7 @@ namespace Barotrauma.Items.Components #if CLIENT if (Light != null) { - Light.Color = IsOn ? lightColor.Multiply(lightBrightness) : Color.Transparent; + Light.Color = IsOn ? lightColor.Multiply(lightColorMultiplier) : Color.Transparent; } #endif } @@ -187,7 +189,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "Should the light sprite be drawn on the item using alpha blending, in addition to being rendered in the light map? Can be used to make the light sprite stand out more.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the light sprite be drawn on the item using alpha blending, in addition to being rendered in the light map? Can be used to make the light sprite stand out more.")] public bool AlphaBlend { get; @@ -214,7 +216,7 @@ namespace Barotrauma.Items.Components { if (base.IsActive == value) { return; } base.IsActive = isOn = value; - SetLightSourceState(value); + SetLightSourceState(value, value ? lightBrightness : 0.0f); } } @@ -228,7 +230,7 @@ namespace Barotrauma.Items.Components Position = item.Position, CastShadows = castShadows, IsBackground = drawBehindSubs, - SpriteScale = Vector2.One * item.Scale, + SpriteScale = Vector2.One * item.Scale * LightSpriteScale, Range = range }; Light.LightSourceParams.Flicker = flicker; @@ -245,7 +247,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { base.OnItemLoaded(); - SetLightSourceState(IsActive); + SetLightSourceState(IsActive, lightBrightness); turret = item.GetComponent(); #if CLIENT Drawable = AlphaBlend && Light.LightSprite != null; @@ -258,6 +260,12 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { +#if CLIENT + if (item.HiddenInGame) + { + Light.Enabled = false; + } +#endif CheckIfNeedsUpdate(); } @@ -273,7 +281,8 @@ namespace Barotrauma.Items.Components (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) { - SetLightSourceState(true); + lightBrightness = 1.0f; + SetLightSourceState(true, lightBrightness); SetLightSourceTransformProjSpecific(); base.IsActive = false; isOn = true; @@ -300,18 +309,21 @@ namespace Barotrauma.Items.Components #if CLIENT Light.ParentSub = item.Submarine; #endif - if (item.Container != null && item.GetRootInventoryOwner() is not Character) + var ownerCharacter = item.GetRootInventoryOwner() as Character; + if ((item.Container != null && ownerCharacter == null) || + (ownerCharacter != null && ownerCharacter.InvisibleTimer > 0.0f)) { - SetLightSourceState(false); + lightBrightness = 0.0f; + SetLightSourceState(false, 0.0f); return; } - SetLightSourceTransformProjSpecific(); PhysicsBody body = ParentBody ?? item.body; if (body != null && !body.Enabled) { - SetLightSourceState(false); + lightBrightness = 0.0f; + SetLightSourceState(false, 0.0f); return; } @@ -338,7 +350,7 @@ namespace Barotrauma.Items.Components public override void UpdateBroken(float deltaTime, Camera cam) { - SetLightSourceState(false); + SetLightSourceState(false, 0.0f); } public override bool Use(float deltaTime, Character character = null) @@ -370,7 +382,7 @@ namespace Barotrauma.Items.Components { LightColor = XMLExtensions.ParseColor(signal.value, false); #if CLIENT - SetLightSourceState(Light.Enabled); + SetLightSourceState(Light.Enabled, lightColorMultiplier); #endif prevColorSignal = signal.value; } @@ -388,7 +400,7 @@ namespace Barotrauma.Items.Components target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness); } - partial void SetLightSourceState(bool enabled, float? brightness = null); + partial void SetLightSourceState(bool enabled, float brightness); public void SetLightSourceTransform() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 6ebfe3fc8..93527069d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -136,7 +136,7 @@ namespace Barotrauma.Items.Components } } - [Editable(DecimalCount = 3), Serialize(0.01f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] + [Editable(DecimalCount = 3), Serialize(0.1f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] public float MinimumVelocity { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 1d6188ebd..4f09c6764 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components private const int MaxMessages = 60; - private List messageHistory = new List(MaxMessages); + private readonly List messageHistory = new List(MaxMessages); public LocalizedString DisplayedWelcomeMessage { @@ -67,6 +67,12 @@ namespace Barotrauma.Items.Components [Editable, Serialize(false, IsPropertySaveable.Yes, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)] public bool UseMonospaceFont { get; set; } + [Serialize(false, IsPropertySaveable.No)] + public bool AutoHideScrollbar { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public bool WelcomeMessageDisplayed { get; set; } + private Color textColor = Color.LimeGreen; [Editable, Serialize("50,205,50,255", IsPropertySaveable.Yes, description: "Color of the terminal text.", alwaysUseInstanceValues: true)] @@ -85,6 +91,15 @@ namespace Barotrauma.Items.Components } } + [Editable, Serialize("> ", IsPropertySaveable.Yes)] + public string LineStartSymbol { get; set; } + + [Editable, Serialize(false, IsPropertySaveable.No)] + public bool Readonly { get; set; } + + [Serialize(true, IsPropertySaveable.No)] + public bool AutoScrollToBottom { get; set; } + private string OutputValue { get; set; } private string prevColorSignal; @@ -143,14 +158,14 @@ namespace Barotrauma.Items.Components #endif base.OnItemLoaded(); - if (!DisplayedWelcomeMessage.IsNullOrEmpty()) + if (!DisplayedWelcomeMessage.IsNullOrEmpty() && !WelcomeMessageDisplayed) { ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor); DisplayedWelcomeMessage = ""; - //remove welcome message if a game session is running so it doesn't reappear on successive rounds + //disable welcome message if a game session is running so it doesn't reappear on successive rounds if (GameMain.GameSession != null && !isSubEditor) { - welcomeMessage = null; + WelcomeMessageDisplayed = true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 09dfe9080..09563de0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -162,14 +162,50 @@ namespace Barotrauma.Items.Components SetConnectedDirty(); } - public bool Connect(Connection newConnection, bool addNode = true, bool sendNetworkEvent = false) + /// + /// Tries to add the given connection to this wire. Note that this only affects the wire - + /// adding the wire to the connection is done in + /// + + public bool TryConnect(Connection newConnection, bool addNode = true, bool sendNetworkEvent = false) + { + if (connections[0] == null) + { + return Connect(newConnection, 0, addNode, sendNetworkEvent); + } + else if (connections[1] == null) + { + return Connect(newConnection, 1, addNode, sendNetworkEvent); + } + return false; + } + + + /// + /// Tries to add the given connection to this wire. Note that this only affects the wire - + /// adding the wire to the connection is done in + /// + /// Which end of the wire to add the connection to? 0 or 1. + /// Normally doesn't make a difference, but matters if we're copying/loading a wire, + /// in which case the 1st node should be located at the same item as the 1st connection. + /// + public bool Connect(Connection newConnection, int connectionIndex, bool addNode = true, bool sendNetworkEvent = false) { for (int i = 0; i < 2; i++) { if (connections[i] == newConnection) { return false; } } - if (!connections.Any(c => c == null)) { return false; } + if (connectionIndex < 0 || connectionIndex > 1) + { + DebugConsole.ThrowError($"Error while connecting a wire to {newConnection.Item}: {connectionIndex} is not a valid index."); + return false; + } + if (connections[connectionIndex] != null) + { + DebugConsole.ThrowError($"Error while connecting a wire to {newConnection.Item}: a wire is already connected to the index {connectionIndex}."); + return false; + } for (int i = 0; i < 2; i++) { @@ -183,70 +219,12 @@ namespace Barotrauma.Items.Components newConnection.ConnectionPanel.DisconnectedWires.Remove(this); - for (int i = 0; i < 2; i++) + connections[connectionIndex] = newConnection; + FixNodeEnds(); + + if (addNode) { - if (connections[i] != null) { continue; } - - connections[i] = newConnection; - FixNodeEnds(); - - if (!addNode) { break; } - - Submarine refSub = newConnection.Item.Submarine; - if (refSub == null) - { - Structure attachTarget = Structure.GetAttachTarget(newConnection.Item.WorldPosition); - if (attachTarget == null && !(newConnection.Item.GetComponent()?.Attached ?? false)) - { - connections[i] = null; - continue; - } - refSub = attachTarget?.Submarine; - } - - Vector2 nodePos = refSub == null ? - newConnection.Item.Position : - newConnection.Item.Position - refSub.HiddenSubPosition; - - if (nodes.Count > 0 && nodes[0] == nodePos) { break; } - if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { break; } - - //make sure we place the node at the correct end of the wire (the end that's closest to the new node pos) - int newNodeIndex = 0; - if (nodes.Count > 1) - { - if (connections[0] != null && connections[0] != newConnection) - { - if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < - Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) - { - newNodeIndex = nodes.Count; - } - } - else if (connections[1] != null && connections[1] != newConnection) - { - if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < - Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) - { - newNodeIndex = nodes.Count; - } - } - else if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos)) - { - newNodeIndex = nodes.Count; - } - } - - if (newNodeIndex == 0 && nodes.Count > 1) - { - nodes.Insert(0, nodePos); - } - else - { - nodes.Add(nodePos); - } - - break; + AddNode(newConnection, connectionIndex); } SetConnectedDirty(); @@ -258,7 +236,7 @@ namespace Barotrauma.Items.Components if (ic == this) { continue; } ic.Drop(null); } - if (item.Container != null) { item.Container.RemoveContained(this.item); } + item.Container?.RemoveContained(item); if (item.body != null) { item.body.Enabled = false; } IsActive = false; @@ -286,6 +264,63 @@ namespace Barotrauma.Items.Components return true; } + private void AddNode(Connection newConnection, int selectedIndex) + { + Submarine refSub = newConnection.Item.Submarine; + if (refSub == null) + { + Structure attachTarget = Structure.GetAttachTarget(newConnection.Item.WorldPosition); + if (attachTarget == null && !(newConnection.Item.GetComponent()?.Attached ?? false)) + { + connections[selectedIndex] = null; + return; + } + refSub = attachTarget?.Submarine; + } + + Vector2 nodePos = refSub == null ? + newConnection.Item.Position : + newConnection.Item.Position - refSub.HiddenSubPosition; + + if (nodes.Count > 0 && nodes[0] == nodePos) { return; } + if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { return; } + + //make sure we place the node at the correct end of the wire (the end that's closest to the new node pos) + int newNodeIndex = 0; + if (nodes.Count > 1) + { + if (connections[0] != null && connections[0] != newConnection) + { + if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < + Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) + { + newNodeIndex = nodes.Count; + } + } + else if (connections[1] != null && connections[1] != newConnection) + { + if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < + Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) + { + newNodeIndex = nodes.Count; + } + } + else if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos)) + { + newNodeIndex = nodes.Count; + } + } + + if (newNodeIndex == 0 && nodes.Count > 1) + { + nodes.Insert(0, nodePos); + } + else + { + nodes.Add(nodePos); + } + } + public override void Equip(Character character) { if (shouldClearConnections) { ClearConnections(character); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 012548826..85e995e5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -67,7 +67,13 @@ namespace Barotrauma.Items.Components { return GameMain.GameSession?.RoundDuration ?? 0.0f; } - } + } + + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public bool ApplyEffectsToCharactersInsideSub { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public bool MoveOutsideSub { get; set; } private readonly LevelTrigger.TriggererType triggeredBy; private readonly HashSet triggerers = new HashSet(); @@ -124,7 +130,7 @@ namespace Barotrauma.Items.Components PhysicsBody.FarseerBody.SetIsSensor(true); PhysicsBody.FarseerBody.OnCollision += OnCollision; PhysicsBody.FarseerBody.OnSeparation += OnSeparation; - RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.radius); + RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.Radius); } public override void OnMapLoaded() @@ -137,7 +143,7 @@ namespace Barotrauma.Items.Components private bool OnCollision(Fixture sender, Fixture other, Contact contact) { if (!(LevelTrigger.GetEntity(other) is Entity entity)) { return false; } - if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (true, item.Submarine))) { return false; } + if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; } triggerers.Add(entity); return true; } @@ -162,6 +168,15 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + if (item.Submarine != null && MoveOutsideSub) + { + item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition), item.Rotation); + item.CurrentHull = null; + item.Submarine = null; + PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f); + PhysicsBody.Submarine = item.Submarine; + } + LevelTrigger.RemoveInActiveTriggerers(PhysicsBody, triggerers); if (triggerOnce) @@ -201,6 +216,13 @@ namespace Barotrauma.Items.Components else if (triggerer is Submarine submarine) { LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime); + foreach (Character c2 in Character.CharacterList) + { + if (c2.Submarine == submarine) + { + LevelTrigger.ApplyAttacks(attacks, c2, item.WorldPosition, deltaTime); + } + } } if (Math.Abs(Force) < 0.01f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 221ce222a..263bb9f9a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -59,8 +59,9 @@ namespace Barotrauma.Items.Components private float aiTargetingGraceTimer; private float aiFindTargetTimer; - private Character currentTarget; - const float aiFindTargetInterval = 5.0f; + private ISpatialEntity currentTarget; + private const float CrewAiFindTargetMaxInterval = 3.0f; + private const float CrewAIFindTargetMinInverval = 0.2f; private int currentLoaderIndex; @@ -73,6 +74,8 @@ namespace Barotrauma.Items.Components private List lightComponents; + private readonly bool isSlowTurret; + public float Rotation { get { return rotation; } @@ -317,6 +320,42 @@ namespace Barotrauma.Items.Components private set; } + [Serialize(false, IsPropertySaveable.Yes, description:"Should the turret operate automatically using AI targeting? Comes with some optional random movement that can be adjusted below."), Editable] + public bool AutoOperate { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How much the turret should adjust the aim off the target randomly instead of tracking the target perfectly? In Degrees."), Editable] + public float RandomAimAmount { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Minimum wait time, in seconds."), Editable] + public float RandomAimMinTime { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Maximum wait time, in seconds."), Editable] + public float RandomAimMaxTime { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret move randomly while idle?"), Editable] + public bool RandomMovement { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret have a delay while targeting targets or always aim prefectly?"), Editable] + public bool AimDelay { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target characters in general?"), Editable] + public bool TargetCharacters { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all monsters?"), Editable] + public bool TargetMonsters { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all humans (or creatures in the same group, like pets)?"), Editable] + public bool TargetHumans { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target other submarines?"), Editable] + public bool TargetSubmarines { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target items?"), Editable] + public bool TargetItems { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."), Editable] + public Identifier FriendlyTag { get; private set; } + public Turret(Item item, ContentXElement element) : base(item, element) { @@ -346,6 +385,7 @@ namespace Barotrauma.Items.Components } item.IsShootable = true; item.RequireAimToUse = false; + isSlowTurret = item.HasTag("slowturret"); InitProjSpecific(element); } @@ -560,6 +600,11 @@ namespace Barotrauma.Items.Components } UpdateLightComponents(); + + if (AutoOperate) + { + UpdateAutoOperate(deltaTime); + } } public void UpdateLightComponents() @@ -658,13 +703,20 @@ namespace Barotrauma.Items.Components loaderBroken = true; continue; } - ItemContainer projectileContainer = linkedItem.GetComponent(); + if (tryUseProjectileContainer(linkedItem)) { break; } + } + tryUseProjectileContainer(item); + + bool tryUseProjectileContainer(Item containerItem) + { + ItemContainer projectileContainer = containerItem.GetComponent(); if (projectileContainer != null) { - linkedItem.Use(deltaTime, null); + containerItem.Use(deltaTime, null); projectiles = GetLoadedProjectiles(); - if (projectiles.Any()) { break; } + if (projectiles.Any()) { return true; } } + return false; } } if (projectiles.Count == 0 && !LaunchWithoutProjectile) @@ -833,9 +885,21 @@ namespace Barotrauma.Items.Components } float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f); - projectile.SetTransform( - ConvertUnits.ToSimUnits(GetRelativeFiringPosition()), - -(launchRotation ?? rotation) + spread); + + Vector2 launchPos = ConvertUnits.ToSimUnits(GetRelativeFiringPosition()); + + //check if there's some other sub between the turret's origin and the launch pos, + //and if so, launch at the intersection of the turret and the sub to prevent the projectile from spawning inside the other sub + Body pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), launchPos, null, Physics.CollisionWall, allowInsideFixture: true, + customPredicate: (Fixture f) => + { + return f.Body.UserData is not Submarine sub || sub != item.Submarine; + }); + if (pickedBody != null) + { + launchPos = Submarine.LastPickedPosition; + } + projectile.SetTransform(launchPos, -(launchRotation ?? rotation) + spread); projectile.UpdateTransform(); projectile.Submarine = projectile.body?.Submarine; @@ -895,17 +959,21 @@ namespace Barotrauma.Items.Components } private float waitTimer; - private float disorderTimer; + private float randomAimTimer; private float prevTargetRotation; private float updateTimer; private bool updatePending; - public void ThalamusOperate(WreckAI ai, float deltaTime, bool targetHumans, bool targetOtherCreatures, bool targetSubmarines, bool ignoreDelay) - { - if (ai == null) { return; } + public void UpdateAutoOperate(float deltaTime, Identifier friendlyTag = default) + { IsActive = true; + if (friendlyTag.IsEmpty) + { + friendlyTag = FriendlyTag; + } + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; @@ -924,7 +992,7 @@ namespace Barotrauma.Items.Components updateTimer -= deltaTime; } - if (!ignoreDelay && waitTimer > 0) + if (AimDelay && waitTimer > 0) { waitTimer -= deltaTime; return; @@ -934,40 +1002,48 @@ namespace Barotrauma.Items.Components float shootDistance = AIRange; ISpatialEntity target = null; float closestDist = shootDistance * shootDistance; - if (targetHumans || targetOtherCreatures) + if (TargetCharacters) { foreach (var character in Character.CharacterList) { - if (character == null || character.Removed || character.IsDead) { continue; } - if (character.Params.Group == ai.Config.Entity) { continue; } - bool isHuman = character.IsHuman || character.Params.Group == CharacterPrefab.HumanSpeciesName; - if (isHuman) - { - if (!targetHumans) - { - // Don't target humans if not defined to. - continue; - } - } - else if (!targetOtherCreatures) - { - // Don't target other creatures if not defined to. - continue; - } + if (!IsValidTarget(character)) { continue; } + float priority = isSlowTurret ? character.Params.AISlowTurretPriority : character.Params.AITurretPriority; + if (priority <= 0) { continue; } + if (!IsValidTargetForAutoOperate(character, friendlyTag)) { continue; } float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } + if (!CheckTurretAngle(character.WorldPosition)) { continue; } target = character; - closestDist = dist; + closestDist = dist / priority; } } - if (targetSubmarines) + if (TargetItems) + { + foreach (Item targetItem in Item.ItemList) + { + if (!IsValidTarget(targetItem)) { continue; } + float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority; + if (priority <= 0) { continue; } + float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); + if (dist > closestDist) { continue; } + if (dist > shootDistance * shootDistance) { continue; } + if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + target = targetItem; + closestDist = dist / priority; + } + } + if (TargetSubmarines) { if (target == null || target.Submarine != null) { closestDist = maxDistance * maxDistance; foreach (Submarine sub in Submarine.Loaded) { - if (sub.Info.Type != SubmarineType.Player) { continue; } + if (sub == Item.Submarine) { continue; } + if (item.Submarine != null) + { + if (Character.IsOnFriendlyTeam(item.Submarine.TeamID, sub.TeamID)) { continue; } + } float dist = Vector2.DistanceSquared(sub.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } closestSub = sub; @@ -981,34 +1057,41 @@ namespace Barotrauma.Items.Components if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; } float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } + // Don't check the angle, because it doesn't work on Thalamus spike. The angle check wouldn't be very important here anyway. target = hull; closestDist = dist; } } } } - if (!ignoreDelay) + + if (target == null && RandomMovement) { - if (target == null) + // Random movement while there's no target + waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f); + targetRotation = Rand.Range(minRotation, maxRotation); + updatePending = true; + return; + } + + if (AimDelay) + { + if (RandomAimAmount > 0) { - // Random movement - waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f); - targetRotation = Rand.Range(minRotation, maxRotation); - updatePending = true; - return; - } - if (disorderTimer < 0) - { - // Random disorder - disorderTimer = Rand.Range(0f, 3f); - waitTimer = Rand.Range(0.25f, 1f); - targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-1f, 1f)); - updatePending = true; - return; - } - else - { - disorderTimer -= deltaTime; + if (randomAimTimer < 0) + { + // Random disorder or other flaw in the targeting. + randomAimTimer = Rand.Range(RandomAimMinTime, RandomAimMaxTime); + waitTimer = Rand.Range(0.25f, 1f); + float randomAim = MathHelper.ToRadians(RandomAimAmount); + targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-randomAim, randomAim)); + updatePending = true; + return; + } + else + { + randomAimTimer -= deltaTime; + } } } if (target == null) { return; } @@ -1043,11 +1126,11 @@ namespace Barotrauma.Items.Components start -= target.Submarine.SimPosition; end -= target.Submarine.SimPosition; Body transformedTarget = CheckLineOfSight(start, end); - shoot = CanShoot(transformedTarget, user: null, ai, targetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, ai, targetSubmarines)); + shoot = CanShoot(transformedTarget, user: null, friendlyTag, TargetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines)); } else { - shoot = CanShoot(worldTarget, user: null, ai, targetSubmarines); + shoot = CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines); } if (shoot) { @@ -1055,7 +1138,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead) { @@ -1205,18 +1288,19 @@ namespace Barotrauma.Items.Components bool hadCurrentTarget = currentTarget != null; if (hadCurrentTarget) { - if (currentTarget.Removed || currentTarget.IsDead) + if (!IsValidTarget(currentTarget)) { currentTarget = null; + aiFindTargetTimer = CrewAIFindTargetMinInverval; } } - - if (aiFindTargetTimer <= 0.0f || currentTarget == null) + if (aiFindTargetTimer <= 0.0f) { foreach (Character enemy in Character.CharacterList) { - // Ignore dead, friendly, and those that are inside the same sub - if (enemy.IsDead || !enemy.Enabled) { continue; } + if (!IsValidTarget(enemy)) { continue; } + float priority = isSlowTurret ? enemy.Params.AISlowTurretPriority : enemy.Params.AITurretPriority; + if (priority <= 0) { continue; } if (character.Submarine != null) { if (enemy.Submarine == character.Submarine) { continue; } @@ -1233,30 +1317,53 @@ namespace Barotrauma.Items.Components // We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at. if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } } + targetPos = enemy.WorldPosition; closestEnemy = enemy; - closestDistance = dist; + closestDistance = dist / priority; + currentTarget = closestEnemy; } - currentTarget = closestEnemy; - aiFindTargetTimer = aiFindTargetInterval; - } - else - { - closestEnemy = currentTarget; - } - - if (closestEnemy != null) - { - targetPos = closestEnemy.WorldPosition; - //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is - if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine && !closestEnemy.CanSeeTarget(Item)) + foreach (Item targetItem in Item.ItemList) { - targetPos = closestEnemy.CurrentHull.WorldPosition; + if (!IsValidTarget(targetItem)) { continue; } + float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority; + if (priority <= 0) { continue; } + float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); + if (dist > closestDistance) { continue; } + if (dist > shootDistance * shootDistance) { continue; } + if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + targetPos = targetItem.WorldPosition; + closestDistance = dist / priority; + // Override the target character so that we can target the item instead. + closestEnemy = null; + currentTarget = targetItem; + } + if (currentTarget == null) + { + aiFindTargetTimer = CrewAIFindTargetMinInverval; + } + else + { + aiFindTargetTimer = CrewAiFindTargetMaxInterval; + } + } + else if (currentTarget != null) + { + targetPos = currentTarget.WorldPosition; + } + bool iceSpireSpotted = false; + // Adjust the target character position (limb or submarine) + if (currentTarget is Character targetCharacter) + { + //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is + if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item)) + { + targetPos = targetCharacter.CurrentHull.WorldPosition; } else { // Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head. float closestDist = closestDistance; - foreach (Limb limb in closestEnemy.AnimController.Limbs) + foreach (Limb limb in targetCharacter.AnimController.Limbs) { if (limb.IsSevered) { continue; } if (limb.Hidden) { continue; } @@ -1270,13 +1377,14 @@ namespace Barotrauma.Items.Components } if (closestDist > shootDistance * shootDistance) { - // Not close enough to shoot + // Not close enough to shoot. + currentTarget = null; closestEnemy = null; targetPos = null; } } } - else if (item.Submarine != null && Level.Loaded != null) + else if (targetPos == null && item.Submarine != null && Level.Loaded != null) { // Check ice spires shootDistance = AIRange * item.OffsetOnSelectedMultiplier; @@ -1286,50 +1394,49 @@ namespace Barotrauma.Items.Components if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; } foreach (var cell in wall.Cells) { - if (cell.DoesDamage) + if (!cell.DoesDamage) { continue; } + foreach (var edge in cell.Edges) { - foreach (var edge in cell.Edges) + Vector2 p1 = edge.Point1 + cell.Translation; + Vector2 p2 = edge.Point2 + cell.Translation; + Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition); + if (!CheckTurretAngle(closestPoint)) { - Vector2 p1 = edge.Point1 + cell.Translation; - Vector2 p2 = edge.Point2 + cell.Translation; - Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition); - if (!CheckTurretAngle(closestPoint)) + // The closest point can't be targeted -> get a point directly in front of the turret + Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) { - // The closest point can't be targeted -> get a point directly in front of the turret - Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); - if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) - { - closestPoint = intersection; - if (!CheckTurretAngle(closestPoint)) { continue; } - } - else - { - continue; - } + closestPoint = intersection; + if (!CheckTurretAngle(closestPoint)) { continue; } } - float dist = Vector2.Distance(closestPoint, item.WorldPosition); - - //add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell - closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1); - - if (dist > AIRange + 1000) { continue; } - float dot = 0; - if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero)) + else { - dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition)); - } - float minAngle = 0.5f; - if (dot < minAngle && dist > 1000) - { - // The sub is not moving towards the target and it's not very close to the turret either -> ignore continue; } - // Allow targeting farther when heading towards the spire (up to 1000 px) - dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); - if (dist > closestDistance) { continue; } - targetPos = closestPoint; - closestDistance = dist; } + float dist = Vector2.Distance(closestPoint, item.WorldPosition); + + //add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell + closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1); + + if (dist > AIRange + 1000) { continue; } + float dot = 0; + if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero)) + { + dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition)); + } + float minAngle = 0.5f; + if (dot < minAngle && dist > 1000) + { + // The sub is not moving towards the target and it's not very close to the turret either -> ignore + continue; + } + // Allow targeting farther when heading towards the spire (up to 1000 px) + dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); + if (dist > closestDistance) { continue; } + targetPos = closestPoint; + closestDistance = dist; + iceSpireSpotted = true; } } } @@ -1345,13 +1452,13 @@ namespace Barotrauma.Items.Components { if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget) { - if (CreatureMetrics.Instance.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman) + if (CreatureMetrics.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman) { character.Speak(TextManager.Get("DialogNewTargetSpotted").Value, identifier: "newtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } - else if (CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) + else if (CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName)) { character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName).Value, identifier: "identifiedtargetspotted".ToIdentifier(), @@ -1364,17 +1471,17 @@ namespace Barotrauma.Items.Components minDurationBetweenSimilar: 5.0f); } } - else if (!CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) + else if (!CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName)) { character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value, identifier: "unidentifiedtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 5.0f); } - character.AddEncounter(closestEnemy); + CreatureMetrics.AddEncounter(closestEnemy.SpeciesName); } character.AIController.SelectTarget(closestEnemy.AiTarget); } - else if (closestEnemy == null && character.IsOnPlayerTeam) + else if (iceSpireSpotted && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogIceSpireSpotted").Value, identifier: "icespirespotted".ToIdentifier(), @@ -1437,7 +1544,55 @@ namespace Barotrauma.Items.Components return 0; } - private bool CanShoot(Body targetBody, Character user = null, WreckAI ai = null, bool targetSubmarines = true) + // Not exahustive, but helps to get rid of some code duplication + private static bool IsValidTarget(ISpatialEntity target) + { + if (target == null) { return false; } + if (target is Character targetCharacter) + { + if (!targetCharacter.Enabled || targetCharacter.Removed || targetCharacter.IsDead || targetCharacter.AITurretPriority <= 0) + { + return false; + } + } + else if (target is Item targetItem) + { + if (targetItem.Removed || targetItem.Condition <= 0 || !targetItem.Prefab.IsAITurretTarget || targetItem.Prefab.AITurretPriority <= 0 || targetItem.HiddenInGame) + { + return false; + } + if (targetItem.Submarine != null) + { + return false; + } + } + return true; + } + + private bool IsValidTargetForAutoOperate(Character target, Identifier friendlyTag) + { + if (!friendlyTag.IsEmpty) + { + if (target.SpeciesName.Equals(friendlyTag) || target.Group.Equals(friendlyTag)) { return false; } + } + bool isHuman = target.IsHuman || target.Group == CharacterPrefab.HumanSpeciesName; + if (isHuman) + { + if (item.Submarine != null) + { + // Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP). + return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans; + } + return TargetHumans; + } + else + { + // Shouldn't check the team here, because all the enemies are in the same team (None). + return TargetMonsters; + } + } + + private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true) { if (targetBody == null) { return false; } Character targetCharacter = null; @@ -1449,7 +1604,7 @@ namespace Barotrauma.Items.Components { targetCharacter = limb.character; } - if (targetCharacter != null) + if (targetCharacter != null && !targetCharacter.Removed) { if (user != null) { @@ -1458,27 +1613,25 @@ namespace Barotrauma.Items.Components return false; } } - if (ai != null) + else if (!IsValidTargetForAutoOperate(targetCharacter, friendlyTag)) { - if (targetCharacter.Params.Group == ai.Config.Entity) - { - return false; - } + // Note that Thalamus runs this even when AutoOperate is false. + return false; } } else { if (targetBody.UserData is ISpatialEntity e) { - if (e is Structure s && s.Indestructible) { return false; } - Submarine sub = e.Submarine ?? e as Submarine; + if (e is Structure { Indestructible: true }) { return false; } if (!targetSubmarines && e is Submarine) { return false; } - if (sub == null) { return false; } + Submarine sub = e.Submarine ?? e as Submarine; + if (sub == null) { return true; } if (sub == Item.Submarine) { return false; } if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; } if (sub.TeamID == Item.Submarine.TeamID) { return false; } } - else if (!(targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)) + else if (targetBody.UserData is not Voronoi2.VoronoiCell { IsDestructible: true }) { // Hit something else, probably a level wall return false; @@ -1489,7 +1642,7 @@ namespace Barotrauma.Items.Components private Body CheckLineOfSight(Vector2 start, Vector2 end) { - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; + var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionProjectile; Body pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, customPredicate: (Fixture f) => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index bd06a6ed3..c5a73d53f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -288,7 +288,7 @@ namespace Barotrauma.Items.Components public bool AutoEquipWhenFull { get; private set; } public bool DisplayContainedStatus { get; private set; } - [Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + [Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn.")] public bool AllowUseWhenWorn { get; set; } public readonly int Variants; @@ -527,14 +527,17 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (picker.Removed) + if (picker == null || picker.Removed) { IsActive = false; return; } - item.SetTransform(picker.SimPosition, 0.0f); - + //if the item is also being held, let the Holdable component control the position + if (item.GetComponent() is not { IsActive: true }) + { + item.SetTransform(picker.SimPosition, 0.0f); + } item.ApplyStatusEffects(ActionType.OnWearing, deltaTime, picker); #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index c31f6c337..5a08c3838 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -141,7 +141,18 @@ namespace Barotrauma } } if (items.Contains(item)) { return; } - items.Add(item); + + //keep lowest-condition items at the top of the stack + int index = 0; + for (int i = 0; i < items.Count; i++) + { + if (items[i].Condition > item.Condition) + { + break; + } + index++; + } + items.Insert(index, item); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 346f772dd..b4b5ef815 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -99,7 +99,18 @@ namespace Barotrauma private bool hasComponentsToDraw; public PhysicsBody body; - private float waterDragCoefficient; + private readonly float originalWaterDragCoefficient; + private float? overrideWaterDragCoefficient; + public float WaterDragCoefficient + { + get => overrideWaterDragCoefficient ?? originalWaterDragCoefficient; + set => overrideWaterDragCoefficient = value; + } + + /// + /// Removes the override value -> falls back to using the original value defined in the xml. + /// + public void ResetWaterDragCoefficient() => overrideWaterDragCoefficient = null; public readonly XElement StaticBodyConfig; @@ -142,6 +153,8 @@ namespace Barotrauma private readonly bool[] hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; private readonly Dictionary> statusEffectLists; + public Action OnInteract; + public Dictionary SerializableProperties { get; protected set; } private bool? hasInGameEditableProperties; @@ -423,8 +436,6 @@ namespace Barotrauma } } - public Color? HighlightColor; - /// /// Can be used by status effects or conditionals to check whether the item is contained inside something /// @@ -459,6 +470,8 @@ namespace Barotrauma } } + public Color? HighlightColor; + [Serialize("", IsPropertySaveable.Yes)] /// @@ -471,7 +484,8 @@ namespace Barotrauma { if (AiTarget != null) { - AiTarget.SonarLabel = !string.IsNullOrEmpty(value) && value.Length > 200 ? value.Substring(200) : value; + string trimmedStr = !string.IsNullOrEmpty(value) && value.Length > 250 ? value.Substring(250) : value; + AiTarget.SonarLabel = TextManager.Get(trimmedStr).Fallback(trimmedStr); } } } @@ -637,11 +651,13 @@ namespace Barotrauma } } - [Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] - public bool AllowStealing + private bool allowStealing; + [Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true, + description: $"Determined by where/how the item originally spawned. If ItemPrefab.AllowStealing is true, stealing the item is always allowed.")] + public bool AllowStealing { - get; - set; + get { return allowStealing || Prefab.AllowStealingAlways; } + set { allowStealing = value; } } private string originalOutpost; @@ -948,7 +964,7 @@ namespace Barotrauma { if (!Physics.TryParseCollisionCategory(collisionCategoryStr, out Category cat)) { - DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategoryStr + ")"); + DebugConsole.ThrowError("Invalid collision category in item \"" + Name + "\" (" + collisionCategoryStr + ")"); } else { @@ -987,6 +1003,7 @@ namespace Barotrauma case "infectedsprite": case "damagedinfectedsprite": case "swappableitem": + case "skillrequirementhint": break; case "staticbody": StaticBodyConfig = subElement; @@ -1055,8 +1072,7 @@ namespace Barotrauma if (body != null) { body.Submarine = submarine; - waterDragCoefficient = bodyElement.GetAttributeFloat("waterdragcoefficient", - GetComponent() != null || GetComponent() != null ? 0.1f : 1.0f); + originalWaterDragCoefficient = bodyElement.GetAttributeFloat("waterdragcoefficient", 5.0f); } //cache connections into a dictionary for faster lookups @@ -1600,7 +1616,7 @@ namespace Barotrauma public bool ConditionalMatches(PropertyConditional conditional) { - if (string.IsNullOrEmpty(conditional.TargetItemComponentName)) + if (string.IsNullOrEmpty(conditional.TargetItemComponent)) { if (!conditional.Matches(this)) { return false; } } @@ -1608,7 +1624,7 @@ namespace Barotrauma { foreach (ItemComponent component in components) { - if (component.Name != conditional.TargetItemComponentName) { continue; } + if (component.Name != conditional.TargetItemComponent) { continue; } if (!conditional.Matches(component)) { return false; } } } @@ -1653,7 +1669,7 @@ namespace Barotrauma if (effect.TargetSlot > -1) { - if (OwnInventory.FindIndex(containedItem) != effect.TargetSlot) { continue; } + if (!OwnInventory.GetItemsAt(effect.TargetSlot).Contains(containedItem)) { continue; } } hasTargets = true; @@ -1708,8 +1724,15 @@ namespace Barotrauma { targets.AddRange(character.AnimController.Limbs.ToList()); } + if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb == null && effect.targetLimbs != null) + { + foreach (var characterLimb in character.AnimController.Limbs) + { + if (effect.targetLimbs.Contains(characterLimb.type)) { targets.Add(characterLimb); } + } + } } - if (effect.HasTargetType(StatusEffect.TargetType.Limb)) + if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb != null) { targets.Add(limb); } @@ -1724,7 +1747,7 @@ namespace Barotrauma { if (Indestructible || InvulnerableToDamage) { return new AttackResult(); } - float damageAmount = attack.GetItemDamage(deltaTime); + float damageAmount = attack.GetItemDamage(deltaTime, Prefab.ItemDamageMultiplier); Condition -= damageAmount; if (damageAmount >= Prefab.OnDamagedThreshold) @@ -1735,7 +1758,7 @@ namespace Barotrauma return new AttackResult(damageAmount, null); } - private void SetCondition(float value, bool isNetworkEvent) + private void SetCondition(float value, bool isNetworkEvent, bool executeEffects = true) { if (!isNetworkEvent) { @@ -1758,16 +1781,22 @@ namespace Barotrauma //Flag connections to be updated as device is broken flagChangedConnections(connections); #if CLIENT - foreach (ItemComponent ic in components) - { - ic.PlaySound(ActionType.OnBroken); - ic.StopSounds(ActionType.OnActive); + if (executeEffects) + { + foreach (ItemComponent ic in components) + { + ic.PlaySound(ActionType.OnBroken); + ic.StopSounds(ActionType.OnActive); + } } if (Screen.Selected == GameMain.SubEditorScreen) { return; } #endif // Have to set the previous condition here or OnBroken status effects that reduce the condition will keep triggering the status effects, resulting in a stack overflow. SetPreviousCondition(); - ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); + if (executeEffects) + { + ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); + } } else if (condition > 0.0f && prevCondition <= 0.0f) { @@ -1851,15 +1880,15 @@ namespace Barotrauma if (!(GameMain.NetworkMember is { IsServer: true })) { return; } if (!conditionUpdatePending) { return; } - CreateStatusEvent(); + CreateStatusEvent(loadingRound: false); lastSentCondition = condition; sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; conditionUpdatePending = false; } - public void CreateStatusEvent() + public void CreateStatusEvent(bool loadingRound) { - GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound)); } private bool isActive = true; @@ -1901,7 +1930,7 @@ namespace Barotrauma if (ic.IsActiveConditionals != null) { - if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And) + if (ic.IsActiveConditionalComparison == PropertyConditional.LogicalOperatorType.And) { bool shouldBeActive = true; foreach (var conditional in ic.IsActiveConditionals) @@ -1972,7 +2001,10 @@ namespace Barotrauma if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f || transformDirty) { - UpdateTransform(); + if (body.CollisionCategories != Category.None) + { + UpdateTransform(); + } if (CurrentHull == null && Level.Loaded != null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth)) { Spawner?.AddItemToRemoveQueue(this); @@ -1991,8 +2023,7 @@ namespace Barotrauma if (needsWaterCheck) { bool wasInWater = inWater; - inWater = IsInWater(); - bool waterProof = WaterProof; + inWater = IsInWater() && !WaterProof; if (inWater) { //the item has gone through the surface of the water @@ -2007,15 +2038,19 @@ namespace Barotrauma } Item container = this.Container; - while (!waterProof && container != null) + while (container != null) { - waterProof = container.WaterProof; + if (container.WaterProof) + { + inWater = false; + break; + } container = container.Container; } } if (hasWaterStatusEffects && condition > 0.0f) { - ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); + ApplyStatusEffects(inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); } } else @@ -2141,7 +2176,7 @@ namespace Barotrauma Vector2 frontVel = body.FarseerBody.GetLinearVelocityFromLocalPoint(localFront); float speed = frontVel.Length(); - float drag = speed * speed * waterDragCoefficient * volume * Physics.NeutralDensity; + float drag = speed * speed * WaterDragCoefficient * volume * Physics.NeutralDensity; //very small drag on active projectiles to prevent affecting their trajectories much if (body.FarseerBody.IsBullet) { drag *= 0.1f; } Vector2 dragVec = -frontVel / speed * drag; @@ -2628,12 +2663,14 @@ namespace Barotrauma if (user == Character.Controlled) { GUI.ForceMouseOn(null); } if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; } #endif - if (ic.CanBeSelected && !(ic is Door)) { selected = true; } + if (ic.CanBeSelected && ic is not Door) { selected = true; } } } if (!picked) { return false; } + OnInteract?.Invoke(); + if (user != null) { if (user.SelectedItem == this) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index 4ad6b2731..3130414a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -82,6 +82,13 @@ namespace Barotrauma private readonly struct ItemStatusEventData : IEventData { public EventType EventType => EventType.Status; + + public readonly bool LoadingRound; + + public ItemStatusEventData(bool loadingRound) + { + LoadingRound = loadingRound; + } } private readonly struct AssignCampaignInteractionEventData : IEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 761e84d24..5c1c5c573 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -1,16 +1,33 @@ -using Barotrauma.IO; +using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Barotrauma.Extensions; using System.Security.Cryptography; using System.Xml.Linq; namespace Barotrauma { + readonly struct SkillRequirementHint + { + public readonly Identifier Skill; + public readonly float Level; + public readonly LocalizedString SkillName; + + public LocalizedString GetFormattedText(int skillLevel, string levelColorTag) => + $"{SkillName} {Level} (‖color:{levelColorTag}‖{skillLevel}‖color:end‖)"; + + public SkillRequirementHint(ContentXElement element) + { + Skill = element.GetAttributeIdentifier("identifier", Identifier.Empty); + Level = element.GetAttributeFloat("level", 0); + SkillName = TextManager.Get("skillname." + Skill); + } + } + readonly struct DeconstructItem { public readonly Identifier ItemIdentifier; @@ -315,6 +332,8 @@ namespace Barotrauma public readonly bool TransferOnlyOnePerContainer; public readonly bool AllowTransfersHere = true; + public readonly float MinLevelDifficulty, MaxLevelDifficulty; + public PreferredContainer(XElement element) { Primary = XMLExtensions.GetAttributeIdentifierArray(element, "primary", Array.Empty()).ToImmutableHashSet(); @@ -330,6 +349,9 @@ namespace Barotrauma TransferOnlyOnePerContainer = element.GetAttributeBool("TransferOnlyOnePerContainer", TransferOnlyOnePerContainer); AllowTransfersHere = element.GetAttributeBool("AllowTransfersHere", AllowTransfersHere); + MinLevelDifficulty = element.GetAttributeFloat(nameof(MinLevelDifficulty), float.MinValue); + MaxLevelDifficulty = element.GetAttributeFloat(nameof(MaxLevelDifficulty), float.MaxValue); + if (element.GetAttribute("spawnprobability") == null) { //if spawn probability is not defined but amount is, assume the probability is 1 @@ -443,6 +465,8 @@ namespace Barotrauma //Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced. public ImmutableArray PreferredContainers { get; private set; } + public ImmutableArray SkillRequirementHints { get; private set; } + public SwappableItem SwappableItem { get; @@ -651,6 +675,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool AllowSellingWhenBroken { get; private set; } + [Serialize(false, IsPropertySaveable.No)] + public bool AllowStealingAlways { get; private set; } + [Serialize(false, IsPropertySaveable.No)] public bool Indestructible { get; private set; } @@ -660,6 +687,9 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.No)] public float ExplosionDamageMultiplier { get; private set; } + [Serialize(1f, IsPropertySaveable.No)] + public float ItemDamageMultiplier { get; private set; } + [Serialize(false, IsPropertySaveable.No)] public bool DamagedByProjectiles { get; private set; } @@ -772,6 +802,18 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.No, description: "How much the bots prioritize this item when they seek for items. For example, bots prioritize less exosuit than the other diving suits. Defaults to 1. Note that there's also a specific CombatPriority for items that can be used as weapons.")] public float BotPriority { get; private set; } + [Serialize(true, IsPropertySaveable.No)] + public bool ShowNameInHealthBar { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description:"Should the bots shoot at this item with turret or not? Disabled by default.")] + public bool IsAITurretTarget { get; private set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with turrets? Defaults to 1. Distance to the target affects the decision making.")] + public float AITurretPriority { get; private set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")] + public float AISlowTurretPriority { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -874,6 +916,15 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, ConfigElement); LoadDescription(ConfigElement); + var skillRequirementHints = new List(); + foreach (var skillRequirementHintElement in ConfigElement.GetChildElements("SkillRequirementHint")) + { + skillRequirementHints.Add(new SkillRequirementHint(skillRequirementHintElement)); + } + if (skillRequirementHints.Any()) + { + SkillRequirementHints = skillRequirementHints.ToImmutableArray(); + } var allowDroppingOnSwapWith = ConfigElement.GetAttributeIdentifierArray("allowdroppingonswapwith", Array.Empty()); AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet(); @@ -1167,7 +1218,10 @@ namespace Barotrauma public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo) { priceInfo = GetPriceInfo(store); - return priceInfo is { CanBeBought: true } && (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; + return + priceInfo is { CanBeBought: true } && + (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty && + (!priceInfo.MinReputation.Any() || priceInfo.MinReputation.Any(p => store?.Location.Faction?.Prefab.Identifier == p.Key || store?.Location.SecondaryFaction?.Prefab.Identifier == p.Key)); } public bool CanBeBoughtFrom(Location location) @@ -1179,6 +1233,15 @@ namespace Barotrauma if (priceInfo == null) { continue; } if (!priceInfo.CanBeBought) { continue; } if (location.LevelData.Difficulty < priceInfo.MinLevelDifficulty) { continue; } + if (priceInfo.MinReputation.Any()) + { + if (!priceInfo.MinReputation.Any(p => + location?.Faction?.Prefab.Identifier == p.Key || + location?.SecondaryFaction?.Prefab.Identifier == p.Key)) + { + continue; + } + } return true; } return false; @@ -1335,11 +1398,43 @@ namespace Barotrauma } public Identifier VariantOf { get; } - + public ItemPrefab ParentPrefab { get; set; } + public void InheritFrom(ItemPrefab parent) { - ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); + ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement, CheckXML).FromPackage(ConfigElement.ContentPackage); ParseConfigElement(parent); + + void CheckXML(XElement originalElement, XElement variantElement, XElement result) + { + if (result == null) { return; } + if (result.Name.ToIdentifier() == "RequiredItem" && + result.Parent?.Name.ToIdentifier() == "Fabricate") + { + int originalAmount = originalElement.GetAttributeInt("amount", 1); + Identifier originalIdentifier = originalElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (variantElement == null) + { + //if the variant defines some fabrication requirements, we probably don't want to inherit anything extra from the base item? + if (this.originalElement.GetChildElement("Fabricate")?.GetChildElement("RequiredItem") != null) + { + DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + + $"the item inherits the fabrication requirement of x{originalAmount} \"{originalIdentifier}\" from the base item \"{parent.Identifier}\". " + + $"If this is not intentional, you can use empty elements in the item variant to remove any excess inherited fabrication requirements."); + } + return; + } + + Identifier resultIdentifier = result.GetAttributeIdentifier("identifier", Identifier.Empty); + if (originalAmount > 1 && variantElement.GetAttribute("amount") == null) + { + DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + + $"the base item \"{parent.Identifier}\" requires x{originalAmount} \"{originalIdentifier}\" to fabricate. " + + $"The variant only overrides the required item, not the amount, resulting in a requirement of x{originalAmount} \"{resultIdentifier}\". "+ + "Specify the amount in the variant to fix this."); + } + } + } } public override string ToString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 41e4f333c..c4647ef25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -8,44 +8,89 @@ using Barotrauma.Extensions; namespace Barotrauma { + /// + /// Used by various features to define different kinds of relations between items: + /// for example, which item a character must have equipped to interact with some item in some way, + /// which items can go inside a container, or which kind of item the target of a status effect must have for the effect to execute. + /// class RelatedItem { public enum RelationType { None, + /// + /// The item must be contained inside the item this relation is defined in. + /// Can for example by used to make an item usable only when there's a specific kind of item inside it. + /// Contained, + /// + /// The user must have equipped the item (i.e. held or worn). + /// Equipped, + /// + /// The user must have picked up the item (i.e. the item needs to be in the user's inventory). + /// Picked, - Container + /// + /// The item this relation is defined in must be inside a specific kind of container. + /// Can for example by used to make an item do something when it's inside some other type of item. + /// + Container, + /// + /// Signifies an error (type could not be parsed) + /// + Invalid } - public bool IsOptional { get; set; } - + /// + /// Should an empty inventory be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it. + /// public bool MatchOnEmpty { get; set; } + /// + /// Should only an empty inventory be considered valid? Can be used to, for example, make an item do something when there's nothing inside it. + /// public bool RequireEmpty { get; set; } + /// + /// Only valid for the RequiredItems of an ItemComponent. Can be used to ignore the requirement in the submarine editor, + /// making it easier to for example make rewire things that require some special tool to rewire. + /// public bool IgnoreInEditor { get; set; } + /// + /// Identifier(s) or tag(s) of the items that are NOT considered valid. + /// Can be used to, for example, exclude some specific items when using tags that apply to multiple items. + /// public ImmutableHashSet ExcludedIdentifiers { get; private set; } - private RelationType type; + private readonly RelationType type; - public List statusEffects; - + public List StatusEffects = new List(); + + /// + /// Only valid for the RequiredItems of an ItemComponent. A message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel). + /// public LocalizedString Msg; + + /// + /// Only valid for the RequiredItems of an ItemComponent. The localization tag of a message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel). + /// public Identifier MsgTag; /// - /// Should broken (0 condition) items be excluded + /// Should broken (0 condition) items be excluded? /// public bool ExcludeBroken { get; private set; } /// - /// Should full condition (100%) items be excluded + /// Should full condition (100%) items be excluded? /// public bool ExcludeFullCondition { get; private set; } + /// + /// Are item variants considered valid? + /// public bool AllowVariants { get; private set; } = true; public RelationType Type @@ -59,19 +104,34 @@ namespace Barotrauma public int TargetSlot = -1; /// - /// Overrides the position defined in ItemContainer. + /// Overrides the position defined in ItemContainer. Only valid when used in the Containable definitions of an ItemContainer. /// public Vector2? ItemPos; /// - /// Only affects when ItemContainer.hideItems is false. Doesn't override the value. + /// Only valid when used in the Containable definitions of an ItemContainer. + /// Only affects when ItemContainer.hideItems is false. Doesn't override the value. /// - public bool? Hide; + public bool Hide; + /// + /// Only valid when used in the Containable definitions of an ItemContainer. + /// Can be used to override the rotation of specific items in the container. + /// public float Rotation; + /// + /// Only valid when used in the Containable definitions of an ItemContainer. + /// Can be used to force specific items to stay active inside the container (such as flashlights attached to a gun). + /// public bool SetActive; + /// + /// Only valid for the RequiredItems of an ItemComponent. Can be used to make the requirement optional, + /// meaning that you don't need to have the item to interact with something, but having it may still affect what the interaction does (such as using a crowbar on a door). + /// + public bool IsOptional { get; set; } + public string JoinedIdentifiers { get { return string.Join(",", Identifiers); } @@ -83,6 +143,9 @@ namespace Barotrauma } } + /// + /// Identifier(s) or tag(s) of the items that are considered valid. + /// public ImmutableHashSet Identifiers { get; private set; } public string JoinedExcludedIdentifiers @@ -139,8 +202,121 @@ namespace Barotrauma { this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); this.ExcludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet(); + } + + public RelatedItem(ContentXElement element, string parentDebugName) + { + Identifier[] identifiers; + if (element.GetAttribute("name") != null) + { + //backwards compatibility + a console warning + DebugConsole.ThrowError($"Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names."); + Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty()); + //attempt to convert to identifiers and tags + List convertedIdentifiers = new List(); + foreach (Identifier itemName in itemNames) + { + var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName.Value); + if (matchingItem != null) + { + convertedIdentifiers.Add(matchingItem.Identifier); + } + else + { + //no matching item found, this must be a tag + convertedIdentifiers.Add(itemName); + } + } + identifiers = convertedIdentifiers.ToArray(); + } + else + { + identifiers = element.GetAttributeIdentifierArray("items", null) ?? element.GetAttributeIdentifierArray("item", null); + if (identifiers == null) + { + identifiers = element.GetAttributeIdentifierArray("identifiers", null) ?? element.GetAttributeIdentifierArray("tags", null); + if (identifiers == null) + { + identifiers = element.GetAttributeIdentifierArray("identifier", null) ?? element.GetAttributeIdentifierArray("tag", Array.Empty()); + } + } + } + this.Identifiers = identifiers.ToImmutableHashSet(); + + Identifier[] excludedIdentifiers = element.GetAttributeIdentifierArray("excludeditems", null) ?? element.GetAttributeIdentifierArray("excludeditem", null); + if (excludedIdentifiers == null) + { + excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifiers", null) ?? element.GetAttributeIdentifierArray("excludedtags", null); + if (excludedIdentifiers == null) + { + excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifier", null) ?? element.GetAttributeIdentifierArray("excludedtag", Array.Empty()); + } + } + this.ExcludedIdentifiers = excludedIdentifiers.ToImmutableHashSet(); + + ExcludeBroken = element.GetAttributeBool("excludebroken", true); + RequireEmpty = element.GetAttributeBool("requireempty", false); + ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false); + AllowVariants = element.GetAttributeBool("allowvariants", true); + Rotation = element.GetAttributeFloat("rotation", 0f); + SetActive = element.GetAttributeBool("setactive", false); + + if (element.GetAttribute(nameof(Hide)) != null) + { + Hide = element.GetAttributeBool(nameof(Hide), false); + } + if (element.GetAttribute(nameof(ItemPos)) != null) + { + ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero); + } + string typeStr = element.GetAttributeString("type", ""); + if (string.IsNullOrEmpty(typeStr)) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "containable": + typeStr = "Contained"; + break; + case "suitablefertilizer": + case "suitableseed": + typeStr = "None"; + break; + } + } + if (!Enum.TryParse(typeStr, true, out type)) + { + DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type."); + type = RelationType.Invalid; + } + + MsgTag = element.GetAttributeIdentifier("msg", Identifier.Empty); + LocalizedString msg = TextManager.Get(MsgTag); + if (!msg.Loaded) + { + Msg = MsgTag.Value; + } + else + { +#if CLIENT + foreach (InputType inputType in Enum.GetValues(typeof(InputType))) + { + msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType)); + } + Msg = msg; +#endif + } + + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } + StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); + } + + IsOptional = element.GetAttributeBool("optional", false); + IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false); + MatchOnEmpty = element.GetAttributeBool("matchonempty", false); + TargetSlot = element.GetAttributeInt("targetslot", -1); - statusEffects = new List(); } public bool CheckRequirements(Character character, Item parentItem) @@ -197,11 +373,14 @@ namespace Barotrauma bool isEmpty = parentItem.OwnInventory.IsEmpty(); if (RequireEmpty && !isEmpty) { return false; } if (MatchOnEmpty && isEmpty) { return true; } - foreach (Item contained in parentItem.ContainedItems) + foreach (var container in parentItem.GetComponents()) { - if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } - if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; } - if (CheckContained(contained)) { return true; } + foreach (Item contained in container.Inventory.AllItems) + { + if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } + if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; } + if (CheckContained(contained)) { return true; } + } } return false; } @@ -221,9 +400,9 @@ namespace Barotrauma new XAttribute("rotation", Rotation), new XAttribute("setactive", SetActive)); - if (Hide.HasValue) + if (Hide) { - element.Add(new XAttribute(nameof(Hide), Hide.Value)); + element.Add(new XAttribute(nameof(Hide), true)); } if (ItemPos.HasValue) { @@ -239,120 +418,10 @@ namespace Barotrauma } public static RelatedItem Load(ContentXElement element, bool returnEmpty, string parentDebugName) - { - Identifier[] identifiers; - if (element.GetAttribute("name") != null) - { - //backwards compatibility + a console warning - DebugConsole.ThrowError("Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names."); - Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty()); - //attempt to convert to identifiers and tags - List convertedIdentifiers = new List(); - foreach (Identifier itemName in itemNames) - { - var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName.Value); - if (matchingItem != null) - { - convertedIdentifiers.Add(matchingItem.Identifier); - } - else - { - //no matching item found, this must be a tag - convertedIdentifiers.Add(itemName); - } - } - identifiers = convertedIdentifiers.ToArray(); - } - else - { - identifiers = element.GetAttributeIdentifierArray("items", null) ?? element.GetAttributeIdentifierArray("item", null); - if (identifiers == null) - { - identifiers = element.GetAttributeIdentifierArray("identifiers", null) ?? element.GetAttributeIdentifierArray("tags", null); - if (identifiers == null) - { - identifiers = element.GetAttributeIdentifierArray("identifier", null) ?? element.GetAttributeIdentifierArray("tag", Array.Empty()); - } - } - } - - Identifier[] excludedIdentifiers = element.GetAttributeIdentifierArray("excludeditems", null) ?? element.GetAttributeIdentifierArray("excludeditem", null); - if (excludedIdentifiers == null) - { - excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifiers", null) ?? element.GetAttributeIdentifierArray("excludedtags", null); - if (excludedIdentifiers == null) - { - excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifier", null) ?? element.GetAttributeIdentifierArray("excludedtag", Array.Empty()); - } - } - - if (identifiers.Length == 0 && excludedIdentifiers.Length == 0 && !returnEmpty) { return null; } - - RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers) - { - ExcludeBroken = element.GetAttributeBool("excludebroken", true), - RequireEmpty = element.GetAttributeBool("requireempty", false), - ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false), - AllowVariants = element.GetAttributeBool("allowvariants", true), - Rotation = element.GetAttributeFloat("rotation", 0f), - SetActive = element.GetAttributeBool("setactive", false) - }; - if (element.GetAttribute(nameof(Hide)) != null) - { - ri.Hide = element.GetAttributeBool(nameof(Hide), false); - } - if (element.GetAttribute(nameof(ItemPos)) != null) - { - ri.ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero); - } - string typeStr = element.GetAttributeString("type", ""); - if (string.IsNullOrEmpty(typeStr)) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "containable": - typeStr = "Contained"; - break; - case "suitablefertilizer": - case "suitableseed": - typeStr = "None"; - break; - } - } - if (!Enum.TryParse(typeStr, true, out ri.type)) - { - DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type."); - return null; - } - - ri.MsgTag = element.GetAttributeIdentifier("msg", Identifier.Empty); - LocalizedString msg = TextManager.Get(ri.MsgTag); - if (!msg.Loaded) - { - ri.Msg = ri.MsgTag.Value; - } - else - { -#if CLIENT - foreach (InputType inputType in Enum.GetValues(typeof(InputType))) - { - msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType)); - } - ri.Msg = msg; -#endif - } - - foreach (var subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } - ri.statusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); - } - - ri.IsOptional = element.GetAttributeBool("optional", false); - ri.IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false); - ri.MatchOnEmpty = element.GetAttributeBool("matchonempty", false); - ri.TargetSlot = element.GetAttributeInt("targetslot", -1); - + { + RelatedItem ri = new RelatedItem(element, parentDebugName); + if (ri.Type == RelationType.Invalid) { return null; } + if (ri.Identifiers.None() && ri.ExcludedIdentifiers.None() && !returnEmpty) { return null; } return ri; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 92effd495..59721d257 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -36,6 +36,10 @@ namespace Barotrauma public bool IdFreed { get; private set; } + /// + /// Unique, but non-persistent identifier. + /// Stays the same if the entities are created in the exactly same order, but doesn't persist e.g. between the rounds. + /// public readonly ushort ID; public virtual Vector2 SimPosition => Vector2.Zero; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index b22a9280e..2ce9bd0e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -127,6 +127,7 @@ namespace Barotrauma hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false); } + Attack.DamageMultiplier = 1.0f; float displayRange = Attack.Range; if (damageSource is Item sourceItem) { @@ -157,6 +158,10 @@ namespace Barotrauma Color flashColor = Color.Lerp(Color.Transparent, screenColor, Math.Max((screenColorRange - cameraDist) / screenColorRange, 0.0f)); Screen.Selected.ColorFade(flashColor, Color.Transparent, screenColorDuration); } + foreach (Item item in Item.ItemList) + { + item.GetComponent()?.RegisterExplosion(this, worldPosition); + } #endif if (displayRange < 0.1f) { return; } @@ -201,7 +206,7 @@ namespace Barotrauma powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor; } } - static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - (float)Math.Sqrt(distSqr) / displayRange; + static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - MathF.Sqrt(distSqr) / displayRange; } if (itemRepairStrength > 0.0f) @@ -210,7 +215,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); - if (distSqr > displayRangeSqr) continue; + if (distSqr > displayRangeSqr) { continue; } float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange; //repair repairable items @@ -266,7 +271,7 @@ namespace Barotrauma if (item.Prefab.DamagedByExplosions && !item.Indestructible) { float distFactor = 1.0f - dist / displayRange; - float damageAmount = Attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier; + float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier); Vector2 explosionPos = worldPosition; if (item.Submarine != null) { explosionPos -= item.Submarine.Position; } @@ -354,7 +359,7 @@ namespace Barotrauma if (affliction.DivideByLimbCount) { float limbCountFactor = distFactors.Count; - if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == "damage") + if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType) { // Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor // Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing. @@ -396,9 +401,12 @@ namespace Barotrauma if (attack.StatusEffects != null && attack.StatusEffects.Any()) { attack.SetUser(attacker); - var statusEffectTargets = new List() { c, limb }; + var statusEffectTargets = new List(); foreach (StatusEffect statusEffect in attack.StatusEffects) { + statusEffectTargets.Clear(); + if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { statusEffectTargets.Add(c); } + if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) { statusEffectTargets.Add(limb); } statusEffect.Apply(ActionType.OnUse, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(ActionType.Always, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(underWater ? ActionType.InWater : ActionType.NotInWater, 1.0f, damageSource, statusEffectTargets); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index b7f384cea..ed866eed9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -4,11 +4,12 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma { - partial class Gap : MapEntity + partial class Gap : MapEntity, ISerializableEntity { public static List GapList = new List(); @@ -31,6 +32,8 @@ namespace Barotrauma /// public bool IsDiagonal { get; } + public readonly float GlowEffectT; + //a value between 0.0f-1.0f (0.0 = closed, 1.0f = open) private float open; @@ -51,7 +54,6 @@ namespace Barotrauma //can ambient light get through the gap even if it's not open public bool PassAmbientLight; - //a collider outside the gap (for example an ice wall next to the sub) //used by ragdolls to prevent them from ending up inside colliders when teleporting out of the sub private Body outsideCollisionBlocker; @@ -63,8 +65,43 @@ namespace Barotrauma set { if (float.IsNaN(value)) { return; } - if (value > open) { openedTimer = 1.0f; } + if (value > open) + { + openedTimer = 1.0f; + } + if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull)) + { + if (value > open && value >= 1.0f) + { + InformWaypointsAboutGapState(this, open: true); + } + else if (value < open && open >= 1.0f) + { + InformWaypointsAboutGapState(this, open: false); + } + } open = MathHelper.Clamp(value, 0.0f, 1.0f); + + static void InformWaypointsAboutGapState(Gap gap, bool open) + { + foreach (var wp in WayPoint.WayPointList) + { + if (IsWaypointRightAboveGap(gap, wp)) + { + wp.OnGapStateChanged(open, gap); + } + } + } + + static bool IsWaypointRightAboveGap(Gap gap, WayPoint wp) + { + if (wp.SpawnType != SpawnType.Path) { return false; } + if (!gap.linkedTo.Contains(wp.CurrentHull)) { return false; } + if (wp.Position.Y < gap.Rect.Top) { return false; } + if (wp.Position.X > gap.Rect.Right) { return false; } + if (wp.Position.X < gap.Rect.Left) { return false; } + return true; + } } } @@ -118,12 +155,12 @@ namespace Barotrauma } } - public override string Name + public override string Name => "Gap"; + + public readonly Dictionary properties; + public Dictionary SerializableProperties { - get - { - return "Gap"; - } + get { return properties; } } public Gap(Rectangle rectangle) @@ -150,10 +187,14 @@ namespace Barotrauma IsDiagonal = isDiagonal; open = 1.0f; + properties = SerializableProperty.GetProperties(this); + FindHulls(); GapList.Add(this); InsertToList(); + GlowEffectT = Rand.Range(0.0f, 1.0f); + float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2; outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize, BodyType.Static, @@ -272,30 +313,51 @@ namespace Barotrauma for (int i = 0; i < 2; i++) { hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false); - if (hulls[i] == null) hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false, true); + if (hulls[i] == null) { hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false, true); } } - if (hulls[0] == null && hulls[1] == null) { return; } + if (hulls[0] != null || hulls[1] != null) + { + if (hulls[0] == null && hulls[1] != null) + { + (hulls[1], hulls[0]) = (hulls[0], hulls[1]); + } - if (hulls[0] == null && hulls[1] != null) - { - Hull temp = hulls[0]; - hulls[0] = hulls[1]; - hulls[1] = temp; + flowTargetHull = hulls[0]; + + for (int i = 0; i < 2; i++) + { + if (hulls[i] == null) { continue; } + linkedTo.Add(hulls[i]); + if (!hulls[i].ConnectedGaps.Contains(this)) { hulls[i].ConnectedGaps.Add(this); } + } } - flowTargetHull = hulls[0]; - - for (int i = 0; i < 2; i++) - { - if (hulls[i] == null) { continue; } - linkedTo.Add(hulls[i]); - if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this); - } + RefreshOutsideCollider(); } + private int updateCount; + public override void Update(float deltaTime, Camera cam) { + int updateInterval = 4; + float flowMagnitude = flowForce.LengthSquared(); + if (flowMagnitude < 1.0f) + { + //very sparse updates if there's practically no water moving + updateInterval = 8; + } + else if (linkedTo.Count == 2 && flowMagnitude > 10.0f) + { + //frequent updates if water is moving between hulls + updateInterval = 1; + } + + updateCount++; + if (updateCount < updateInterval) { return; } + deltaTime *= updateCount; + updateCount = 0; + flowForce = Vector2.Zero; outsideColliderRaycastTimer -= deltaTime; @@ -593,7 +655,12 @@ namespace Barotrauma public bool RefreshOutsideCollider() { - if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || !(linkedTo[0] is Hull)) return false; + if (outsideCollisionBlocker == null) { return false; } + if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || linkedTo[0] is not Hull) + { + outsideCollisionBlocker.Enabled = false; + return false; + } if (outsideColliderRaycastTimer <= 0.0f) { @@ -740,8 +807,7 @@ namespace Barotrauma public static Gap Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { - Rectangle rect = Rectangle.Empty; - + Rectangle rect; if (element.GetAttribute("rect") != null) { rect = element.GetAttributeRect("rect", Rectangle.Empty); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 3752320d8..c604cc10d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1083,7 +1083,7 @@ namespace Barotrauma if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) { //gap blocked if the door is not open or the predicted state is not open - if ((!g.ConnectedDoor.IsOpen && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) + if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) { if (g.ConnectedDoor.OpenState < 0.1f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs index f48a4ec23..1aeb8ddd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs @@ -9,7 +9,7 @@ namespace Barotrauma Vector2 WorldPosition { get; } float Health { get; } - AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound=true); + AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true); public readonly struct AttackEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs index 1144fc52b..2cc1d7c27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -1,3 +1,4 @@ +using System; using Barotrauma.Extensions; using System.Collections.Generic; using System.Collections.Immutable; @@ -13,11 +14,14 @@ namespace Barotrauma public readonly LocalizedString Description; public readonly bool IsEndBiome; + public readonly int EndBiomeLocationCount; + public readonly float MinDifficulty; private readonly float maxDifficulty; public float ActualMaxDifficulty => maxDifficulty; public float AdjustedMaxDifficulty => maxDifficulty - 0.1f; + public readonly ImmutableHashSet AllowedZones; private readonly SubmarineAvailability? submarineAvailability; @@ -41,6 +45,8 @@ namespace Barotrauma element.GetAttributeString("description", "")); IsEndBiome = element.GetAttributeBool("endbiome", false); + EndBiomeLocationCount = Math.Max(1, element.GetAttributeInt("endbiomelocationcount", 1)); + AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet(); MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0); maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs index c80309932..b2aaf0964 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs @@ -65,28 +65,28 @@ namespace Barotrauma set { maxHeight = Math.Max(value, minHeight); } } - [Serialize(2, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(2, IsPropertySaveable.Yes, description: "Minimum number of tunnel branches in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MinBranchCount { get { return minBranchCount; } set { minBranchCount = Math.Max(value, 0); } } - [Serialize(4, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(4, IsPropertySaveable.Yes, description: "Maximum number of tunnel branches in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MaxBranchCount { get { return maxBranchCount; } set { maxBranchCount = Math.Max(value, minBranchCount); } } - [Serialize(50, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10000)] + [Serialize(50, IsPropertySaveable.Yes, description: "Total amount of level objects in the cave."), Editable(MinValueInt = 0, MaxValueInt = 10000)] public int LevelObjectAmount { get; set; } - [Serialize(0.1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )] + [Serialize(0.1f, IsPropertySaveable.Yes, description: "What portion of the empty cells in the cave should be turned into destructible walls? For example, 0.1 = 10%."), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )] public float DestructibleWallRatio { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 72bbace41..eb1e44499 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -16,6 +16,11 @@ namespace Barotrauma { partial class Level : Entity, IServerSerializable { + public enum PlacementType + { + Top, Bottom + } + public enum EventType { SingleDestructibleWall, @@ -61,6 +66,7 @@ namespace Barotrauma [Flags] public enum PositionType { + None = 0, MainPath = 0x1, SidePath = 0x2, Cave = 0x4, @@ -68,7 +74,8 @@ namespace Barotrauma Wreck = 0x10, BeaconStation = 0x20, Abyss = 0x40, - AbyssCave = 0x80 + AbyssCave = 0x80, + Outpost = 0x100, } public struct InterestingPosition @@ -413,6 +420,9 @@ namespace Barotrauma get { return LevelData.Type; } } + + public bool IsEndBiome => LevelData.Biome != null && LevelData.Biome.IsEndBiome; + /// /// Is there a loaded level set and is it an outpost? /// @@ -451,6 +461,19 @@ namespace Barotrauma borders = new Rectangle(Point.Zero, levelData.Size); } + public bool ShouldSpawnCrewInsideOutpost() + { + if (StartOutpost != null && + Type == LevelData.LevelType.Outpost && + (StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) && + StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) + { + var reputation = GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation; + return reputation == null || reputation.NormalizedValue >= Reputation.HostileThreshold; + } + return false; + } + public static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) { Debug.Assert(levelData.Biome != null); @@ -482,11 +505,8 @@ namespace Barotrauma EntitiesBeforeGenerate = GetEntities().ToList(); EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count(); - if (LevelData.ForceOutpostGenerationParams == null) - { - StartLocation = startLocation; - EndLocation = endLocation; - } + StartLocation = startLocation; + EndLocation = endLocation; GenerateEqualityCheckValue(LevelGenStage.GenStart); SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier)); @@ -889,6 +909,12 @@ namespace Barotrauma // remove unnecessary cells and create some holes at the bottom of the level //---------------------------------------------------------------------------------- + if (GenerationParams.NoLevelGeometry) + { + cells.ForEach(c => c.CellType = CellType.Removed); + cells.Clear(); + } + cells = cells.Except(pathCells).ToList(); //remove cells from the edges and bottom of the map because a clean-cut edge of the level looks bad cells.ForEachMod(c => @@ -1687,14 +1713,22 @@ namespace Barotrauma foreach (VoronoiCell cell in closeCells) { bool tooClose = false; - foreach (GraphEdge edge in cell.Edges) - { - if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr || - Vector2.DistanceSquared(edge.Point2, position) < minDistSqr || - MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr) + + if (cell.IsPointInsideAABB(position, margin: minDistance)) + { + tooClose = true; + } + else + { + foreach (GraphEdge edge in cell.Edges) { - tooClose = true; - break; + if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr || + Vector2.DistanceSquared(edge.Point2, position) < minDistSqr || + MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr) + { + tooClose = true; + break; + } } } if (tooClose) { tooCloseCells.Add(cell); } @@ -1779,6 +1813,7 @@ namespace Barotrauma if (AbyssArea.Height < islandSize.Y) { return; } + int createdCaves = 0; int islandCount = GenerationParams.AbyssIslandCount; for (int i = 0; i < islandCount; i++) { @@ -1808,7 +1843,11 @@ namespace Barotrauma break; } - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > GenerationParams.AbyssIslandCaveProbability) + bool createCave = + //force at least one abyss cave + (i == islandCount - 1 && createdCaves == 0) || + Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > GenerationParams.AbyssIslandCaveProbability; + if (!createCave) { float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f; var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance); @@ -1867,6 +1906,7 @@ namespace Barotrauma new Point(islandArea.Center.X, islandArea.Center.Y + (int)(islandArea.Size.Y * (1.0f - caveScaleRelativeToIsland)) / 2), new Point((int)(islandArea.Size.X * caveScaleRelativeToIsland), (int)(islandArea.Size.Y * caveScaleRelativeToIsland))); AbyssIslands.Add(new AbyssIsland(islandArea, islandCells)); + createdCaves++; } } @@ -2942,13 +2982,13 @@ namespace Barotrauma } /// Used by clients to set the rotation for the resources - public List GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, out float rotation, IEnumerable targetCaves = null) + public List GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, IEnumerable targetCaves = null) { var allValidLocations = GetAllValidClusterLocations(); var placedResources = new List(); - rotation = 0.0f; - if (allValidLocations.None()) { return placedResources; } // TODO: WHAT?! + // if there are no valid locations, don't place anything + if (allValidLocations.None()) { return placedResources; } // Make sure not to pick a spot that already has other level resources for (int i = allValidLocations.Count - 1; i >= 0; i--) @@ -2979,10 +3019,12 @@ namespace Barotrauma if (PositionsOfInterest.None(p => p.PositionType == positionType)) { + DebugConsole.AddWarning($"Failed to find a position of the type \"{positionType}\" for mission resources."); foreach (var validType in MineralMission.ValidPositionTypes) { if (validType != positionType && PositionsOfInterest.Any(p => p.PositionType == validType)) { + DebugConsole.AddWarning($"Placing in \"{validType}\" instead."); positionType = validType; break; } @@ -3042,7 +3084,6 @@ namespace Barotrauma } PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources); Vector2 edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell); - rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); return placedResources; static bool IsOnMainPath(ClusterLocation location) => location.Edge.NextToMainPath; @@ -3151,9 +3192,7 @@ namespace Barotrauma if (item.GetComponent() is Holdable h) { h.AttachToWall(); -#if CLIENT item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); -#endif } else if (item.body != null) { @@ -3234,7 +3273,8 @@ namespace Barotrauma { suitablePositions.RemoveAll(p => !filter(p)); } - if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath)) + if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath) || positionType.HasFlag(PositionType.Abyss) || + positionType.HasFlag(PositionType.Cave) || positionType.HasFlag(PositionType.AbyssCave)) { suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2())); } @@ -3399,8 +3439,7 @@ namespace Barotrauma bool closeEnough = false; foreach (VoronoiCell cell in wall.Cells) { - if (Math.Abs(cell.Center.X - worldPos.X) < (searchDepth + 1) * GridCellSize && - Math.Abs(cell.Center.Y - worldPos.Y) < (searchDepth + 1) * GridCellSize) + if (cell.IsPointInsideAABB(worldPos, margin: (searchDepth + 1) * GridCellSize / 2)) { closeEnough = true; break; @@ -3553,6 +3592,13 @@ namespace Barotrauma var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value); Rectangle subBorders = Submarine.GetBorders(subDoc.Root); + SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value) + { + Type = type + }; + + //place downwards by default + var placement = info.BeaconStationInfo?.Placement ?? PlacementType.Bottom; // Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by. Point paddedDimensions = new Point(subBorders.Width + 3000, subBorders.Height + 3000); @@ -3573,7 +3619,7 @@ namespace Barotrauma attemptsLeft--; if (TryGetSpawnPoint(out spawnPoint)) { - success = TryPositionSub(subBorders, subName, ref spawnPoint); + success = TryPositionSub(subBorders, subName, placement, ref spawnPoint); if (success) { break; @@ -3594,10 +3640,6 @@ namespace Barotrauma { Debug.WriteLine($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)"); tempSW.Restart(); - SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value) - { - Type = type - }; Submarine sub = new Submarine(info); if (type == SubmarineType.Wreck) { @@ -3647,10 +3689,10 @@ namespace Barotrauma return null; } - bool TryPositionSub(Rectangle subBorders, string subName, ref Vector2 spawnPoint) - { + bool TryPositionSub(Rectangle subBorders, string subName, PlacementType placement, ref Vector2 spawnPoint) + { positions.Add(spawnPoint); - bool bottomFound = TryRaycastToBottom(subBorders, ref spawnPoint); + bool bottomFound = TryRaycast(subBorders, placement, ref spawnPoint); positions.Add(spawnPoint); bool leftSideBlocked = IsSideBlocked(subBorders, false); @@ -3658,21 +3700,21 @@ namespace Barotrauma int step = 5; if (rightSideBlocked && !leftSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, -step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step); } else if (leftSideBlocked && !rightSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, step); } else if (!bottomFound) { if (!leftSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, -step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step); } else if (!rightSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, step); } else { @@ -3702,14 +3744,14 @@ namespace Barotrauma } return !isBlocked && bottomFound; - bool TryMove(Rectangle subBorders, ref Vector2 spawnPoint, float amount) + bool TryMove(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint, float amount) { float maxMovement = 5000; float totalAmount = 0; - bool foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint); + bool foundBottom = TryRaycast(subBorders, placement, ref spawnPoint); while (!IsSideBlocked(subBorders, amount > 0)) { - foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint); + foundBottom = TryRaycast(subBorders, placement, ref spawnPoint); totalAmount += amount; spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y); if (Math.Abs(totalAmount) > maxMovement) @@ -3738,7 +3780,7 @@ namespace Barotrauma return false; } - bool TryRaycastToBottom(Rectangle subBorders, ref Vector2 spawnPoint) + bool TryRaycast(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint) { // Shoot five rays and pick the highest hit point. int rayCount = 5; @@ -3764,16 +3806,18 @@ namespace Barotrauma break; } var simPos = ConvertUnits.ToSimUnits(rayStart); - var body = Submarine.PickBody(simPos, new Vector2(simPos.X, -1), - customPredicate: f => f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body), + var body = Submarine.PickBody(simPos, new Vector2(simPos.X, placement == PlacementType.Bottom ? -1 : Size.Y + 1), + customPredicate: f => f.Body == TopBarrier || f.Body == BottomBarrier || (f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body)), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); if (body != null) { - positions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + new Vector2(0, subBorders.Height / 2); + positions[i] = + ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + + new Vector2(0, subBorders.Height / 2 * (placement == PlacementType.Bottom ? 1 : -1)); hit = true; } } - float highestPoint = positions.Max(p => p.Y); + float highestPoint = placement == PlacementType.Bottom ? positions.Max(p => p.Y) : positions.Min(p => p.Y); spawnPoint = new Vector2(spawnPoint.X, highestPoint); return hit; } @@ -3953,8 +3997,18 @@ namespace Barotrauma if (LevelData.OutpostGenerationParamsExist) { Location location = i == 0 ? StartLocation : EndLocation; - OutpostGenerationParams outpostGenerationParams = LevelData.ForceOutpostGenerationParams ?? - LevelData.GetSuitableOutpostGenerationParams(location).GetRandom(Rand.RandSync.ServerAndClient); + OutpostGenerationParams outpostGenerationParams = null; + if (LevelData.ForceOutpostGenerationParams != null) + { + outpostGenerationParams = LevelData.ForceOutpostGenerationParams; + } + else + { + outpostGenerationParams = + LevelData.ForceOutpostGenerationParams ?? + LevelData.GetSuitableOutpostGenerationParams(location, LevelData).GetRandom(Rand.RandSync.ServerAndClient); + } + LocationType locationType = location?.Type; if (locationType == null) { @@ -4030,52 +4084,70 @@ namespace Barotrauma } } - DockingPort outpostPort = null; - closestDistance = float.MaxValue; - foreach (DockingPort port in DockingPort.List) + Vector2 spawnPos; + if (GenerationParams.ForceOutpostPosition != Vector2.Zero) { - if (port.IsHorizontal || port.Docked) { continue; } - if (port.Item.Submarine != outpost) { continue; } - //the outpost port has to be at the bottom of the outpost - if (port.Item.WorldPosition.Y > outpost.WorldPosition.Y) { continue; } - float dist = Math.Abs(port.Item.WorldPosition.X - outpost.WorldPosition.X); - if (dist < closestDistance) + spawnPos = new Vector2(Size.X * GenerationParams.ForceOutpostPosition.X, Size.Y * GenerationParams.ForceOutpostPosition.Y); + } + else + { + DockingPort outpostPort = null; + closestDistance = float.MaxValue; + foreach (DockingPort port in DockingPort.List) { - outpostPort = port; - closestDistance = dist; + if (port.IsHorizontal || port.Docked) { continue; } + if (port.Item.Submarine != outpost) { continue; } + //the outpost port has to be at the bottom of the outpost + if (port.Item.WorldPosition.Y > outpost.WorldPosition.Y) { continue; } + float dist = Math.Abs(port.Item.WorldPosition.X - outpost.WorldPosition.X); + if (dist < closestDistance) + { + outpostPort = port; + closestDistance = dist; + } } - } - float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X; - //don't try to compensate if the port is very far from the sub's center of mass - if (Math.Abs(subDockingPortOffset) > 5000.0f) - { - subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f); - string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; - DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); - } - - float? outpostDockingPortOffset = null; - if (outpostPort != null) - { - outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X; - //don't try to compensate if the port is very far from the outpost's center of mass - if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f) + float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X; + //don't try to compensate if the port is very far from the sub's center of mass + if (Math.Abs(subDockingPortOffset) > 5000.0f) { - outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f); - string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; + subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f); + string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + } + + float? outpostDockingPortOffset = null; + if (outpostPort != null) + { + outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X; + //don't try to compensate if the port is very far from the outpost's center of mass + if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f) + { + outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f); + string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; + DebugConsole.NewMessage(warningMsg, Color.Orange); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + } + } + + spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1); + if (Type == LevelData.LevelType.Outpost) + { + spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2); } } - Vector2 spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1); - if (Type == LevelData.LevelType.Outpost) - { - spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2); - } outpost.SetPosition(spawnPos, forceUndockFromStaticSubmarines: false); + + foreach (WayPoint wp in WayPoint.WayPointList) + { + if (wp.Submarine == outpost && wp.SpawnType != SpawnType.Path) + { + PositionsOfInterest.Add(new InterestingPosition(wp.WorldPosition.ToPoint(), PositionType.Outpost, outpost)); + } + } + if ((i == 0) == !Mirrored) { StartOutpost = outpost; @@ -4094,13 +4166,12 @@ namespace Barotrauma outpost.Info.Name = EndLocation.Name; } } - } } private void CreateBeaconStation() { - if (!LevelData.HasBeaconStation) { return; } + if (!LevelData.HasBeaconStation && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; } var beaconStationFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) .OrderBy(f => f.UintIdentifier).ToList(); @@ -4111,27 +4182,40 @@ namespace Barotrauma } var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon); - for (int i = beaconStationFiles.Count - 1; i >= 0; i--) + ContentFile contentFile = null; + if (!string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { - var beaconStationFile = beaconStationFiles[i]; - var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value); - Debug.Assert(matchingInfo != null); - if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo) + contentFile = beaconStationFiles.FirstOrDefault(f => f.Path == GenerationParams.ForceBeaconStation); + if (contentFile == null) { - if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty) - { - beaconStationFiles.RemoveAt(i); - } + DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead..."); } } - if (beaconStationFiles.None()) - { - DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!"); - return; - } - var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient); - string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); + if (contentFile == null) + { + for (int i = beaconStationFiles.Count - 1; i >= 0; i--) + { + var beaconStationFile = beaconStationFiles[i]; + var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value); + Debug.Assert(matchingInfo != null); + if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo) + { + if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty) + { + beaconStationFiles.RemoveAt(i); + } + } + } + if (beaconStationFiles.None()) + { + DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!"); + return; + } + contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient); + } + + string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation); if (BeaconStation == null) { @@ -4195,7 +4279,7 @@ namespace Barotrauma { bool allowDisconnectedWires = true; bool allowDamagedWalls = true; - if (BeaconStation.Info?.BeaconStationInfo is BeaconStationInfo info) + if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info) { allowDisconnectedWires = info.AllowDisconnectedWires; allowDamagedWalls = info.AllowDamagedWalls; @@ -4346,7 +4430,7 @@ namespace Barotrauma corpse.AnimController.FindHull(worldPos, setSubmarine: true); corpse.TeamID = CharacterTeamType.None; corpse.EnableDespawn = false; - selectedPrefab.GiveItems(corpse, wreck); + selectedPrefab.GiveItems(corpse, wreck, sp); corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200)); bool applyBurns = Rand.Value() < 0.1f; @@ -4377,7 +4461,6 @@ namespace Barotrauma } } corpse.CharacterHealth.ForceUpdateVisuals(); - corpse.GiveIdCardTags(sp); bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index f926af7ca..b7a5aea58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -24,7 +24,7 @@ namespace Barotrauma public readonly Biome Biome; - public readonly LevelGenerationParams GenerationParams; + public LevelGenerationParams GenerationParams { get; private set; } public bool HasBeaconStation; public bool IsBeaconActive; @@ -60,12 +60,14 @@ namespace Barotrauma /// /// Events that have previously triggered in this level. Used for making events the player hasn't seen yet more likely to trigger when re-entering the level. Has a maximum size of . /// - public readonly List EventHistory = new List(); + public readonly List EventHistory = new List(); /// /// Events that have already triggered in this level and can never trigger again. . /// - public readonly List NonRepeatableEvents = new List(); + public readonly List NonRepeatableEvents = new List(); + + public readonly Dictionary FinishedEvents = new Dictionary(); /// /// 'Exhaustible' sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. . @@ -150,10 +152,51 @@ namespace Barotrauma } string[] prefabNames = element.GetAttributeStringArray("eventhistory", Array.Empty()); - EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n))); + EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier)); string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", Array.Empty()); - NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n))); + NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier)); + + string finishedEventsName = nameof(FinishedEvents); + if (element.GetChildElement(finishedEventsName) is { } finishedEventsElement) + { + foreach (var childElement in finishedEventsElement.GetChildElements(finishedEventsName)) + { + Identifier eventSetIdentifier = childElement.GetAttributeIdentifier("set", Identifier.Empty); + if (eventSetIdentifier.IsEmpty) { continue; } + if (!EventSet.Prefabs.TryGet(eventSetIdentifier, out EventSet eventSet)) + { + foreach (var prefab in EventSet.Prefabs) + { + if (FindSetRecursive(prefab, eventSetIdentifier) is { } foundSet) + { + eventSet = foundSet; + break; + } + } + } + if (eventSet is null) { continue; } + int count = childElement.GetAttributeInt("count", 0); + if (count < 1) { continue; } + FinishedEvents.Add(eventSet, count); + } + + static EventSet FindSetRecursive(EventSet parentSet, Identifier setIdentifier) + { + foreach (var childSet in parentSet.ChildSets) + { + if (childSet.Identifier == setIdentifier) + { + return childSet; + } + if (FindSetRecursive(childSet, setIdentifier) is { } foundSet) + { + return foundSet; + } + } + return null; + } + } EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false); } @@ -163,7 +206,7 @@ namespace Barotrauma /// public LevelData(LocationConnection locationConnection) { - Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName; + Seed = locationConnection.Locations[0].LevelData.Seed + locationConnection.Locations[1].LevelData.Seed; Biome = locationConnection.Biome; Type = LevelType.LocationConnection; Difficulty = locationConnection.Difficulty; @@ -196,9 +239,9 @@ namespace Barotrauma /// /// Instantiates level data using the properties of the location /// - public LevelData(Location location, float difficulty) + public LevelData(Location location, Map map, float difficulty) { - Seed = location.BaseName; + Seed = location.BaseName + map.Locations.IndexOf(location); Biome = location.Biome; Type = LevelType.Outpost; Difficulty = difficulty; @@ -254,14 +297,22 @@ namespace Barotrauma return levelData; } + public void ReassignGenerationParams(string seed) + { + GenerationParams = LevelGenerationParams.GetRandom(seed, Type, Difficulty, Biome.Identifier); + } public bool OutpostGenerationParamsExist => ForceOutpostGenerationParams != null || OutpostGenerationParams.OutpostParams.Any(); - public static IEnumerable GetSuitableOutpostGenerationParams(Location location) + public static IEnumerable GetSuitableOutpostGenerationParams(Location location, LevelData levelData) { - var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); + var suitableParams = OutpostGenerationParams.OutpostParams + .Where(p => p.LevelType == null || levelData.Type == p.LevelType) + .Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); if (!suitableParams.Any()) { - suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any()); + suitableParams = OutpostGenerationParams.OutpostParams + .Where(p => p.LevelType == null || levelData.Type == p.LevelType) + .Where(p => location == null || !p.AllowedLocationTypes.Any()); if (!suitableParams.Any()) { DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters."); @@ -305,11 +356,23 @@ namespace Barotrauma { if (EventHistory.Any()) { - newElement.Add(new XAttribute("eventhistory", string.Join(',', EventHistory.Select(p => p.Identifier)))); + newElement.Add(new XAttribute("eventhistory", string.Join(',', EventHistory))); } if (NonRepeatableEvents.Any()) { - newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents.Select(p => p.Identifier)))); + newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents))); + } + if (FinishedEvents.Any()) + { + var finishedEventsElement = new XElement(nameof(FinishedEvents)); + foreach (var (set, count) in FinishedEvents) + { + var element = new XElement(nameof(FinishedEvents), + new XAttribute("set", set.Identifier), + new XAttribute("count", count)); + finishedEventsElement.Add(element); + } + newElement.Add(finishedEventsElement); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 498702fa1..1a1210460 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -67,7 +67,7 @@ namespace Barotrauma set; } - [Serialize(1.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(100.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float Commonness { get; @@ -95,7 +95,7 @@ namespace Barotrauma set; } - [Serialize("20,40,50", IsPropertySaveable.Yes), Editable()] + [Serialize("20,40,50", IsPropertySaveable.Yes), Editable] public Color BackgroundTextureColor { get; @@ -116,6 +116,13 @@ namespace Barotrauma set; } + [Serialize("255,255,255", IsPropertySaveable.Yes), Editable] + public Color WaterParticleColor + { + get; + set; + } + private Vector2 startPosition; [Serialize("0,0", IsPropertySaveable.Yes, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] public Vector2 StartPosition @@ -142,6 +149,19 @@ namespace Barotrauma } } + private Vector2 forceOutpostPosition; + [Serialize("0,0", IsPropertySaveable.Yes, "Position of the outpost (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner). If set to 0,0, the outpost is placed in a suitable position automatically."), Editable(DecimalCount = 2)] + public Vector2 ForceOutpostPosition + { + get { return forceOutpostPosition; } + set + { + forceOutpostPosition = new Vector2( + MathHelper.Clamp(value.X, 0.0f, 1.0f), + MathHelper.Clamp(value.Y, 0.0f, 1.0f)); + } + } + [Serialize(true, IsPropertySaveable.Yes, "Should there be a hole in the wall next to the end outpost (can be used to prevent players from having to backtrack if they approach the outpost from the wrong side of the main path's walls)."), Editable] public bool CreateHoleNextToEnd { @@ -156,6 +176,13 @@ namespace Barotrauma set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, no walls generate in the level. Can be useful for e.g. levels that are just supposed to consist of a pre-built outpost."), Editable] + public bool NoLevelGeometry + { + get; + set; + } + [Serialize(1000, IsPropertySaveable.Yes, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)] public int LevelObjectAmount { @@ -191,21 +218,21 @@ namespace Barotrauma set { height = MathHelper.Clamp(value, 2000, 1000000); } } - [Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(80000, IsPropertySaveable.Yes, description: "Minimum depth at the top of the level (100 corresponds to 1 meter)."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int InitialDepthMin { get { return initialDepthMin; } set { initialDepthMin = Math.Max(value, 0); } } - [Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(80000, IsPropertySaveable.Yes, description: "Maximum depth at the top of the level (100 corresponds to 1 meter)."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int InitialDepthMax { get { return initialDepthMax; } set { initialDepthMax = Math.Max(value, initialDepthMin); } } - [Serialize(6500, IsPropertySaveable.Yes), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] + [Serialize(6500, IsPropertySaveable.Yes, description: "Minimum width of the main tunnel going through the level, in pixels. Can be automatically increased by the level editor if the submarine is larger than this."), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] public int MinTunnelRadius { get; @@ -213,7 +240,7 @@ namespace Barotrauma } - [Serialize("0,1", IsPropertySaveable.Yes), Editable] + [Serialize("0,1", IsPropertySaveable.Yes, description: "Amount of side tunnels in the level (min,max)."), Editable] public Point SideTunnelCount { get; @@ -221,14 +248,14 @@ namespace Barotrauma } - [Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the side tunnels can \"zigzag\". 0 = completely straight tunnel, 1 = can go all the way from the top of the level to the bottom."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float SideTunnelVariance { get; set; } - [Serialize("2000,6000", IsPropertySaveable.Yes), Editable] + [Serialize("2000,6000", IsPropertySaveable.Yes, description: "Minimum width of the side tunnels, in pixels. Unlike the main tunnel, does not get adjusted based on the size of the submarine."), Editable] public Point MinSideTunnelRadius { get; @@ -309,7 +336,7 @@ namespace Barotrauma } } - [Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the side tunnels can \"zigzag\". 0 = completely straight tunnel, 1 = can go all the way from the top of the level to the bottom."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float MainPathVariance { get; @@ -358,28 +385,28 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.Yes, description: "How likely a resource spawn point on a cave path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float CaveResourceSpawnChance { get; set; } - [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes, description: "Number of floating, destructible ice chunks in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int FloatingIceChunkCount { get; set; } - [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 100)] + [Serialize(0, IsPropertySaveable.Yes, description: "Number of islands (static wall chunks along the main path) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100)] public int IslandCount { get; set; } - [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes, description: "Number of ice spires in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int IceSpireCount { get; set; } - [Serialize(5, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(5, IsPropertySaveable.Yes, description: "Number of abyss islands in the level."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int AbyssIslandCount { get; @@ -400,7 +427,7 @@ namespace Barotrauma set; } - [Serialize(0.5f, IsPropertySaveable.Yes), Editable()] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "The probability of an abyss island having a cave. There is always a cave in at least one of the islands regardless of this setting."), Editable()] public float AbyssIslandCaveProbability { get; @@ -478,14 +505,14 @@ namespace Barotrauma [Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int RuinCount { get; set; } + // TODO: Move the wreck parameters under a separate class? +#region Wreck parameters [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MinWreckCount { get; set; } [Serialize(1, IsPropertySaveable.Yes, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MaxWreckCount { get; set; } - // TODO: Move the wreck parameters under a separate class? -#region Wreck parameters [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MinCorpseCount { get; set; } @@ -503,7 +530,10 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float WreckFloodingHullMaxWaterPercentage { get; set; } -#endregion + #endregion + + [Serialize("", IsPropertySaveable.Yes, description: "Should a beacon station always spawn in this type of level?")] + public string ForceBeaconStation { get; set; } [Serialize(0.4f, IsPropertySaveable.Yes, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] public float BottomHoleProbability @@ -519,6 +549,14 @@ namespace Barotrauma private set { waterParticleScale = Math.Max(value, 0.01f); } } + private Vector2 waterParticleVelocity; + [Serialize("0,10", IsPropertySaveable.Yes, description: "How fast the water particle texture scrolls."), Editable] + public Vector2 WaterParticleVelocity + { + get { return waterParticleVelocity; } + private set { waterParticleVelocity = value; } + } + [Serialize(2048.0f, IsPropertySaveable.Yes, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)] public float WallTextureSize { @@ -533,6 +571,34 @@ namespace Barotrauma private set; } + [Serialize("0,0", IsPropertySaveable.Yes, description: "Interval of lightning-like flashes of light in the level."), Editable] + public Vector2 FlashInterval + { + get; + set; + } + + [Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Color of lightning-like flashes of light in the level."), Editable] + public Color FlashColor + { + get; + set; + } + + [Serialize(false, IsPropertySaveable.Yes, description: "Should the \"ambient noise\" of the biome play in this level if it's an outpost level."), Editable] + public bool PlayNoiseLoopInOutpostLevel + { + get; + set; + } + + [Serialize(1.0f, IsPropertySaveable.Yes), Editable] + public float WaterAmbienceVolume + { + get; + set; + } + [Serialize(120.0f, IsPropertySaveable.Yes, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)] public float WallEdgeExpandOutwardsAmount { @@ -556,8 +622,13 @@ namespace Barotrauma public Sprite WallSpriteDestroyed { get; private set; } public Sprite WaterParticles { get; private set; } +#if CLIENT + public Sounds.Sound FlashSound { get; private set; } +#endif + #warning TODO: this should be in the unit test project (#3164) public static void CheckValidity() + { foreach (Biome biome in Biome.Prefabs) { @@ -575,7 +646,7 @@ namespace Barotrauma } } - public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biome = default) + public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biomeId = default) { Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); @@ -590,14 +661,29 @@ namespace Barotrauma lp.Type == type && (lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) && !lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier())); - matchingLevelParams = biome.IsEmpty - ? matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome)) - : matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biome)); + if (biomeId.IsEmpty) + { + //we don't want end levels when generating a completely random level (e.g. in mission mode) + matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome)); + } + else + { + bool isEndBiome = Biome.Prefabs.TryGet(biomeId, out Biome biome) && biome.IsEndBiome; + if (isEndBiome && matchingLevelParams.Any(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId))) + { + //in the end biome, we must choose level parameters meant specifically for the end levels + matchingLevelParams = matchingLevelParams.Where(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId)); + } + else + { + matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biomeId)); + } + } if (!matchingLevelParams.Any()) { - DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")"); - if (!biome.IsEmpty) + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")"); + if (!biomeId.IsEmpty) { //try to find params that at least have a suitable type matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type); @@ -611,7 +697,7 @@ namespace Barotrauma if (!matchingLevelParams.Any(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty)) { - DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})"); + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})"); } else { @@ -661,6 +747,11 @@ namespace Barotrauma case "waterparticles": WaterParticles = new Sprite(subElement); break; +#if CLIENT + case "flashsound": + FlashSound = GameMain.SoundManager.LoadSound(subElement); + break; +#endif } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 5b9d587fc..1bec94d04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -235,9 +235,22 @@ namespace Barotrauma SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient); if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; } PlaceObject(prefab, spawnPosition, level, cave); - if (prefab.MaxCount < amount) + if (amount > prefab.MaxCount && objects.Count > prefab.MaxCount) { - if (objects.Count(o => o.Prefab == prefab && o.ParentCave == cave) >= prefab.MaxCount) + bool maxReached = false; + int objectCount = 0; + for (int j = 0; j < objects.Count; j++) + { + if (objects[j].Prefab == prefab && objects[j].ParentCave == cave) + { + objectCount++; + if (objectCount >= prefab.MaxCount) + { + break; + } + } + } + if (objectCount >= prefab.MaxCount) { availablePrefabs.Remove(prefab); } @@ -350,8 +363,8 @@ namespace Barotrauma private void AddObject(LevelObject newObject, Level level) { - if (newObject.Triggers != null) - { + if (newObject.Triggers != null) + { foreach (LevelTrigger trigger in newObject.Triggers) { trigger.OnTriggered += (levelTrigger, obj) => @@ -360,7 +373,7 @@ namespace Barotrauma }; } } - + var spriteCorners = new List { Vector2.Zero, Vector2.Zero, Vector2.Zero, Vector2.Zero @@ -393,11 +406,11 @@ namespace Barotrauma } } - float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z; - float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z; + float minX = spriteCorners.Min(c => c.X) - newObject.Position.Z / 10000.0f; + float maxX = spriteCorners.Max(c => c.X) + newObject.Position.Z / 10000.0f; - float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z - level.BottomPos; - float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z - level.BottomPos; + float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z / 10000.0f - level.BottomPos; + float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z / 10000.0f - level.BottomPos; if (newObject.Triggers != null) { @@ -436,11 +449,11 @@ namespace Barotrauma int xStart = (int)Math.Floor(minX / GridSize); int xEnd = (int)Math.Floor(maxX / GridSize); - if (xEnd < 0 || xStart >= objectGrid.GetLength(0)) return; + if (xEnd < 0 || xStart >= objectGrid.GetLength(0)) { return; } int yStart = (int)Math.Floor(minY / GridSize); int yEnd = (int)Math.Floor(maxY / GridSize); - if (yEnd < 0 || yStart >= objectGrid.GetLength(1)) return; + if (yEnd < 0 || yStart >= objectGrid.GetLength(1)) { return; } xStart = Math.Max(xStart, 0); xEnd = Math.Min(xEnd, objectGrid.GetLength(0) - 1); @@ -451,13 +464,21 @@ namespace Barotrauma { for (int y = yStart; y <= yEnd; y++) { - if (objectGrid[x, y] == null) objectGrid[x, y] = new List(); - objectGrid[x, y].Add(newObject); + var list = objectGrid[x, y]; + if (objectGrid[x, y] == null) { list = objectGrid[x, y] = new List(); } + + //insertion sort in ascending order (= prefer rendering objects in front) + int drawOrderIndex = 0; + while (drawOrderIndex < list.Count && list[drawOrderIndex].Position.Z < newObject.Position.Z) + { + drawOrderIndex++; + } + list.Insert(drawOrderIndex, newObject); } } } - public Microsoft.Xna.Framework.Point GetGridIndices(Vector2 worldPosition) + public static Microsoft.Xna.Framework.Point GetGridIndices(Vector2 worldPosition) { return new Microsoft.Xna.Framework.Point( (int)Math.Floor(worldPosition.X / GridSize), @@ -500,7 +521,7 @@ namespace Barotrauma return objectsInRange; } - private List GetAvailableSpawnPositions(IEnumerable cells, LevelObjectPrefab.SpawnPosType spawnPosType, bool checkFlags = true) + private static List GetAvailableSpawnPositions(IEnumerable cells, LevelObjectPrefab.SpawnPosType spawnPosType, bool checkFlags = true) { List spawnPosTypes = new List(4); List availableSpawnPositions = new List(); @@ -593,12 +614,12 @@ namespace Barotrauma if (obj == triggeredObject || obj.Triggers == null) { continue; } foreach (LevelTrigger otherTrigger in obj.Triggers) { - otherTrigger.OtherTriggered(triggeredObject, trigger); + otherTrigger.OtherTriggered(trigger, triggerer); } } } - private LevelObjectPrefab GetRandomPrefab(Level level, IList availablePrefabs) + private static LevelObjectPrefab GetRandomPrefab(Level level, IList availablePrefabs) { if (availablePrefabs.Sum(p => p.GetCommonness(level.LevelData)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( @@ -606,7 +627,7 @@ namespace Barotrauma availablePrefabs.Select(p => p.GetCommonness(level.LevelData)).ToList(), Rand.RandSync.ServerAndClient); } - private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList availablePrefabs, bool requireCaveSpecificOverride) + private static LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList availablePrefabs, bool requireCaveSpecificOverride) { if (availablePrefabs.Sum(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( @@ -634,7 +655,7 @@ namespace Barotrauma public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - if (!(extraData is EventData eventData)) { throw new Exception($"Malformed LevelObjectManager event: expected {nameof(LevelObjectManager)}.{nameof(EventData)}"); } + if (extraData is not EventData eventData) { throw new Exception($"Malformed LevelObjectManager event: expected {nameof(LevelObjectManager)}.{nameof(EventData)}"); } LevelObject obj = eventData.LevelObject; msg.WriteRangedInteger(objects.IndexOf(obj), 0, objects.Count); obj.ServerWrite(msg, c); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 213fad7bf..60d7ce783 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -129,7 +129,7 @@ namespace Barotrauma private set; } - [Serialize("0.0,1.0", IsPropertySaveable.Yes), Editable] + [Serialize("0.0,1.0", IsPropertySaveable.Yes, description: "The sprite depth of the object (min, max). Values of 0 or less make the object render in front of walls, values larger than 0 make it render behind walls with a parallax effect."), Editable] public Vector2 DepthRange { get; @@ -273,14 +273,14 @@ namespace Barotrauma private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the object disappear if the object is destroyed? Only relevant if TakeLevelWallDamage is true."), Editable] public bool HideWhenBroken { get; private set; } - [Serialize(100.0f, IsPropertySaveable.Yes), Editable] + [Serialize(100.0f, IsPropertySaveable.Yes, description: "Amount of health the object has. Only relevant if TakeLevelWallDamage is true."), Editable] public float Health { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index a5c1b3f85..78deb6fe1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; @@ -215,7 +216,7 @@ namespace Barotrauma PhysicsBody.FarseerBody.SetIsSensor(element.GetAttributeBool("sensor", true)); PhysicsBody.FarseerBody.BodyType = BodyType.Static; - ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f)); + ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.Radius, PhysicsBody.Width / 2.0f), PhysicsBody.Height / 2.0f)); PhysicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation); } @@ -470,7 +471,7 @@ namespace Barotrauma /// /// Another trigger was triggered, check if this one should react to it /// - public void OtherTriggered(LevelObject levelObject, LevelTrigger otherTrigger) + public void OtherTriggered(LevelTrigger otherTrigger, Entity triggerer) { if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) { return; } @@ -486,7 +487,16 @@ namespace Barotrauma triggeredTimer = stayTriggeredDelay; if (!wasAlreadyTriggered) { - OnTriggered?.Invoke(this, null); + if (!IsTriggeredByEntity(triggerer, triggeredBy, mustBeOutside: true)) { return; } + if (!triggerers.Contains(triggerer)) + { + if (!IsTriggered) + { + OnTriggered?.Invoke(this, triggerer); + } + TriggererPosition[triggerer] = triggerer.WorldPosition; + triggerers.Add(triggerer); + } } } } @@ -658,6 +668,10 @@ namespace Barotrauma { effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position); } + else if (triggerer is Submarine sub) + { + effect.Apply(effect.type, deltaTime, sub, Array.Empty(), position); + } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); @@ -747,7 +761,7 @@ namespace Barotrauma Vector2 baseVel = GetWaterFlowVelocity(); if (baseVel.LengthSquared() < 0.1f) return Vector2.Zero; - float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f)); + float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.Radius, PhysicsBody.Width / 2.0f), PhysicsBody.Height / 2.0f)); float dist = Vector2.Distance(viewPosition, WorldPosition); if (dist > triggerSize) return Vector2.Zero; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 636c47c1a..58ae53c65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -476,7 +476,7 @@ namespace Barotrauma bool leaveBehind = false; if (sub.Submarine != null && !sub.DockedTo.Contains(sub.Submarine)) { - System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit); + System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEitherExit); if (Submarine.MainSub.AtEndExit) { leaveBehind = sub.AtEndExit != Submarine.MainSub.AtEndExit; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 7fa9b6fc2..6d13e794e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -3,7 +3,6 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -63,10 +62,17 @@ namespace Barotrauma public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false; + public bool Visited => GameMain.GameSession?.Map?.IsVisited(this) ?? false; + public readonly Dictionary ProximityTimer = new Dictionary(); public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange; public int LocationTypeChangeCooldown; + /// + /// Is some mission blocking this location from changing its type? + /// + public bool LocationTypeChangesBlocked => availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges); + public string BaseName { get => baseName; } public string Name { get; private set; } @@ -83,7 +89,11 @@ namespace Barotrauma public int PortraitId { get; private set; } - public Reputation Reputation { get; set; } + public Faction Faction { get; set; } + + public Faction SecondaryFaction { get; set; } + + public Reputation Reputation => Faction?.Reputation; public int TurnsInRadiation { get; set; } @@ -92,6 +102,7 @@ namespace Barotrauma public class StoreInfo { public Identifier Identifier { get; } + public Identifier MerchantFaction { get; private set; } public int Balance { get; set; } public List Stock { get; } = new List(); public List DailySpecials { get; } = new List(); @@ -101,6 +112,7 @@ namespace Barotrauma /// public int PriceModifier { get; set; } public Location Location { get; } + private float MaxReputationModifier => Location.StoreMaxReputationModifier; private StoreInfo(Location location) { @@ -125,6 +137,7 @@ namespace Barotrauma public StoreInfo(Location location, XElement storeElement) : this(location) { Identifier = storeElement.GetAttributeIdentifier("identifier", ""); + MerchantFaction = storeElement.GetAttributeIdentifier(nameof(MerchantFaction), ""); Balance = storeElement.GetAttributeInt("balance", location.StoreInitialBalance); PriceModifier = storeElement.GetAttributeInt("pricemodifier", 0); // Backwards compatibility: before introducing support for multiple stores, this value was saved as a store element attribute @@ -281,15 +294,17 @@ namespace Barotrauma { price = Location.DailySpecialPriceModifier * price; } - // Adjust by current location reputation - price *= Location.GetStoreReputationModifier(true); + // Adjust by current reputation + price *= GetReputationModifier(true); var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) { - if (Location.Reputation?.Faction is { } faction && faction.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated) + var faction = GetMerchantOrLocationFactionIdentifier(); + if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive) { - price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated)); + price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false)); + price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag))); } price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false)); price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag))); @@ -312,8 +327,8 @@ namespace Barotrauma { price = Location.RequestGoodPriceModifier * price; } - // Adjust by current location reputation - price *= Location.GetStoreReputationModifier(false); + // Adjust by location reputation + price *= GetReputationModifier(false); var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) @@ -326,6 +341,45 @@ namespace Barotrauma return Math.Max((int)price, 1); } + public void SetMerchantFaction(Identifier factionIdentifier) + { + MerchantFaction = factionIdentifier; + } + + public Identifier GetMerchantOrLocationFactionIdentifier() + { + return MerchantFaction.IfEmpty(Location.Faction?.Prefab.Identifier ?? Identifier.Empty); + } + + public float GetReputationModifier(bool buying) + { + var factionIdentifier = GetMerchantOrLocationFactionIdentifier(); + var reputation = GameMain.GameSession.Campaign.GetFaction(factionIdentifier)?.Reputation; + if (reputation == null) { return 1.0f; } + if (buying) + { + if (reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation); + } + } + else + { + if (reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation); + } + } + } + public override string ToString() { return Identifier.Value; @@ -387,6 +441,8 @@ namespace Barotrauma } } + + public void SelectMission(Mission mission) { if (!SelectedMissions.Contains(mission) && mission != null) @@ -449,17 +505,22 @@ namespace Barotrauma public bool IsGateBetweenBiomes; - private struct LoadedMission + private readonly struct LoadedMission { - public MissionPrefab MissionPrefab { get; } - public int DestinationIndex { get; } - public bool SelectedMission { get; } + public readonly MissionPrefab MissionPrefab; + public readonly int TimesAttempted; + public readonly int OriginLocationIndex; + public readonly int DestinationIndex; + public readonly bool SelectedMission; - public LoadedMission(MissionPrefab prefab, int destinationIndex, bool selectedMission) + public LoadedMission(XElement element) { - MissionPrefab = prefab; - DestinationIndex = destinationIndex; - SelectedMission = selectedMission; + var id = element.GetAttributeIdentifier("prefabid", Identifier.Empty); + MissionPrefab = MissionPrefab.Prefabs.TryGet(id, out var prefab) ? prefab : null; + TimesAttempted = element.GetAttributeInt("timesattempted", 0); + OriginLocationIndex = element.GetAttributeInt("origin", -1); + DestinationIndex = element.GetAttributeInt("destinationindex", -1); + SelectedMission = element.GetAttributeBool("selected", false); } } @@ -484,7 +545,7 @@ namespace Barotrauma /// /// Create a location from save data /// - public Location(XElement element) + public Location(CampaignMode campaign, XElement element) { Identifier locationTypeId = element.GetAttributeIdentifier("type", ""); bool typeNotFound = GetTypeOrFallback(locationTypeId, out LocationType type); @@ -497,12 +558,23 @@ namespace Barotrauma baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); MapPosition = element.GetAttributeVector2("position", Vector2.Zero); - PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); - IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); - MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); - TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); - StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0); + PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); + IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); + MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); + TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); + StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0); + + var factionIdentifier = element.GetAttributeIdentifier("faction", Identifier.Empty); + if (!factionIdentifier.IsEmpty) + { + Faction = campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier); + } + var secondaryFactionIdentifier = element.GetAttributeIdentifier("secondaryfaction", Identifier.Empty); + if (!secondaryFactionIdentifier.IsEmpty) + { + SecondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier); + } Identifier biomeId = element.GetAttributeIdentifier("biome", Identifier.Empty); if (biomeId != Identifier.Empty) { @@ -640,13 +712,11 @@ namespace Barotrauma loadedMissions = new List(); foreach (XElement childElement in missionsElement.GetChildElements("mission")) { - var id = childElement.GetAttributeString("prefabid", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } - var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id); - if (prefab == null) { continue; } - var destination = childElement.GetAttributeInt("destinationindex", -1); - var selected = childElement.GetAttributeBool("selected", false); - loadedMissions.Add(new LoadedMission(prefab, destination, selected)); + var loadedMission = new LoadedMission(childElement); + if (loadedMission.MissionPrefab != null) + { + loadedMissions.Add(loadedMission); + } } } } @@ -656,7 +726,7 @@ namespace Barotrauma return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); } - public void ChangeType(LocationType newType, bool createStores = true) + public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true) { if (newType == Type) { return; } @@ -671,56 +741,61 @@ namespace Barotrauma Type = newType; Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); - if (Type.MissionIdentifiers.Any()) + if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandomUnsynced()); + if (Faction == null) + { + Faction = campaign.GetRandomFaction(Rand.RandSync.Unsynced); + } + if (SecondaryFaction == null) + { + SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced); + } } - if (Type.MissionTags.Any()) + else { - UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced()); + Faction = null; + SecondaryFaction = null; } + UnlockInitialMissions(Rand.RandSync.Unsynced); + if (createStores) { CreateStores(force: true); } } - public void UnlockInitialMissions() + public void UnlockInitialMissions(Rand.RandSync randSync = Rand.RandSync.ServerAndClient) { if (Type.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(Rand.RandSync.ServerAndClient)); + UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(randSync)); } if (Type.MissionTags.Any()) { - UnlockMissionByTag(Type.MissionTags.GetRandom(Rand.RandSync.ServerAndClient)); + UnlockMissionByTag(Type.MissionTags.GetRandom(randSync)); } } public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection) { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } - var mission = InstantiateMission(missionPrefab, connection); - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; } + AddMission(InstantiateMission(missionPrefab, connection)); } public void UnlockMission(MissionPrefab missionPrefab) { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } - var mission = InstantiateMission(missionPrefab); - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; } + AddMission(InstantiateMission(missionPrefab)); } public Mission UnlockMissionByIdentifier(Identifier identifier) { if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; } + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; } var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier); if (missionPrefab == null) @@ -735,43 +810,45 @@ namespace Barotrauma { return null; } - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + AddMission(mission); + DebugConsole.NewMessage($"Unlocked a mission by \"{identifier}\".", debugOnly: true); return mission; } return null; } - public Mission UnlockMissionByTag(Identifier tag) + public Mission UnlockMissionByTag(Identifier tag, Random random = null) { - var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Any(t => t == tag)); - if (!matchingMissions.Any()) + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; } + var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Contains(tag)); + if (matchingMissions.None()) { DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found."); } else { - var unusedMissions = matchingMissions.Where(m => !availableMissions.Any(mission => mission.Prefab == m)); + var unusedMissions = matchingMissions.Where(m => availableMissions.None(mission => mission.Prefab == m)); if (unusedMissions.Any()) { var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)) || m.IsAllowed(this, this))); - if (!suitableMissions.Any()) + if (suitableMissions.None()) { suitableMissions = unusedMissions; } - MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(suitableMissions.ToList(), suitableMissions.Select(m => (float)m.Commonness).ToList(), Rand.RandSync.Unsynced); + + MissionPrefab missionPrefab = + random != null ? + ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, random) : + ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, Rand.RandSync.Unsynced); + var mission = InstantiateMission(missionPrefab, out LocationConnection connection); //don't allow duplicate missions in the same connection if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1]))) { return null; } - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + AddMission(mission); + DebugConsole.NewMessage($"Unlocked a random mission by \"{tag}\".", debugOnly: true); return mission; } else @@ -783,6 +860,20 @@ namespace Barotrauma return null; } + private void AddMission(Mission mission) + { + if (!mission.Prefab.AllowOtherMissionsInLevel) + { + availableMissions.Clear(); + } + availableMissions.Add(mission); +#if CLIENT + GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); +#else + (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); +#endif + } + private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection) { if (prefab.IsAllowed(this, this)) @@ -792,7 +883,7 @@ namespace Barotrauma } var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this))); - if (!suitableConnections.Any()) + if (suitableConnections.None()) { suitableConnections = Connections.ToList(); } @@ -877,6 +968,11 @@ namespace Barotrauma destination = Connections.First().OtherLocation(this); } var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub); + if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.Locations.Count) + { + mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex]; + } + mission.TimesAttempted = loadedMission.TimesAttempted; availableMissions.Add(mission); if (loadedMission.SelectedMission) { selectedMissions.Add(mission); } } @@ -978,12 +1074,22 @@ namespace Barotrauma private string RandomName(LocationType type, Random rand, IEnumerable existingLocations) { + if (!type.ForceLocationName.IsNullOrEmpty()) + { + baseName = type.ForceLocationName.Value; + return baseName; + } baseName = type.GetRandomName(rand, existingLocations); if (type.NameFormats == null || !type.NameFormats.Any()) { return baseName; } nameFormatIndex = rand.Next() % type.NameFormats.Count; return type.NameFormats[nameFormatIndex].Replace("[name]", baseName); } + public void ForceName(string name) + { + baseName = Name = name; + } + public void LoadStores(XElement locationElement) { UpdateStoreIdentifiers(); @@ -1068,13 +1174,21 @@ namespace Barotrauma public int GetAdjustedMechanicalCost(int cost) { - float discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f); - return (int) Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier); + float discount = 0.0f; + if (Reputation != null) + { + discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f); + } + return (int)Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier); } public int GetAdjustedHealCost(int cost) { - float discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f); + float discount = 0.0f; + if (Reputation != null) + { + discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f); + } return (int) Math.Ceiling((1.0f - discount) * cost * PriceMultiplier); } @@ -1258,32 +1372,6 @@ namespace Barotrauma } } - public float GetStoreReputationModifier(bool buying) - { - if (buying) - { - if (Reputation.Value > 0.0f) - { - return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation); - } - else - { - return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation); - } - } - else - { - if (Reputation.Value > 0.0f) - { - return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation); - } - else - { - return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation); - } - } - } - public static int GetExtraSpecialSalesCount() { var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); @@ -1314,14 +1402,14 @@ namespace Barotrauma { return LevelData != null && LevelData.OutpostGenerationParamsExist && - LevelData.GetSuitableOutpostGenerationParams(this).Any(p => p.CanHaveCampaignInteraction(interactionType)); + LevelData.GetSuitableOutpostGenerationParams(this, LevelData).Any(p => p.CanHaveCampaignInteraction(interactionType)); } - public void Reset() + public void Reset(CampaignMode campaign) { if (Type != OriginalType) { - ChangeType(OriginalType); + ChangeType(campaign, OriginalType); PendingLocationTypeChange = null; } CreateStores(force: true); @@ -1345,6 +1433,16 @@ namespace Barotrauma new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange), new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation), new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated)); + + if (Faction != null) + { + locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier)); + } + if (SecondaryFaction != null) + { + locationElement.Add(new XAttribute("secondaryfaction", SecondaryFaction.Prefab.Identifier)); + } + LevelData.Save(locationElement); for (int i = 0; i < Type.CanChangeTo.Count; i++) @@ -1403,6 +1501,7 @@ namespace Barotrauma { var storeElement = new XElement("store", new XAttribute("identifier", store.Identifier.Value), + new XAttribute(nameof(store.MerchantFaction), store.MerchantFaction), new XAttribute("balance", store.Balance), new XAttribute("pricemodifier", store.PriceModifier)); foreach (PurchasedItem item in store.Stock) @@ -1442,10 +1541,13 @@ namespace Barotrauma foreach (Mission mission in missions) { var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this); - var i = map.Locations.IndexOf(location); + var destinationIndex = map.Locations.IndexOf(location); + var originIndex = map.Locations.IndexOf(mission.OriginLocation); missionsElement.Add(new XElement("mission", new XAttribute("prefabid", mission.Prefab.Identifier), - new XAttribute("destinationindex", i), + new XAttribute("destinationindex", destinationIndex), + new XAttribute(nameof(Mission.TimesAttempted), mission.TimesAttempted), + new XAttribute("origin", originIndex), new XAttribute("selected", selectedMissions.Contains(mission)))); } locationElement.Add(missionsElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 2a97aa299..f2211a472 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -13,8 +13,8 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private readonly List names; - private readonly List portraits = new List(); + private readonly ImmutableArray names; + private readonly ImmutableArray portraits; // private readonly ImmutableArray<(Identifier Name, float Commonness)> hireableJobs; @@ -26,6 +26,8 @@ namespace Barotrauma public readonly LocalizedString Name; public readonly LocalizedString Description; + public readonly LocalizedString ForceLocationName; + public readonly float BeaconStationChance; public readonly CharacterTeamType OutpostTeam; @@ -39,7 +41,12 @@ namespace Barotrauma public bool IsEnterable { get; private set; } - public bool UseInMainMenu + /// + /// Can this location type be used in the random, non-campaign levels that don't take place in any specific zone + /// + public bool AllowInRandomLevels { get; private set; } + + public bool UsePortraitInRandomLoadingScreens { get; private set; @@ -96,6 +103,8 @@ namespace Barotrauma public int DailySpecialsCount { get; } = 1; public int RequestedGoodsCount { get; } = 1; + public readonly bool ShowSonarMarker = true; + public override string ToString() { return $"LocationType (" + Identifier + ")"; @@ -108,9 +117,12 @@ namespace Barotrauma BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); - UseInMainMenu = element.GetAttributeBool("useinmainmenu", false); + UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true); HasOutpost = element.GetAttributeBool("hasoutpost", true); IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); + AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true); + + ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true); MissionIdentifiers = element.GetAttributeIdentifierArray("missionidentifiers", Array.Empty()).ToImmutableArray(); MissionTags = element.GetAttributeIdentifierArray("missiontags", Array.Empty()).ToImmutableArray(); @@ -126,23 +138,31 @@ namespace Barotrauma string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC"); Enum.TryParse(teamStr, out OutpostTeam); - string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" }); - names = new List(); - foreach (string rawPath in rawNamePaths) + if (element.GetAttribute("name") != null) { - try - { - var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim()); - names.AddRange(File.ReadAllLines(path.Value).ToList()); - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e); - } + ForceLocationName = TextManager.Get(element.GetAttributeString("name", string.Empty)); } - if (!names.Any()) + else { - names.Add("ERROR: No names found"); + string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" }); + var names = new List(); + foreach (string rawPath in rawNamePaths) + { + try + { + var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim()); + names.AddRange(File.ReadAllLines(path.Value).ToList()); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e); + } + } + if (!names.Any()) + { + names.Add("ERROR: No names found"); + } + this.names = names.ToImmutableArray(); } string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty()); @@ -172,7 +192,7 @@ namespace Barotrauma } MinCountPerZone[zoneIndex] = minCount; } - + var portraits = new List(); var hireableJobs = new List<(Identifier, float)>(); foreach (var subElement in element.Elements()) { @@ -213,6 +233,7 @@ namespace Barotrauma break; } } + this.portraits = portraits.ToImmutableArray(); this.hireableJobs = hireableJobs.ToImmutableArray(); } @@ -229,10 +250,10 @@ namespace Barotrauma return null; } - public Sprite GetPortrait(int portraitId) + public Sprite GetPortrait(int randomSeed) { - if (portraits.Count == 0) { return null; } - return portraits[Math.Abs(portraitId) % portraits.Count]; + if (portraits.Length == 0) { return null; } + return portraits[Math.Abs(randomSeed) % portraits.Length]; } public string GetRandomName(Random rand, IEnumerable existingLocations) @@ -245,17 +266,34 @@ namespace Barotrauma return unusedNames[rand.Next() % unusedNames.Count]; } } - return names[rand.Next() % names.Count]; + return names[rand.Next() % names.Length]; } - public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false) + public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func predicate = null) { Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes"); LocationType[] allowedLocationTypes = - Prefabs.Where(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost)) + Prefabs.Where(lt => + (predicate == null || predicate(lt)) && IsValid(lt)) .OrderBy(p => p.UintIdentifier).ToArray(); + bool IsValid(LocationType lt) + { + if (requireOutpost && !lt.HasOutpost) { return false; } + if (zone.HasValue) + { + if (!lt.CommonnessPerZone.ContainsKey(zone.Value)) { return false; } + } + //if zone is not defined, this is a "random" (non-campaign) level + //-> don't choose location types that aren't allowed in those + else if (!lt.AllowInRandomLevels) + { + return false; + } + return true; + } + if (allowedLocationTypes.Length == 0) { DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index 0639ceb6f..8d9e03673 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; +using static Barotrauma.LocationTypeChange; namespace Barotrauma { @@ -58,7 +58,7 @@ namespace Barotrauma public Requirement(XElement element, LocationTypeChange change) { RequiredLocations = element.GetAttributeIdentifierArray("requiredlocations", element.GetAttributeIdentifierArray("requiredadjacentlocations", Array.Empty())).ToImmutableArray(); - RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1); + RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 0); ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1); RequireBeaconStation = element.GetAttributeBool("requirebeaconstation", false); @@ -91,37 +91,30 @@ namespace Barotrauma } } + public bool AnyWithinDistance(Location startLocation, int distance) + { + return Map.LocationOrConnectionWithinDistance( + startLocation, + maxDistance: distance, + criteria: MatchesLocation, + connectionCriteria: MatchesConnection); + } + public bool MatchesLocation(Location location) { return RequiredLocations.Contains(location.Type.Identifier) && !location.IsCriticallyRadiated(); } - public bool AnyWithinDistance(Location location, int maxDistance, int currentDistance = 0, HashSet checkedLocations = null) + public bool MatchesConnection(LocationConnection connection) { - if (currentDistance > maxDistance) { return false; } - if (currentDistance > 0 && MatchesLocation(location)) { return true; } - - checkedLocations ??= new HashSet(); - checkedLocations.Add(location); - - foreach (var connection in location.Connections) + if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive) { - if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive) - { - return true; - } - if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds) - { - return true; - } - - var otherLocation = connection.OtherLocation(location); - if (!checkedLocations.Contains(otherLocation)) - { - if (AnyWithinDistance(otherLocation, maxDistance, currentDistance + 1, checkedLocations)) { return true; } - } + return true; + } + if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds) + { + return true; } - return false; } } @@ -141,24 +134,25 @@ namespace Barotrauma private readonly bool requireChangeMessages; private readonly string messageTag; - private ImmutableArray? messages = null; - public IReadOnlyList Messages - { - get - { - if (!messages.HasValue) - { - messages = TextManager.GetAll(messageTag).ToImmutableArray(); - if (messages.Value.None()) - { - if (requireChangeMessages) - { - DebugConsole.ThrowError($"No messages defined for the location type change {CurrentType} -> {ChangeToType}"); - } - } - } - return messages.Value; + public IReadOnlyList GetMessages(Faction faction) + { + if (faction != null && TextManager.ContainsTag(messageTag + "." + faction.Prefab.Identifier)) + { + return TextManager.GetAll(messageTag + "." + faction.Prefab.Identifier).ToImmutableArray(); + } + + if (TextManager.ContainsTag(messageTag)) + { + return TextManager.GetAll(messageTag).ToImmutableArray(); + } + else + { + if (requireChangeMessages) + { + DebugConsole.ThrowError($"No messages defined for the location type change {CurrentType} -> {ChangeToType}"); + } + return Enumerable.Empty().ToImmutableArray(); } } @@ -226,8 +220,9 @@ namespace Barotrauma if (location.LocationTypeChangeCooldown > 0) { return 0.0f; } if (location.IsGateBetweenBiomes) { return 0.0f; } - if (DisallowedAdjacentLocations.Any() && - AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) + if (DisallowedAdjacentLocations.Any() && + Map.LocationOrConnectionWithinDistance(location, DisallowedProximity, + (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) { return 0.0f; } @@ -246,7 +241,6 @@ namespace Barotrauma probability *= requirement.Probability; } } - if (location.ProximityTimer.ContainsKey(requirement)) { if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease)) @@ -265,25 +259,5 @@ namespace Barotrauma return probability; } - - private bool AnyWithinDistance(Location location, int maxDistance, Func predicate, int currentDistance = 0, HashSet checkedLocations = null) - { - if (currentDistance > maxDistance) { return false; } - if (currentDistance > 0 && predicate(location)) { return true; } - - checkedLocations ??= new HashSet(); - checkedLocations.Add(location); - - foreach (var connection in location.Connections) - { - var otherLocation = connection.OtherLocation(location); - if (!checkedLocations.Contains(otherLocation)) - { - if (AnyWithinDistance(otherLocation, maxDistance, predicate, currentDistance + 1, checkedLocations)) { return true; } - } - } - - return false; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index df2c0679c..e361bd38b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -39,7 +39,8 @@ namespace Barotrauma /// public readonly NamedEvent OnLocationChanged = new NamedEvent(); - public Location EndLocation { get; private set; } + private List endLocations = new List(); + public IReadOnlyList EndLocations { get { return endLocations; } } public Location StartLocation { get; private set; } @@ -69,13 +70,13 @@ namespace Barotrauma public List Locations { get; private set; } private readonly List locationsDiscovered = new List(); - private readonly List outpostsVisited = new List(); + private readonly List locationsVisited = new List(); public List Connections { get; private set; } public Radiation Radiation; - private bool wasLocationDiscoveryOrderTracked = true; + private bool trackedLocationDiscoveryAndVisitOrder = true; public Map(CampaignSettings settings) { @@ -117,7 +118,7 @@ namespace Barotrauma Locations.Add(null); } lairsFound |= subElement.GetAttributeString("type", "").Equals("lair", StringComparison.OrdinalIgnoreCase); - Locations[i] = new Location(subElement); + Locations[i] = new Location(campaign, subElement); break; case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement) @@ -127,11 +128,6 @@ namespace Barotrauma break; } } - System.Diagnostics.Debug.Assert(!Locations.Contains(null)); - for (int i = 0; i < Locations.Count; i++) - { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient)); - } List connectionElements = new List(); foreach (var subElement in element.Elements()) @@ -187,23 +183,72 @@ namespace Barotrauma } } } - int endLocationindex = element.GetAttributeInt("endlocation", -1); - if (endLocationindex >= 0 && endLocationindex < Locations.Count) + + if (element.GetAttribute("endlocation") != null) { - EndLocation = Locations[endLocationindex]; + //backwards compatibility + int endLocationIndex = element.GetAttributeInt("endlocation", -1); + if (endLocationIndex >= 0 && endLocationIndex < Locations.Count) + { + endLocations.Add(Locations[endLocationIndex]); + } + else + { + DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count})."); + } } else { - DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationindex}, location count: {Locations.Count})."); - foreach (Location location in Locations) + int[] endLocationindices = element.GetAttributeIntArray("endlocations", Array.Empty()); + foreach (int endLocationIndex in endLocationindices) { - if (EndLocation == null || location.MapPosition.X > EndLocation.MapPosition.X) + if (endLocationIndex >= 0 && endLocationIndex < Locations.Count) { - EndLocation = location; + endLocations.Add(Locations[endLocationIndex]); + } + else + { + DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count})."); } } } + if (!endLocations.Any()) + { + DebugConsole.AddWarning($"Error while loading the map. No end location(s) found. Choosing the rightmost location as the end location..."); + Location endLocation = null; + foreach (Location location in Locations) + { + if (endLocation == null || location.MapPosition.X > endLocation.MapPosition.X) + { + endLocation = location; + } + } + endLocations.Add(endLocation); + } + + System.Diagnostics.Debug.Assert(endLocations.First().Biome != null, "End location biome was null."); + System.Diagnostics.Debug.Assert(endLocations.First().Biome.IsEndBiome, "The biome of the end location isn't the end biome."); + + //backwards compatibility (or support for loading maps created with mods that modify the end biome setup): + //if there's too few end locations, create more + int missingOutpostCount = endLocations.First().Biome.EndBiomeLocationCount - endLocations.Count; + + Location firstEndLocation = EndLocations[0]; + for (int i = 0; i < missingOutpostCount; i++) + { + Vector2 mapPos = new Vector2( + MathHelper.Lerp(firstEndLocation.MapPosition.X, Width, MathHelper.Lerp(0.2f, 0.8f, i / (float)missingOutpostCount)), + Height * MathHelper.Lerp(0.2f, 1.0f, (float)rand.NextDouble())); + var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations) + { + Biome = endLocations.First().Biome + }; + newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f); + Locations.Add(newEndLocation); + endLocations.Add(newEndLocation); + } + //backwards compatibility: if the map contained the now-removed lairs and has no hunting grounds, create some hunting grounds if (lairsFound && !Connections.Any(c => c.LevelData.HasHuntingGrounds)) { @@ -214,6 +259,17 @@ namespace Barotrauma } } + foreach (var endLocation in EndLocations) + { + if (endLocation.Type?.ForceLocationName != null && + !endLocation.Type.ForceLocationName.IsNullOrEmpty()) + { + endLocation.ForceName(endLocation.Type.ForceLocationName.Value); + } + } + + AssignEndLocationLevelData(); + //backwards compatibility: if locations go out of bounds (map saved with different generation parameters before width/height were included in the xml) float maxX = Locations.Select(l => l.MapPosition.X).Max(); if (maxX > Width) { Width = (int)(maxX + 10); } @@ -231,18 +287,13 @@ namespace Barotrauma Seed = seed; Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); - Generate(campaign.Settings); + Generate(campaign); if (Locations.Count == 0) { throw new Exception($"Generating a campaign map failed (no locations created). Width: {Width}, height: {Height}"); } - for (int i = 0; i < Locations.Count; i++) - { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient)); - } - foreach (Location location in Locations) { if (location.Type.Identifier != "outpost") { continue; } @@ -263,6 +314,20 @@ namespace Barotrauma if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) { CurrentLocation = StartLocation = furthestDiscoveredLocation = location; + StartLocation.SecondaryFaction = null; + var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost); + if (startOutpostFaction != null) + { + StartLocation.Faction = startOutpostFaction; + foreach (var connection in StartLocation.Connections) + { + var otherLocation = connection.OtherLocation(StartLocation); + if (otherLocation.HasOutpost() && otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) + { + otherLocation.Faction = startOutpostFaction; + } + } + } } } @@ -273,7 +338,7 @@ namespace Barotrauma { if (StartLocation != null) { - StartLocation.LevelData = new LevelData(StartLocation, 0); + StartLocation.LevelData = new LevelData(StartLocation, this, 0); } //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy @@ -289,7 +354,7 @@ namespace Barotrauma if (campaign.IsSinglePlayer && campaign.Settings.TutorialEnabled && LocationType.Prefabs.TryGet("tutorialoutpost", out var tutorialOutpost)) { - CurrentLocation.ChangeType(tutorialOutpost); + CurrentLocation.ChangeType(campaign, tutorialOutpost); } Discover(CurrentLocation); Visit(CurrentLocation); @@ -307,7 +372,7 @@ namespace Barotrauma #region Generation - private void Generate(CampaignSettings settings) + private void Generate(CampaignMode campaign) { Connections.Clear(); Locations.Clear(); @@ -525,12 +590,14 @@ namespace Barotrauma connectionsBetweenZones[zone1].Add(connection); } } - else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1]) + else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1] && + connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1]))) { connectionsBetweenZones[zone1].Add(connection); } } + var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList(); for (int i = Connections.Count - 1; i >= 0; i--) { int zone1 = GetZoneIndex(Connections[i].Locations[0].MapPosition.X); @@ -538,9 +605,9 @@ namespace Barotrauma if (zone1 == zone2) { continue; } if (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) { continue; } - if (generationParams.GateCount[Math.Min(zone1, zone2)] == 0) { continue; } - - if (!connectionsBetweenZones[Math.Min(zone1, zone2)].Contains(Connections[i])) + int leftZone = Math.Min(zone1, zone2); + if (generationParams.GateCount[leftZone] == 0) { continue; } + if (!connectionsBetweenZones[leftZone].Contains(Connections[i])) { Connections.RemoveAt(i); } @@ -552,11 +619,18 @@ namespace Barotrauma Connections[i].Locations[1]; if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned") { - leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"), + leftMostLocation.ChangeType( + campaign, + LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"), createStores: false); } leftMostLocation.IsGateBetweenBiomes = true; Connections[i].Locked = true; + + if (leftMostLocation.Type.HasOutpost && campaign != null && gateFactions.Any()) + { + leftMostLocation.Faction = gateFactions[connectionsBetweenZones[leftZone].IndexOf(Connections[i]) % gateFactions.Count]; + } } } @@ -624,21 +698,27 @@ namespace Barotrauma } } - CreateEndLocation(); - foreach (Location location in Locations) { - location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome)); + location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome)); + if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) + { + location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); + location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient); + } location.CreateStores(force: true); } + foreach (LocationConnection connection in Connections) { connection.LevelData = new LevelData(connection); } + CreateEndLocation(campaign); + float CalculateDifficulty(float mapPosition, Biome biome) { - float settingsFactor = settings.LevelDifficultyMultiplier; + float settingsFactor = campaign.Settings.LevelDifficultyMultiplier; float minDifficulty = 0; float maxDifficulty = 100; float difficulty = mapPosition / Width * 100; @@ -707,18 +787,18 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null)); } - private void CreateEndLocation() + private void CreateEndLocation(CampaignMode campaign) { float zoneWidth = Width / generationParams.DifficultyZones; - Vector2 endPos = new Vector2(Width - zoneWidth / 2, Height / 2); + Vector2 endPos = new Vector2(Width - zoneWidth * 0.7f, Height / 2); float closestDist = float.MaxValue; - EndLocation = Locations.First(); + var endLocation = Locations.First(); foreach (Location location in Locations) { float dist = Vector2.DistanceSquared(endPos, location.MapPosition); if (location.Biome.IsEndBiome && dist < closestDist) { - EndLocation = location; + endLocation = location; closestDist = dist; } } @@ -732,17 +812,39 @@ namespace Barotrauma } } - if (EndLocation == null || previousToEndLocation == null) { return; } + if (endLocation == null || previousToEndLocation == null) { return; } + + endLocations = new List() { endLocation }; + if (endLocation.Biome.EndBiomeLocationCount > 1) + { + FindConnectedEndLocations(endLocation); + + void FindConnectedEndLocations(Location currLocation) + { + if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; } + foreach (var connection in currLocation.Connections) + { + if (connection.Biome != endLocation.Biome) { continue; } + var otherLocation = connection.OtherLocation(currLocation); + if (otherLocation != null && !endLocations.Contains(otherLocation)) + { + if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; } + endLocations.Add(otherLocation); + FindConnectedEndLocations(otherLocation); + } + } + } + } if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) { - previousToEndLocation.ChangeType(locationType, createStores: false); + previousToEndLocation.ChangeType(campaign, locationType, createStores: false); } //remove all locations from the end biome except the end location for (int i = Locations.Count - 1; i >= 0; i--) { - if (Locations[i].Biome.IsEndBiome && Locations[i] != EndLocation) + if (Locations[i].Biome.IsEndBiome) { for (int j = Locations[i].Connections.Count - 1; j >= 0; j--) { @@ -753,7 +855,10 @@ namespace Barotrauma otherLocation?.Connections.Remove(connection); Connections.Remove(connection); } - Locations.RemoveAt(i); + if (!endLocations.Contains(Locations[i])) + { + Locations.RemoveAt(i); + } } } @@ -770,22 +875,39 @@ namespace Barotrauma } var newConnection = new LocationConnection(previousToEndLocation, connectTo) { - Biome = EndLocation.Biome, + Biome = endLocation.Biome, Difficulty = 100.0f }; + newConnection.LevelData = new LevelData(newConnection); Connections.Add(newConnection); previousToEndLocation.Connections.Add(newConnection); connectTo.Connections.Add(newConnection); } - var endConnection = new LocationConnection(previousToEndLocation, EndLocation) + var endConnection = new LocationConnection(previousToEndLocation, endLocation) { - Biome = EndLocation.Biome, + Biome = endLocation.Biome, Difficulty = 100.0f }; + endConnection.LevelData = new LevelData(endConnection); Connections.Add(endConnection); previousToEndLocation.Connections.Add(endConnection); - EndLocation.Connections.Add(endConnection); + endLocation.Connections.Add(endConnection); + + AssignEndLocationLevelData(); + } + + private void AssignEndLocationLevelData() + { + for (int i = 0; i < endLocations.Count; i++) + { + endLocations[i].LevelData.ReassignGenerationParams(Seed); + var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i); + if (outpostParams != null) + { + endLocations[i].LevelData.ForceOutpostGenerationParams = outpostParams; + } + } } private void ExpandBiomes(List seeds) @@ -817,19 +939,48 @@ namespace Barotrauma public void MoveToNextLocation() { + if (SelectedLocation == null && Level.Loaded?.EndLocation != null) + { + //force the location at the end of the level to be selected, even if it's been deselect on the map + //(e.g. due to returning to an empty location the beginning of the level during the round) + SelectLocation(Level.Loaded.EndLocation); + } if (SelectedConnection == null) { - DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n"+Environment.StackTrace.CleanupStackTrace()); - return; + if (!endLocations.Contains(CurrentLocation)) + { + DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace()); + return; + } } if (SelectedLocation == null) { - DebugConsole.ThrowError("Could not move to the next location (no location selected).\n" + Environment.StackTrace.CleanupStackTrace()); - return; + if (endLocations.Contains(CurrentLocation)) + { + int currentEndLocationIndex = endLocations.IndexOf(CurrentLocation); + if (currentEndLocationIndex < endLocations.Count - 1) + { + //more end locations to go, progress to the next one + SelectedLocation = endLocations[currentEndLocationIndex + 1]; + } + else + { + //at the last end location, end of campaign + SelectedLocation = StartLocation; + } + } + else + { + DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace()); + return; + } } Location prevLocation = CurrentLocation; - SelectedConnection.Passed = true; + if (SelectedConnection != null) + { + SelectedConnection.Passed = true; + } CurrentLocation = SelectedLocation; Discover(CurrentLocation); @@ -898,12 +1049,24 @@ namespace Barotrauma Location prevSelected = SelectedLocation; SelectedLocation = Locations[index]; var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); - SelectedConnection = - Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ?? - Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + if (currentDisplayLocation == SelectedLocation) + { + SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + } + else + { + SelectedConnection = + Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ?? + Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + } if (SelectedConnection?.Locked ?? false) { - DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace()); + string errorMsg = + $"A locked connection was selected ({SelectedConnection.Locations[0].Name} -> {SelectedConnection.Locations[1].Name}." + + $" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n" + + Environment.StackTrace.CleanupStackTrace(); + GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.ThrowError(errorMsg); } if (prevSelected != SelectedLocation) { @@ -979,7 +1142,7 @@ namespace Barotrauma } } - public void ProgressWorld(CampaignMode.TransitionType transitionType, float roundDuration) + public void ProgressWorld(CampaignMode campaign, CampaignMode.TransitionType transitionType, float roundDuration) { //one step per 10 minutes of play time int steps = (int)Math.Floor(roundDuration / (60.0f * 10.0f)); @@ -992,7 +1155,7 @@ namespace Barotrauma steps = Math.Min(steps, 5); for (int i = 0; i < steps; i++) { - ProgressWorld(); + ProgressWorld(campaign); } // always update specials every step @@ -1008,7 +1171,7 @@ namespace Barotrauma Radiation?.OnStep(steps); } - private void ProgressWorld() + private void ProgressWorld(CampaignMode campaign) { foreach (Location location in Locations) { @@ -1036,14 +1199,14 @@ namespace Barotrauma if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; } - if (!ProgressLocationTypeChanges(location) && location.Discovered) + if (!ProgressLocationTypeChanges(campaign, location) && location.Discovered) { location.UpdateStores(); } } } - private bool ProgressLocationTypeChanges(Location location) + private bool ProgressLocationTypeChanges(CampaignMode campaign, Location location) { location.TimeSinceLastTypeChange++; location.LocationTypeChangeCooldown--; @@ -1063,7 +1226,7 @@ namespace Barotrauma location.PendingLocationTypeChange.Value.parentMission); if (location.PendingLocationTypeChange.Value.delay <= 0) { - return ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange); + return ChangeLocationType(campaign, location, location.PendingLocationTypeChange.Value.typeChange); } } } @@ -1096,7 +1259,7 @@ namespace Barotrauma } else { - return ChangeLocationType(location, selectedTypeChange); + return ChangeLocationType(campaign, location, selectedTypeChange); } return false; } @@ -1121,52 +1284,7 @@ namespace Barotrauma return false; } - public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation) - { - if (startingLocation.Type.HasOutpost) - { - endingLocation = startingLocation; - return 0; - } - - int iterations = 0; - int distance = 0; - endingLocation = null; - - List testedLocations = new List(); - List locationsToTest = new List { startingLocation }; - - while (endingLocation == null && iterations < 100) - { - List nextTestingBatch = new List(); - for (int i = 0; i < locationsToTest.Count; i++) - { - Location testLocation = locationsToTest[i]; - for (int j = 0; j < testLocation.Connections.Count; j++) - { - Location potentialOutpost = testLocation.Connections[j].OtherLocation(testLocation); - if (potentialOutpost.Type.HasOutpost) - { - distance = iterations + 1; - endingLocation = potentialOutpost; - } - else if (!testedLocations.Contains(potentialOutpost)) - { - nextTestingBatch.Add(potentialOutpost); - } - } - - testedLocations.Add(testLocation); - } - - locationsToTest = nextTestingBatch; - iterations++; - } - - return distance; - } - - private bool ChangeLocationType(Location location, LocationTypeChange change) + private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change) { string prevName = location.Name; @@ -1176,12 +1294,14 @@ namespace Barotrauma return false; } + if (location.LocationTypeChangesBlocked) { return false; } + if (newType.OutpostTeam != location.Type.OutpostTeam || newType.HasOutpost != location.Type.HasOutpost) { location.ClearMissions(); } - location.ChangeType(newType); + location.ChangeType(campaign, newType); ChangeLocationTypeProjSpecific(location, prevName, change); foreach (var requirement in change.Requirements) { @@ -1193,6 +1313,50 @@ namespace Barotrauma return true; } + public static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func criteria, Func connectionCriteria = null) + { + return GetDistanceToClosestLocationOrConnection(startLocation, maxDistance, criteria, connectionCriteria) <= maxDistance; + } + + /// + /// Get the shortest distance from the start location to another location that satisfies the specified criteria. + /// + /// The distance to a matching location, or int.MaxValue if none are found. + public static int GetDistanceToClosestLocationOrConnection(Location startLocation, int maxDistance, Func criteria, Func connectionCriteria = null) + { + int distance = 0; + var locationsToTest = new List() { startLocation }; + var nextBatchToTest = new HashSet(); + var checkedLocations = new HashSet(); + while (locationsToTest.Any()) + { + foreach (var location in locationsToTest) + { + checkedLocations.Add(location); + if (criteria(location)) { return distance; } + foreach (var connection in location.Connections) + { + if (connectionCriteria != null && connectionCriteria(connection)) + { + return distance; + } + var otherLocation = connection.OtherLocation(location); + if (!checkedLocations.Contains(otherLocation)) + { + nextBatchToTest.Add(otherLocation); + } + } + if (distance > maxDistance) { return int.MaxValue; } + } + distance++; + locationsToTest.Clear(); + locationsToTest.AddRange(nextBatchToTest); + nextBatchToTest.Clear(); + } + return int.MaxValue; + } + + partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change); partial void ClearAnimQueue(); @@ -1211,29 +1375,38 @@ namespace Barotrauma public void Visit(Location location) { if (location is null) { return; } - if (!location.HasOutpost()) { return; } - if (outpostsVisited.Contains(location)) { return; } - outpostsVisited.Add(location); + if (locationsVisited.Contains(location)) { return; } + locationsVisited.Add(location); + RemoveFogOfWarProjSpecific(location); } public void ClearLocationHistory() { locationsDiscovered.Clear(); - outpostsVisited.Clear(); + locationsVisited.Clear(); } public int? GetDiscoveryIndex(Location location) { - if (!wasLocationDiscoveryOrderTracked) { return null; } + if (!trackedLocationDiscoveryAndVisitOrder) { return null; } if (location is null) { return -1; } return locationsDiscovered.IndexOf(location); } - public int? GetVisitIndex(Location location) + public int? GetVisitIndex(Location location, bool includeLocationsWithoutOutpost = false) { - if (!wasLocationDiscoveryOrderTracked) { return null; } + if (!trackedLocationDiscoveryAndVisitOrder) { return null; } if (location is null) { return -1; } - return outpostsVisited.IndexOf(location); + int index = locationsVisited.IndexOf(location); + if (includeLocationsWithoutOutpost) { return index; } + int noOutpostLocations = 0; + for (int i = 0; i < index; i++) + { + if (locationsVisited[i] is not Location l) { continue; } + if (l.HasOutpost()) { continue; } + noOutpostLocations++; + } + return index - noOutpostLocations; } public bool IsDiscovered(Location location) @@ -1242,13 +1415,21 @@ namespace Barotrauma return locationsDiscovered.Contains(location); } + public bool IsVisited(Location location) + { + if (location is null) { return false; } + return locationsVisited.Contains(location); + } + + partial void RemoveFogOfWarProjSpecific(Location location); + /// /// Load a previously saved map from an xml element /// public static Map Load(CampaignMode campaign, XElement element) { Map map = new Map(campaign, element); - map.LoadState(element, false); + map.LoadState(campaign, element, false); #if CLIENT map.DrawOffset = -map.CurrentLocation.MapPosition; #endif @@ -1258,12 +1439,12 @@ namespace Barotrauma /// /// Load the state of an existing map from xml (current state of locations, where the crew is now, etc). /// - public void LoadState(XElement element, bool showNotifications) + public void LoadState(CampaignMode campaign, XElement element, bool showNotifications) { ClearAnimQueue(); SetLocation(element.GetAttributeInt("currentlocation", 0)); - if (!Version.TryParse(element.GetAttributeString("version", ""), out _)) + if (!Version.TryParse(element.GetAttributeString("version", ""), out Version version)) { DebugConsole.ThrowError("Incompatible map save file, loading the game failed."); return; @@ -1275,7 +1456,13 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "location": - Location location = Locations[subElement.GetAttributeInt("i", 0)]; + int locationIndex = subElement.GetAttributeInt("i", -1); + if (locationIndex < 0 || locationIndex >= Locations.Count) + { + DebugConsole.AddWarning($"Error while loading the campaign map: location index out of bounds ({locationIndex})"); + continue; + } + Location location = Locations[locationIndex]; location.ProximityTimer.Clear(); for (int i = 0; i < location.Type.CanChangeTo.Count; i++) { @@ -1286,18 +1473,20 @@ namespace Barotrauma } location.LoadLocationTypeChange(subElement); - // Backwards compatibility + // Backwards compatibility: if the discovery status is defined in the location element, + // the game was saved using when the discovery order still wasn't being tracked if (subElement.GetAttributeBool("discovered", false)) { Discover(location); - wasLocationDiscoveryOrderTracked = false; + Visit(location); + trackedLocationDiscoveryAndVisitOrder = false; } Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty); string prevLocationName = location.Name; LocationType prevLocationType = location.Type; LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First(); - location.ChangeType(newLocationType); + location.ChangeType(campaign, newLocationType); if (showNotifications && prevLocationType != location.Type) { var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier); @@ -1308,45 +1497,72 @@ namespace Barotrauma } } + var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty); + location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier); + + var secondaryFactionIdentifier = subElement.GetAttributeIdentifier("secondaryfaction", Identifier.Empty); + location.SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier); + location.LoadStores(subElement); location.LoadMissions(subElement); break; case "connection": - int connectionIndex = subElement.GetAttributeInt("i", 0); + //the index wasn't saved previously, skip if that's the case + if (subElement.Attribute("i") == null) { continue; } + + int connectionIndex = subElement.GetAttributeInt("i", -1); + if (connectionIndex < 0 || connectionIndex >= Connections.Count) + { + DebugConsole.AddWarning($"Error while loading the campaign map: connection index out of bounds ({connectionIndex})"); + continue; + } Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false); - Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false); + Connections[connectionIndex].Locked = subElement.GetAttributeBool("locked", false); break; case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement); break; case "discovered": + bool trackedVisitedEmptyLocations = subElement.GetAttributeBool("trackedvisitedemptylocations", false); foreach (var childElement in subElement.GetChildElements("location")) { - int index = childElement.GetAttributeInt("i", -1); - if (index < 0) { continue; } - if (Locations[index] is not Location l) { continue; } - Discover(l); + if (GetLocation(childElement) is Location l) + { + Discover(l); + if (!trackedVisitedEmptyLocations) + { + if (!l.HasOutpost()) + { + Visit(l); + } + trackedLocationDiscoveryAndVisitOrder = false; + } + } } break; case "visited": foreach (var childElement in subElement.GetChildElements("location")) { - int index = childElement.GetAttributeInt("i", -1); - if (index < 0) { continue; } - if (Locations[index] is not Location l) { continue; } - Visit(l); + if (GetLocation(childElement) is Location l) + { + Visit(l); + } } break; } + + Location GetLocation(XElement element) + { + int index = element.GetAttributeInt("i", -1); + if (index < 0) { return null; } + return Locations[index]; + } } void Discover(Location location) { this.Discover(location, checkTalents: false); -#if CLIENT - RemoveFogOfWar(location); -#endif if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { furthestDiscoveredLocation = location; @@ -1358,6 +1574,24 @@ namespace Barotrauma location?.InstantiateLoadedMissions(this); } + //backwards compatibility: + //if the save is from a version prior to the addition of faction-specific outposts, assign factions + if (version < new Version(1, 0) && Locations.None(l => l.Faction != null || l.SecondaryFaction != null)) + { + Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); + foreach (Location location in Locations) + { + if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) + { + location.Faction = campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); + if (location != StartLocation) + { + location.SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient); + } + } + } + } + int currentLocationConnection = element.GetAttributeInt("currentlocationconnection", -1); if (currentLocationConnection >= 0) { @@ -1396,7 +1630,7 @@ namespace Barotrauma mapElement.Add(new XAttribute("height", Height)); mapElement.Add(new XAttribute("selectedlocation", SelectedLocationIndex)); mapElement.Add(new XAttribute("startlocation", Locations.IndexOf(StartLocation))); - mapElement.Add(new XAttribute("endlocation", Locations.IndexOf(EndLocation))); + mapElement.Add(new XAttribute("endlocations", string.Join(',', EndLocations.Select(e => Locations.IndexOf(e))))); mapElement.Add(new XAttribute("seed", Seed)); for (int i = 0; i < Locations.Count; i++) @@ -1415,6 +1649,7 @@ namespace Barotrauma new XAttribute("locked", connection.Locked), new XAttribute("difficulty", connection.Difficulty), new XAttribute("biome", connection.Biome.Identifier), + new XAttribute("i", i), new XAttribute("locations", Locations.IndexOf(connection.Locations[0]) + "," + Locations.IndexOf(connection.Locations[1]))); connection.LevelData.Save(connectionElement); mapElement.Add(connectionElement); @@ -1427,7 +1662,8 @@ namespace Barotrauma if (locationsDiscovered.Any()) { - var discoveryElement = new XElement("discovered"); + var discoveryElement = new XElement("discovered", + new XAttribute("trackedvisitedemptylocations", true)); foreach (Location location in locationsDiscovered) { int index = Locations.IndexOf(location); @@ -1437,10 +1673,10 @@ namespace Barotrauma mapElement.Add(discoveryElement); } - if (outpostsVisited.Any()) + if (locationsVisited.Any()) { var visitElement = new XElement("visited"); - foreach (Location location in outpostsVisited) + foreach (Location location in locationsVisited) { int index = Locations.IndexOf(location); var locationElement = new XElement("location", new XAttribute("i", index)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index e8489c131..dd9c99176 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -19,9 +19,6 @@ namespace Barotrauma protected List linkedToID; public List unresolvedLinkedToID; - private const int GapUpdateInterval = 4; - private static int gapUpdateTimer; - /// /// List of upgrades this item has /// @@ -59,7 +56,24 @@ namespace Barotrauma //the position and dimensions of the entity protected Rectangle rect; - public bool ExternalHighlight = false; + protected static readonly HashSet highlightedEntities = new HashSet(); + + public static IEnumerable HighlightedEntities => highlightedEntities; + + + private bool externalHighlight = false; + public bool ExternalHighlight + { + get { return externalHighlight; } + set + { + if (value != externalHighlight) + { + externalHighlight = value; + CheckIsHighlighted(); + } + } + } //is the mouse inside the rect private bool isHighlighted; @@ -67,7 +81,14 @@ namespace Barotrauma public bool IsHighlighted { get { return isHighlighted || ExternalHighlight; } - set { isHighlighted = value; } + set + { + if (value != IsHighlighted) + { + isHighlighted = value; + CheckIsHighlighted(); + } + } } public virtual Rectangle Rect @@ -158,7 +179,7 @@ namespace Barotrauma { if (!float.IsNaN(value)) { - _spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999f); + _spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999999f); if (this is Item) { _spriteOverrideDepth = Math.Min(_spriteOverrideDepth, 0.9f); } SpriteDepthOverrideIsSet = true; } @@ -362,6 +383,31 @@ namespace Barotrauma return true; } + protected virtual void CheckIsHighlighted() + { + if (IsHighlighted || ExternalHighlight) + { + highlightedEntities.Add(this); + } + else + { + highlightedEntities.Remove(this); + } + } + + private static readonly List tempHighlightedEntities = new List(); + public static void ClearHighlightedEntities() + { + highlightedEntities.RemoveWhere(e => e.Removed); + tempHighlightedEntities.Clear(); + tempHighlightedEntities.AddRange(highlightedEntities); + foreach (var entity in tempHighlightedEntities) + { + entity.IsHighlighted = false; + } + } + + public abstract MapEntity Clone(); public static List Clone(List entitiesToClone) @@ -458,7 +504,7 @@ namespace Barotrauma } (clones[itemIndex] as Item).Connections[connectionIndex].TryAddLink(cloneWire); - cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], false); + cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], n, addNode: false); } if ((cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && cloneItem.GetComponent() == null) @@ -579,14 +625,9 @@ namespace Barotrauma //the water/air will always tend to flow through the first gap in the list, //which may lead to weird behavior like water draining down only through //one gap in a room even if there are several - gapUpdateTimer++; - if (gapUpdateTimer >= GapUpdateInterval) + foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue))) { - foreach (Gap gap in Gap.GapList.OrderBy(g => Rand.Int(int.MaxValue))) - { - gap.Update(deltaTime * GapUpdateInterval, cam); - } - gapUpdateTimer = 0; + gap.Update(deltaTime, cam); } #if CLIENT @@ -649,6 +690,9 @@ namespace Barotrauma List entities = new List(); foreach (var element in parentElement.Elements()) { +#if CLIENT + GameMain.GameSession?.Campaign?.ThrowIfStartRoundCancellationRequested(); +#endif string typeName = element.Name.ToString(); Type t; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs index 7d9f33be6..feec0ea3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs @@ -17,6 +17,9 @@ namespace Barotrauma [Serialize(100.0f, IsPropertySaveable.Yes), Editable] public float MaxLevelDifficulty { get; set; } + [Serialize(Level.PlacementType.Bottom, IsPropertySaveable.Yes), Editable] + public Level.PlacementType Placement { get; set; } + public string Name { get; private set; } public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs index 965965805..8be567543 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs @@ -1,10 +1,6 @@ #nullable enable -using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -12,23 +8,23 @@ namespace Barotrauma { public readonly static PrefabCollection Sets = new PrefabCollection(); - private readonly ImmutableArray Humans; - private bool Disposed { get; set; } - public NPCSet(ContentXElement element, NPCSetsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { Humans = element.Elements().Select(npcElement => new HumanPrefab(npcElement, file, Identifier)).ToImmutableArray(); } - public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier) + public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier, bool logError = true) { HumanPrefab? prefab = Sets.Where(set => set.Identifier == setIdentifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault(); if (prefab == null) { - DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\"."); + if (logError) + { + DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\"."); + } return null; } return prefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index f91f5e1a6..35a758882 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -23,77 +23,106 @@ namespace Barotrauma get { return allowedLocationTypes; } } - [Serialize(10, IsPropertySaveable.Yes), Editable(MinValueInt = 1, MaxValueInt = 50)] + + [Serialize(-1, IsPropertySaveable.Yes, description: "Should this type of outpost be forced to the locations at the end of the campaign map? 0 = first end level, 1 = second end level, and so on."), Editable(MinValueInt = -1, MaxValueInt = 10)] + public int ForceToEndLocationIndex + { + get; + set; + } + + + [Serialize(10, IsPropertySaveable.Yes, description: "Total number of modules in the outpost."), Editable(MinValueInt = 1, MaxValueInt = 50)] public int TotalModuleCount { get; set; } - [Serialize(200.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the generator append generic (module flag \"none\") modules to the outpost to reach the total module count."), Editable] + public bool AppendToReachTotalModuleCount + { + get; + set; + } + + [Serialize(200.0f, IsPropertySaveable.Yes, description: "Minimum length of the hallways between modules. If 0, the generator will place the modules directly against each other assuming it can be done without making any modules overlap."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float MinHallwayLength { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should this outpost always be destructible, regardless if damaging outposts is allowed by the server?"), Editable] public bool AlwaysDestructible { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should this outpost always be rewireable, regardless if rewiring is allowed by the server?"), Editable] public bool AlwaysRewireable { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should stealing from this outpost be always allowed?"), Editable] public bool AllowStealing { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the crew spawn inside the outpost (if not, they'll spawn in the submarine)."), Editable] public bool SpawnCrewInsideOutpost { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should doors at the edges of an outpost module that didn't get connected to another module be locked?"), Editable] public bool LockUnusedDoors { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should gaps at the edges of an outpost module that didn't get connected to another module be removed?"), Editable] public bool RemoveUnusedGaps { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the whole outpost render behind submarines? Only set this to true if the submarine is intended to go inside the outpost."), Editable] + public bool DrawBehindSubs + { + get; + set; + } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum amount of water in the hulls of the outpost."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MinWaterPercentage { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Maximum amount of water in the hulls of the outpost."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MaxWaterPercentage { get; set; } - [Serialize("", IsPropertySaveable.Yes), Editable] + public LevelData.LevelType? LevelType + { + get; + set; + } + + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the outpost generation parameters that should be used if this outpost has become critically irradiated."), Editable] public string ReplaceInRadiation { get; set; } public ContentPath OutpostFilePath { get; set; } @@ -104,17 +133,21 @@ namespace Barotrauma public int Count; public int Order; + public Identifier RequiredFaction; + public ModuleCount(ContentXElement element) { Identifier = element.GetAttributeIdentifier("flag", element.GetAttributeIdentifier("moduletype", "")); Count = element.GetAttributeInt("count", 0); Order = element.GetAttributeInt("order", 0); + RequiredFaction = element.GetAttributeIdentifier("requiredfaction", Identifier.Empty); } public ModuleCount(Identifier id, int count) { Identifier = id; Count = count; + RequiredFaction = Identifier.Empty; } } @@ -132,16 +165,20 @@ namespace Barotrauma private readonly HumanPrefab humanPrefab = null; private readonly Identifier setIdentifier = Identifier.Empty; private readonly Identifier npcIdentifier = Identifier.Empty; + + public readonly Identifier FactionIdentifier = Identifier.Empty; - public Entry(HumanPrefab humanPrefab) + public Entry(HumanPrefab humanPrefab, Identifier factionIdentifier) { this.humanPrefab = humanPrefab; + this.FactionIdentifier = factionIdentifier; } - public Entry(Identifier setIdentifier, Identifier npcIdentifier) + public Entry(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier) { this.setIdentifier = setIdentifier; this.npcIdentifier = npcIdentifier; + this.FactionIdentifier = factionIdentifier; } public HumanPrefab HumanPrefab @@ -150,29 +187,41 @@ namespace Barotrauma private readonly List entries = new List(); - public void Add(HumanPrefab humanPrefab) - => entries.Add(new Entry(humanPrefab)); + public void Add(HumanPrefab humanPrefab, Identifier factionIdentifier) + => entries.Add(new Entry(humanPrefab, factionIdentifier)); - public void Add(Identifier setIdentifier, Identifier npcIdentifier) - => entries.Add(new Entry(setIdentifier, npcIdentifier)); + public void Add(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier) + => entries.Add(new Entry(setIdentifier, npcIdentifier, factionIdentifier)); public IEnumerator GetEnumerator() { foreach (var entry in entries) { + if (entry == null) { continue; } yield return entry.HumanPrefab; } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerable GetByFaction(IEnumerable factions) + { + foreach (var entry in entries) + { + if (entry.FactionIdentifier == Identifier.Empty || factions.Any(f => f.Identifier == entry.FactionIdentifier)) + { + yield return entry.HumanPrefab; + } + } + } + public int Count => entries.Count; public HumanPrefab this[int index] => entries[index].HumanPrefab; } - private readonly ImmutableArray> humanPrefabCollections; + private readonly ImmutableArray humanPrefabCollections; public Dictionary SerializableProperties { get; private set; } @@ -184,9 +233,23 @@ namespace Barotrauma Name = element.GetAttributeString("name", Identifier.Value); allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty()).ToHashSet(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + + if (element.GetAttribute("leveltype") != null) + { + string levelTypeStr = element.GetAttributeString("leveltype", ""); + if (Enum.TryParse(levelTypeStr, out LevelData.LevelType parsedLevelType)) + { + LevelType = parsedLevelType; + } + else + { + DebugConsole.ThrowError($"Error in outpost generation parameters \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); + } + } + OutpostFilePath = element.GetAttributeContentPath(nameof(OutpostFilePath)); - var humanPrefabCollections = new List>(); + var humanPrefabCollections = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -199,14 +262,14 @@ namespace Barotrauma foreach (var npcElement in subElement.Elements()) { Identifier from = npcElement.GetAttributeIdentifier("from", Identifier.Empty); - + Identifier faction = npcElement.GetAttributeIdentifier("faction", Identifier.Empty); if (from != Identifier.Empty) { - newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty)); + newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty), faction); } else { - newCollection.Add(new HumanPrefab(npcElement, file, npcSetIdentifier: from)); + newCollection.Add(new HumanPrefab(npcElement, file, npcSetIdentifier: from), faction); } } humanPrefabCollections.Add(newCollection); @@ -254,10 +317,12 @@ namespace Barotrauma } } - public IReadOnlyList GetHumanPrefabs(Rand.RandSync randSync) + public IReadOnlyList GetHumanPrefabs(IEnumerable factions, Rand.RandSync randSync) { if (!humanPrefabCollections.Any()) { return Array.Empty(); } - return humanPrefabCollections.GetRandom(randSync); + + var collection = humanPrefabCollections.GetRandom(randSync); + return collection.GetByFaction(factions).ToImmutableList(); } public bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType) @@ -266,7 +331,7 @@ namespace Barotrauma { foreach (var prefab in collection) { - if (prefab.CampaignInteractionType == interactionType) + if (prefab != null && prefab.CampaignInteractionType == interactionType) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 414c4c8da..c0f436f27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -150,11 +150,14 @@ namespace Barotrauma selectedModules.Clear(); //select which module types the outpost should consist of - List pendingModuleFlags = - onlyEntrance ? - (generationParams.ModuleCounts.FirstOrDefault()?.Identifier.ToEnumerable() ?? Enumerable.Empty()).ToList() : - SelectModules(outpostModules, generationParams); - + List pendingModuleFlags = new List(); + if (generationParams.ModuleCounts.Any()) + { + pendingModuleFlags = onlyEntrance ? + generationParams.ModuleCounts[0].Identifier.ToEnumerable().ToList() : + SelectModules(outpostModules, location, generationParams); + } + foreach (Identifier flag in pendingModuleFlags) { if (flag == "none") { continue; } @@ -246,12 +249,17 @@ namespace Barotrauma wp.FindHull(); } } + EnableFactionSpecificEntities(sub, location); return sub; } remainingTries--; } +#if DEBUG + DebugConsole.ThrowError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead..."); +#else DebugConsole.NewMessage("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead..."); +#endif var outpostFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) @@ -268,6 +276,7 @@ namespace Barotrauma sub = new Submarine(prebuiltOutpostInfo); sub.Info.OutpostGenerationParams = generationParams; location?.RemoveTakenItems(); + EnableFactionSpecificEntities(sub, location); return sub; List loadEntities(Submarine sub) @@ -297,7 +306,7 @@ namespace Barotrauma } idOffset = moduleEntities.Max(e => e.ID) + 1; - var wallEntities = moduleEntities.Where(e => e is Structure).Cast(); + var wallEntities = moduleEntities.Where(e => e is Structure s && s.HasBody).Cast(); var hullEntities = moduleEntities.Where(e => e is Hull).Cast(); // Tell the hulls what tags the module has, used to spawn NPCs on specific rooms @@ -306,18 +315,27 @@ namespace Barotrauma hull.SetModuleTags(selectedModule.Info.OutpostModuleInfo.ModuleFlags); } - selectedModule.HullBounds = new Rectangle( - hullEntities.Min(e => e.WorldRect.X), hullEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height), - hullEntities.Max(e => e.WorldRect.Right), hullEntities.Max(e => e.WorldRect.Y)); - selectedModule.HullBounds = new Rectangle( - selectedModule.HullBounds.X, selectedModule.HullBounds.Y, - selectedModule.HullBounds.Width - selectedModule.HullBounds.X, selectedModule.HullBounds.Height - selectedModule.HullBounds.Y); - selectedModule.Bounds = new Rectangle( - wallEntities.Min(e => e.WorldRect.X), wallEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height), - wallEntities.Max(e => e.WorldRect.Right), wallEntities.Max(e => e.WorldRect.Y)); - selectedModule.Bounds = new Rectangle( - selectedModule.Bounds.X, selectedModule.Bounds.Y, - selectedModule.Bounds.Width - selectedModule.Bounds.X, selectedModule.Bounds.Height - selectedModule.Bounds.Y); + if (!hullEntities.Any()) + { + selectedModule.HullBounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint()); + } + else + { + Point min = new Point(hullEntities.Min(e => e.WorldRect.X), hullEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height)); + Point max = new Point(hullEntities.Max(e => e.WorldRect.Right), hullEntities.Max(e => e.WorldRect.Y)); + selectedModule.HullBounds = new Rectangle(min, max - min); + } + + if (!wallEntities.Any()) + { + selectedModule.Bounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint()); + } + else + { + Point min = new Point(wallEntities.Min(e => e.WorldRect.X), wallEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height)); + Point max = new Point(wallEntities.Max(e => e.WorldRect.Right), wallEntities.Max(e => e.WorldRect.Y)); + selectedModule.Bounds = new Rectangle(min, max - min); + } if (selectedModule.PreviousModule != null) { @@ -406,6 +424,23 @@ namespace Barotrauma { LockUnusedDoors(selectedModules, entities, generationParams.RemoveUnusedGaps); } + if (generationParams.DrawBehindSubs) + { + foreach (var entity in allEntities) + { + if (entity is Structure structure) + { + //eww + structure.SpriteDepth = MathHelper.Lerp(0.999f, 0.9999f, structure.SpriteDepth); +#if CLIENT + foreach (var light in structure.Lights) + { + light.IsBackground = true; + } +#endif + } + } + } AlignLadders(selectedModules, entities); PowerUpOutpost(entities.SelectMany(e => e.Value)); if (generationParams.MaxWaterPercentage > 0.0f) @@ -436,7 +471,7 @@ namespace Barotrauma /// /// Select the number and types of the modules to use in the outpost /// - private static List SelectModules(IEnumerable modules, OutpostGenerationParams generationParams) + private static List SelectModules(IEnumerable modules, Location location, OutpostGenerationParams generationParams) { int totalModuleCount = generationParams.TotalModuleCount; var pendingModuleFlags = new List(); @@ -447,23 +482,29 @@ namespace Barotrauma while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound) { availableModulesFound = false; - foreach (var moduleFlag in generationParams.ModuleCounts) + foreach (var moduleCount in generationParams.ModuleCounts) { - if (pendingModuleFlags.Count(m => m == moduleFlag.Identifier) >= generationParams.GetModuleCount(moduleFlag.Identifier)) + if (!moduleCount.RequiredFaction.IsEmpty && + location.Faction?.Prefab.Identifier != moduleCount.RequiredFaction && + location.SecondaryFaction?.Prefab.Identifier != moduleCount.RequiredFaction) { continue; } - if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag.Identifier))) + if (pendingModuleFlags.Count(m => m == moduleCount.Identifier) >= generationParams.GetModuleCount(moduleCount.Identifier)) { - DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleFlag.Identifier}\" found)."); + continue; + } + if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleCount.Identifier))) + { + DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleCount.Identifier}\" found)."); continue; } availableModulesFound = true; - pendingModuleFlags.Add(moduleFlag.Identifier); + pendingModuleFlags.Add(moduleCount.Identifier); } } - pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f)).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient)); - while (pendingModuleFlags.Count < totalModuleCount) + pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f).Order).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient)); + while (pendingModuleFlags.Count < totalModuleCount && generationParams.AppendToReachTotalModuleCount) { //don't place "none" modules at the end because // a. "filler rooms" at the end of a hallway are pointless @@ -514,12 +555,8 @@ namespace Barotrauma foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - if (!allowExtendBelowInitialModule) - { - //don't continue downwards if it'd extend below the airlock - if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } - } - + if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; } + PlacedModule newModule = null; //try appending to the current module if possible if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) @@ -540,6 +577,7 @@ namespace Barotrauma foreach (OutpostModuleInfo.GapPosition otherGapPosition in GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) { + if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; } newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); if (newModule != null) { @@ -588,6 +626,16 @@ namespace Barotrauma { System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule))); } + + static bool DisallowBelowAirlock(bool allowExtendBelowInitialModule, OutpostModuleInfo.GapPosition gapPosition, PlacedModule currentModule) + { + if (!allowExtendBelowInitialModule) + { + //don't continue downwards if it'd extend below the airlock + if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { return true; } + } + return false; + } } /// @@ -1057,8 +1105,8 @@ namespace Barotrauma DebugConsole.AddWarning($"Failed to connect junction boxes between outpost modules (not enough free connections in module \"{module.PreviousModule.Info.Name}\")"); continue; } - wire.Connect(thisJunctionBox.Connections[i], addNode: false); - wire.Connect(previousJunctionBox.Connections[i], addNode: false); + wire.TryConnect(thisJunctionBox.Connections[i], addNode: false); + wire.TryConnect(previousJunctionBox.Connections[i], addNode: false); wire.SetNodes(new List()); } } @@ -1390,6 +1438,31 @@ namespace Barotrauma } } + private static void EnableFactionSpecificEntities(Submarine sub, Location location) + { + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (string.IsNullOrEmpty(me.Layer) || me.Submarine != sub) { continue; } + + var layerAsIdentifier = me.Layer.ToIdentifier(); + if (FactionPrefab.Prefabs.ContainsKey(layerAsIdentifier)) + { + me.HiddenInGame = + location?.Faction?.Prefab != FactionPrefab.Prefabs[layerAsIdentifier]; +#if CLIENT + //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that + if (me.HiddenInGame && me is Item item) + { + foreach (var lightComponent in item.GetComponents()) + { + lightComponent.Light.Enabled = false; + } + } +#endif + } + } + } + private static void LockUnusedDoors(IEnumerable placedModules, Dictionary> entities, bool removeUnusedGaps) { foreach (PlacedModule module in placedModules) @@ -1592,7 +1665,12 @@ namespace Barotrauma List killedCharacters = new List(); List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)> selectedCharacters = new List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)>(); - var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.ServerAndClient); + + List factions = new List(); + if (location?.Faction != null) { factions.Add(location.Faction.Prefab); } + if (location?.SecondaryFaction != null) { factions.Add(location.SecondaryFaction.Prefab); } + + var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(factions, Rand.RandSync.ServerAndClient); foreach (HumanPrefab humanPrefab in humanPrefabs) { if (humanPrefab is null) { continue; } @@ -1611,7 +1689,7 @@ namespace Barotrauma for (int tries = 0; tries < 100; tries++) { var characterInfo = killedCharacter.CreateCharacterInfo(Rand.RandSync.ServerAndClient); - if (!location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) + if (location != null && !location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { selectedCharacters.Add((killedCharacter, characterInfo)); break; @@ -1633,22 +1711,21 @@ namespace Barotrauma npc.AnimController.FindHull(gotoTarget.WorldPosition, setSubmarine: true); npc.TeamID = CharacterTeamType.FriendlyNPC; npc.HumanPrefab = humanPrefab; - if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier)) + outpost.Info.AddOutpostNPCIdentifierOrTag(npc, humanPrefab.Identifier); + foreach (Identifier tag in humanPrefab.GetTags()) { - outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List()); + outpost.Info.AddOutpostNPCIdentifierOrTag(npc, tag); } - outpost.Info.OutpostNPCs[humanPrefab.Identifier].Add(npc); if (GameMain.NetworkMember?.ServerSettings != null && !GameMain.NetworkMember.ServerSettings.KillableNPCs) { npc.CharacterHealth.Unkillable = true; } - humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient); + humanPrefab.GiveItems(npc, outpost, gotoTarget as WayPoint, Rand.RandSync.ServerAndClient); foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing; item.SpawnedInCurrentOutpost = true; } - npc.GiveIdCardTags(gotoTarget as WayPoint); humanPrefab.InitializeCharacter(npc, gotoTarget); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index 794a9671b..9a9405020 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -93,13 +93,11 @@ namespace Barotrauma { moduleFlags.Add("hallwayhorizontal".ToIdentifier()); if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); } - return; } if (newFlags.Contains("hallwayvertical".ToIdentifier())) { moduleFlags.Add("hallwayvertical".ToIdentifier()); if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); } - return; } if (!newFlags.Any()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 532ce3957..696eda83f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -34,6 +34,13 @@ namespace Barotrauma /// public const int DefaultAmount = 5; + private readonly Dictionary minReputation = new Dictionary(); + + /// + /// Minimum reputation needed to buy the item (Key = faction ID, Value = min rep) + /// + public IReadOnlyDictionary MinReputation => minReputation; + /// /// Support for the old style of determining item prices /// when there were individual Price elements for each location type @@ -70,6 +77,19 @@ namespace Barotrauma RequiresUnlock = requiresUnlock; } + private void LoadReputationRestrictions(XElement priceInfoElement) + { + foreach (XElement childElement in priceInfoElement.GetChildElements("reputation")) + { + Identifier factionId = childElement.GetAttributeIdentifier("faction", Identifier.Empty); + float rep = childElement.GetAttributeFloat("min", 0.0f); + if (!factionId.IsEmpty && rep > 0) + { + minReputation.Add(factionId, rep); + } + } + } + public static List CreatePriceInfos(XElement element, out PriceInfo defaultPrice) { var priceInfos = new List(); @@ -106,6 +126,7 @@ namespace Barotrauma displayNonEmpty: displayNonEmpty, requiresUnlock: requiresUnlock, storeIdentifier: storeIdentifier); + priceInfo.LoadReputationRestrictions(childElement); priceInfos.Add(priceInfo); } bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false)); @@ -117,7 +138,8 @@ namespace Barotrauma minLevelDifficulty: minLevelDifficulty, buyingPriceMultiplier: buyingPriceMultiplier, displayNonEmpty: displayNonEmpty, - requiresUnlock: requiresUnlock); + requiresUnlock: requiresUnlock); + defaultPrice.LoadReputationRestrictions(element); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 8cd78e3f1..d27dd4471 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -81,6 +81,8 @@ namespace Barotrauma get { return base.Prefab.Sprite; } } + public bool IsExteriorWall { get; private set; } = true; + public bool IsPlatform { get { return Prefab.Platform; } @@ -402,6 +404,8 @@ namespace Barotrauma } } + CheckIsExteriorWall(); + #if CLIENT convexHulls?.ForEach(x => x.Move(amount)); @@ -697,15 +701,41 @@ namespace Barotrauma } } - private static Vector2[] CalculateExtremes(Rectangle sectionRect) + public void CheckIsExteriorWall() { - Vector2[] corners = new Vector2[4]; - corners[0] = new Vector2(sectionRect.X, sectionRect.Y - sectionRect.Height); - corners[1] = new Vector2(sectionRect.X, sectionRect.Y); - corners[2] = new Vector2(sectionRect.Right, sectionRect.Y); - corners[3] = new Vector2(sectionRect.Right, sectionRect.Y - sectionRect.Height); + if (!HasBody) + { + IsExteriorWall = false; + return; + } - return corners; + Vector2 point1 = WorldPosition + BodyOffset * Scale; + //point1 = MathUtils.RotatePointAroundTarget(WorldPosition, point1, BodyRotation); + Vector2 point2 = point1; + + Vector2 normal = new Vector2( + (float)-Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), + (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)); + + float thickness = IsHorizontal ? + (BodyHeight > 0 ? BodyHeight : rect.Height) : + (BodyWidth > 0 ? BodyWidth : rect.Width); + + point1 += normal * (thickness / 2 + 16); + point2 -= normal * (thickness / 2 + 16); + + IsExteriorWall = + Hull.FindHullUnoptimized(point1, null, useWorldCoordinates: true) == null || + Hull.FindHullUnoptimized(point2, null, useWorldCoordinates: true) == null; +#if CLIENT + if (convexHulls != null) + { + foreach (ConvexHull ch in convexHulls) + { + ch.IsExteriorWall = IsExteriorWall; + } + } +#endif } /// @@ -715,7 +745,7 @@ namespace Barotrauma { foreach (MapEntity mapEntity in mapEntityList) { - if (!(mapEntity is Structure structure)) { continue; } + if (mapEntity is not Structure structure) { continue; } if (!structure.Prefab.AllowAttachItems) { continue; } if (structure.Bodies != null && structure.Bodies.Count > 0) { continue; } Rectangle worldRect = mapEntity.WorldRect; @@ -939,7 +969,7 @@ namespace Barotrauma Rand.Range(worldRect.X, worldRect.Right + 1), Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y + 1)); - var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); + var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle, particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); if (particle == null) break; } } @@ -1085,9 +1115,9 @@ namespace Barotrauma return new AttackResult(damageAmount, null); } - public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool createExplosionEffect = true) + public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool isNetworkEvent = true, bool createExplosionEffect = true) { - if (Submarine != null && Submarine.GodMode || Indestructible) { return; } + if (Submarine != null && Submarine.GodMode || (Indestructible && !isNetworkEvent)) { return; } if (!Prefab.Body) { return; } if (!MathUtils.IsValid(damage)) { return; } @@ -1635,6 +1665,7 @@ namespace Barotrauma { SetDamage(i, Sections[i].damage, createNetworkEvent: false, createExplosionEffect: false); } + CheckIsExteriorWall(); } public virtual void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 18e48a4c2..941ddab07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Xml.Linq; using Barotrauma.IO; using System.Collections.Immutable; +using System.ComponentModel; #if CLIENT using Microsoft.Xna.Framework.Graphics; #endif @@ -44,30 +45,26 @@ namespace Barotrauma public override ImmutableHashSet Aliases { get; } - //does the structure have a physics body - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Does the structure have a physics body?")] public bool Body { get; private set; } - //rotation of the physics body in degrees - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Rotation of the physics body in degrees.")] public float BodyRotation { get; private set; } - //in display units - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Width of the physics body in pixels.")] public float BodyWidth { get; private set; } - //in display units - [Serialize(0.0f, IsPropertySaveable.No)] + [Serialize(0.0f, IsPropertySaveable.No, description: "Height of the physics body in pixels.")] public float BodyHeight { get; private set; } //in display units - [Serialize("0.0,0.0", IsPropertySaveable.No)] + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "Offset of the physics body from the center of the structure in pixels.")] public Vector2 BodyOffset { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Is the structure a platform (i.e. a \"floor\" the players can pass through)? Only relevant if the structure has a physics body.")] public bool Platform { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Can items like signal components be attached on this structure? Should be enabled on structures like decorative background walls.")] public bool AllowAttachItems { get; private set; } [Serialize(0.0f, IsPropertySaveable.No)] @@ -81,27 +78,30 @@ namespace Barotrauma private set { health = Math.Max(value, MinHealth); } } - [Serialize(true, IsPropertySaveable.No)] + [Serialize(true, IsPropertySaveable.No, description: "Should the structure be indestructible when used in an outpost?")] public bool IndestructibleInOutposts { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "Should the structure cast shadows and obstruct visibility when LOS is enabled?")] public bool CastShadow { get; private set; } - [Serialize(Direction.None, IsPropertySaveable.No)] + [Serialize(Direction.None, IsPropertySaveable.No, description: "Makes the structure function as a staircase.")] public Direction StairDirection { get; private set; } - [Serialize(45.0f, IsPropertySaveable.No)] + [Serialize(45.0f, IsPropertySaveable.No, description: "Angle of the stairs in degrees. Only relevant if StairDirection is something else than None.")] public float StairAngle { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: "If enabled, monsters will not be able to target this structure.")] public bool NoAITarget { get; private set; } - [Serialize("0,0", IsPropertySaveable.Yes)] + [Serialize("0,0", IsPropertySaveable.Yes, description: "Size of the structure in pixels. If not set, the size is determined, based on the attributes width and height, and if those aren't defined either, based on the size of the structure's sprite.")] public Vector2 Size { get; private set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the sound that plays when something damages the wall.")] public string DamageSound { get; private set; } + [Serialize("shrapnel", IsPropertySaveable.Yes, description: "Identifier of the particles emitted when something damages the wall.")] + public string DamageParticle { get; private set; } + protected Vector2 textureScale = Vector2.One; [Editable(DecimalCount = 3), Serialize("1.0, 1.0", IsPropertySaveable.Yes)] public Vector2 TextureScale @@ -175,7 +175,7 @@ namespace Barotrauma #endif foreach (var subElement in element.Elements()) { - switch (subElement.Name.ToString()) + switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": Sprite = new Sprite(subElement, lazyLoad: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index d5f4e7387..178edc186 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -213,9 +213,21 @@ namespace Barotrauma get { if (Level.Loaded == null) { return false; } - if (Level.Loaded.EndOutpost != null && DockedTo.Contains(Level.Loaded.EndOutpost)) + if (Level.Loaded.EndOutpost != null) { - return true; + if (DockedTo.Contains(Level.Loaded.EndOutpost)) + { + return true; + } + else if (Level.Loaded.EndOutpost.exitPoints.Any()) + { + return IsAtOutpostExit(Level.Loaded.EndOutpost); + } + } + else if (Level.Loaded.Type == LevelData.LevelType.Outpost && Level.Loaded.StartOutpost != null) + { + //in outpost levels, the outpost is always the start outpost: check it if has an exit + return IsAtOutpostExit(Level.Loaded.StartOutpost); } return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.EndExitPosition) < Level.ExitDistance * Level.ExitDistance); } @@ -226,14 +238,44 @@ namespace Barotrauma get { if (Level.Loaded == null) { return false; } - if (Level.Loaded.StartOutpost != null && DockedTo.Contains(Level.Loaded.StartOutpost)) + if (Level.Loaded.StartOutpost != null) { - return true; + if (DockedTo.Contains(Level.Loaded.StartOutpost)) + { + return true; + } + else if (Level.Loaded.StartOutpost.exitPoints.Any()) + { + return IsAtOutpostExit(Level.Loaded.StartOutpost); + } } return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.StartExitPosition) < Level.ExitDistance * Level.ExitDistance); } } + public bool AtEitherExit => AtStartExit || AtEndExit; + + private bool IsAtOutpostExit(Submarine outpost) + { + if (outpost.exitPoints.Any()) + { + Rectangle worldBorders = Borders; + worldBorders.Location += WorldPosition.ToPoint(); + foreach (var exitPoint in outpost.exitPoints) + { + if (exitPoint.ExitPointSize != Point.Zero) + { + if (RectsOverlap(worldBorders, exitPoint.ExitPointWorldRect)) { return true; } + } + else + { + if (RectContains(worldBorders, exitPoint.WorldPosition)) { return true; } + } + } + } + return false; + } + public new Vector2 DrawPosition { @@ -284,6 +326,9 @@ namespace Barotrauma } } + private readonly List exitPoints = new List(); + public IReadOnlyList ExitPoints { get { return exitPoints; } } + public override string ToString() { return "Barotrauma.Submarine (" + (Info?.Name ?? "[NULL INFO]") + ", " + IdOffset + ")"; @@ -350,12 +395,23 @@ namespace Barotrauma } public WreckAI WreckAI { get; private set; } + public SubmarineTurretAI TurretAI { get; private set; } + public bool CreateWreckAI() { WreckAI = WreckAI.Create(this); return WreckAI != null; } + /// + /// Creates an AI that operates all the turrets on a sub, same as Thalamus but only operates the turrets. + /// + public bool CreateTurretAI() + { + TurretAI = new SubmarineTurretAI(this); + return TurretAI != null; + } + public void DisableWreckAI() { if (WreckAI == null) @@ -991,6 +1047,7 @@ namespace Barotrauma { WreckAI?.Update(deltaTime); } + TurretAI?.Update(deltaTime); if (subBody?.Body == null) { return; } @@ -1038,7 +1095,7 @@ namespace Barotrauma public void ApplyForce(Vector2 force) { - if (subBody != null) subBody.ApplyForce(force); + if (subBody != null) { subBody.ApplyForce(force); } } public void EnableMaintainPosition() @@ -1271,22 +1328,38 @@ namespace Barotrauma } /// - /// Finds the sub whose borders contain the position + /// Finds the sub whose borders contain the position. Note that this method uses the "actual" position of the sub outside the level: + /// only use this if the position is in a submarine's local coordinate space! /// - public static Submarine FindContaining(Vector2 position) + public static Submarine FindContainingInLocalCoordinates(Vector2 position, float inflate = 500.0f) { foreach (Submarine sub in Loaded) { Rectangle subBorders = sub.Borders; - subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Microsoft.Xna.Framework.Point(0, sub.Borders.Height); - - subBorders.Inflate(500.0f, 500.0f); - - if (subBorders.Contains(position)) return sub; + subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Point(0, sub.Borders.Height); + subBorders.Inflate(inflate, inflate); + if (subBorders.Contains(position)) { return sub; } } return null; } + + /// + /// Finds the sub whose world borders contain the position. + /// + public static Submarine FindContaining(Vector2 worldPosition, float inflate = 500.0f) + { + foreach (Submarine sub in Loaded) + { + Rectangle worldBorders = sub.Borders; + worldBorders.Location += sub.WorldPosition.ToPoint(); + worldBorders.Inflate(inflate, inflate); + if (RectContains(worldBorders, worldPosition)) { return sub; } + } + return null; + } + + public static Rectangle GetBorders(XElement submarineElement) { Vector4 bounds = Vector4.Zero; @@ -1325,14 +1398,19 @@ namespace Barotrauma //place the sub above the top of the level HiddenSubPosition = HiddenSubStartPosition; - if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null) + if (GameMain.GameSession?.LevelData != null) { HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y; } - foreach (Submarine sub in loaded) + for (int i = 0; i < loaded.Count; i++) { - HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f); + Submarine sub = loaded[i]; + HiddenSubPosition = + new Vector2( + //1st sub on the left side, 2nd on the right, etc + HiddenSubPosition.X * (i % 2 == 0 ? 1 : -1), + HiddenSubPosition.Y + sub.Borders.Height + 5000.0f); } IdOffset = IdRemap.DetermineNewOffset(); @@ -1460,10 +1538,15 @@ namespace Barotrauma MapEntity.MapLoaded(newEntities, true); foreach (MapEntity me in MapEntity.mapEntityList) { - if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) + if (me.Submarine != this) { continue; } + if (me is LinkedSubmarine linkedSub) { linkedSub.LinkDummyToMainSubmarine(); } + else if (me is WayPoint wayPoint && wayPoint.SpawnType.HasFlag(SpawnType.ExitPoint)) + { + exitPoints.Add(wayPoint); + } } foreach (Hull hull in matchingHulls) @@ -1478,6 +1561,7 @@ namespace Barotrauma #if CLIENT GameMain.LightManager.OnMapLoaded(); + Lights.ConvexHull.RecalculateAll(this); #endif //if the sub was made using an older version, //halve the brightness of the lights to make them look (almost) right on the new lighting formula @@ -1683,57 +1767,66 @@ namespace Barotrauma public static void Unload() { + if (Unloading) + { + DebugConsole.AddWarning($"Called {nameof(Submarine.Unload)} when already unloading."); + return; + } + Unloading = true; + try + { #if CLIENT - RoundSound.RemoveAllRoundSounds(); - GameMain.LightManager?.ClearLights(); + RoundSound.RemoveAllRoundSounds(); + GameMain.LightManager?.ClearLights(); #endif - var _loaded = new List(loaded); - foreach (Submarine sub in _loaded) - { - sub.Remove(); - } - - loaded.Clear(); - - visibleEntities = null; - - if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; } - - RemoveAll(); - - if (Item.ItemList.Count > 0) - { - List items = new List(Item.ItemList); - foreach (Item item in items) + var _loaded = new List(loaded); + foreach (Submarine sub in _loaded) { - DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed"); - try - { - item.Remove(); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e); - } + sub.Remove(); } - Item.ItemList.Clear(); + + loaded.Clear(); + + visibleEntities = null; + + if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; } + + RemoveAll(); + + if (Item.ItemList.Count > 0) + { + List items = new List(Item.ItemList); + foreach (Item item in items) + { + DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed"); + try + { + item.Remove(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e); + } + } + Item.ItemList.Clear(); + } + + Ragdoll.RemoveAll(); + PhysicsBody.RemoveAll(); + GameMain.World = null; + + Powered.Grids.Clear(); + + GC.Collect(); + + } + finally + { + Unloading = false; } - - Ragdoll.RemoveAll(); - - PhysicsBody.RemoveAll(); - - GameMain.World?.Clear(); - GameMain.World = null; - - Powered.Grids.Clear(); - - GC.Collect(); - - Unloading = false; } public override void Remove() @@ -1800,18 +1893,18 @@ namespace Barotrauma { if (node == null || node.Waypoint == null) { continue; } var wp = node.Waypoint; - if (wp.isObstructed) { continue; } + if (wp.IsObstructed) { continue; } foreach (var connection in node.connections) { var connectedWp = connection.Waypoint; - if (connectedWp.isObstructed) { continue; } + if (connectedWp.IsObstructed) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition); var body = PickBody(start, end, null, Physics.CollisionLevel, allowInsideFixture: false); if (body != null) { - connectedWp.isObstructed = true; - wp.isObstructed = true; + connectedWp.IsObstructed = true; + wp.IsObstructed = true; break; } } @@ -1830,11 +1923,11 @@ namespace Barotrauma { if (node == null || node.Waypoint == null) { continue; } var wp = node.Waypoint; - if (wp.isObstructed) { continue; } + if (wp.IsObstructed) { continue; } foreach (var connection in node.connections) { var connectedWp = connection.Waypoint; - if (connectedWp.isObstructed || connectedWp.Ladders != null) { continue; } + if (connectedWp.IsObstructed || connectedWp.Ladders != null) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); @@ -1842,8 +1935,8 @@ namespace Barotrauma { if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { - connectedWp.isObstructed = true; - wp.isObstructed = true; + connectedWp.IsObstructed = true; + wp.IsObstructed = true; if (!obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) { nodes = new HashSet(); @@ -1865,7 +1958,7 @@ namespace Barotrauma { if (obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) { - nodes.ForEach(n => n.Waypoint.isObstructed = false); + nodes.ForEach(n => n.Waypoint.IsObstructed = false); nodes.Clear(); obstructedNodes.Remove(otherSub); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 7867f1166..4bf6ca6e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -18,6 +18,13 @@ namespace Barotrauma { public const float NeutralBallastPercentage = 0.07f; + public const Category CollidesWith = + Physics.CollisionItem | + Physics.CollisionLevel | + Physics.CollisionCharacter | + Physics.CollisionProjectile | + Physics.CollisionWall; + const float HorizontalDrag = 0.01f; const float VerticalDrag = 0.05f; const float MaxDrag = 0.1f; @@ -32,6 +39,8 @@ namespace Barotrauma private const float MaxCollisionImpact = 5.0f; private const float Friction = 0.2f, Restitution = 0.0f; + private readonly List levelContacts = new List(); + public List HullVertices { get; @@ -39,6 +48,7 @@ namespace Barotrauma } private float depthDamageTimer = 10.0f; + private float damageSoundTimer = 10.0f; private readonly Submarine submarine; @@ -48,6 +58,9 @@ namespace Barotrauma private readonly Queue impactQueue = new Queue(); + private float forceUpwardsTimer; + private const float ForceUpwardsDelay = 30.0f; + struct Impact { public Fixture Target; @@ -146,9 +159,13 @@ namespace Barotrauma farseerBody.CollidesWith = collidesWith; farseerBody.Enabled = false; farseerBody.UserData = this; + if (sub.Info.IsOutpost) + { + farseerBody.BodyType = BodyType.Static; + } foreach (var mapEntity in MapEntity.mapEntityList) { - if (mapEntity.Submarine != submarine || !(mapEntity is Structure wall)) { continue; } + if (mapEntity.Submarine != submarine || mapEntity is not Structure wall) { continue; } bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None; Rectangle rect = wall.Rect; @@ -185,13 +202,20 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Submarine != submarine) { continue; } - if (item.StaticBodyConfig == null || item.Submarine != submarine) { continue; } + + Vector2 simPos = ConvertUnits.ToSimUnits(item.Position); + if (item.GetComponent() is Door door) + { + door.OutsideSubmarineFixture = farseerBody.CreateRectangle(door.Body.Width, door.Body.Height, 5.0f, simPos, collisionCategory, collidesWith); + door.OutsideSubmarineFixture.UserData = item; + } + + if (item.StaticBodyConfig == null) { continue; } float radius = item.StaticBodyConfig.GetAttributeFloat("radius", 0.0f) * item.Scale; float width = item.StaticBodyConfig.GetAttributeFloat("width", 0.0f) * item.Scale; float height = item.StaticBodyConfig.GetAttributeFloat("height", 0.0f) * item.Scale; - Vector2 simPos = ConvertUnits.ToSimUnits(item.Position); float simRadius = ConvertUnits.ToSimUnits(radius); float simWidth = ConvertUnits.ToSimUnits(width); float simHeight = ConvertUnits.ToSimUnits(height); @@ -374,6 +398,33 @@ namespace Barotrauma //------------------------- + //if heading up and there's another sub on top of us, gradually force it upwards + //(i.e. apply "artificial buoyancy" to it) to prevent us from getting pinned under it + //only applies to enemy subs with no enemies inside them (like destroyed pirate subs) + if (totalForce.Y > 0) + { + ContactEdge contactEdge = Body?.FarseerBody?.ContactList; + while (contactEdge?.Next != null) + { + if (contactEdge.Contact.Enabled && + contactEdge.Other.UserData is Submarine otherSubmarine && + otherSubmarine.TeamID != Submarine.TeamID && + contactEdge.Contact.IsTouching) + { + contactEdge.Contact.GetWorldManifold(out Vector2 _, out FixedArray2 points); + if (points[0].Y > Body.SimPosition.Y && + !Character.CharacterList.Any(c => c.Submarine == otherSubmarine && !c.IsIncapacitated && c.TeamID == otherSubmarine.TeamID)) + { + otherSubmarine.SubBody.forceUpwardsTimer += deltaTime; + break; + } + } + contactEdge = contactEdge.Next; + } + } + + //------------------------- + if (Body.LinearVelocity.LengthSquared() > 0.0001f) { //TODO: sync current drag with clients? @@ -397,7 +448,34 @@ namespace Barotrauma ApplyForce(totalForce); + if (Velocity.LengthSquared() < 0.01f) + { + levelContacts.Clear(); + levelContacts.AddRange(GetLevelContacts(Body)); + for (int i = 0; i < levelContacts.Count; i++) + { + for (int j = i + 1; j < levelContacts.Count; j++) + { + levelContacts[i].GetWorldManifold(out Vector2 normal1, out _); + levelContacts[j].GetWorldManifold(out Vector2 normal2, out _); + + //normals pointing in different directions = sub lodged between two walls + if (Vector2.Dot(normal1, normal2) < 0) + { + //apply an extra force to hopefully dislodge the sub + ApplyForce(totalForce * 100.0f); + i = levelContacts.Count; + break; + } + } + } + } + + totalForcePerFrame = Vector2.Zero; + UpdateDepthDamage(deltaTime); + + forceUpwardsTimer = MathHelper.Clamp(forceUpwardsTimer - deltaTime * 0.1f, 0.0f, ForceUpwardsDelay); } partial void ClientUpdatePosition(float deltaTime); @@ -464,16 +542,28 @@ namespace Barotrauma float buoyancy = NeutralBallastPercentage - waterPercentage; if (buoyancy > 0.0f) + { buoyancy *= 2.0f; + } else + { buoyancy = Math.Max(buoyancy, -0.5f); + } + + if (forceUpwardsTimer > 0.0f) + { + buoyancy = MathHelper.Lerp(buoyancy, 0.1f, forceUpwardsTimer / ForceUpwardsDelay); + } return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f); } + + private Vector2 totalForcePerFrame; public void ApplyForce(Vector2 force) { Body.ApplyForce(force); + totalForcePerFrame += force; } public void SetPosition(Vector2 position) @@ -489,36 +579,58 @@ namespace Barotrauma if (Level.Loaded == null) { return; } //camera shake and sounds start playing 500 meters before crush depth - float depthEffectThreshold = 500.0f; - if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth - depthEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth - depthEffectThreshold) + const float CosmeticEffectThreshold = -500.0f; + //breaches won't get any more severe 500 meters below crush depth + const float MaxEffectThreshold = 500.0f; + const float MinWallDamageProbability = 0.1f; + const float MaxWallDamageProbability = 1.0f; + const float MinWallDamage = 50f; + const float MaxWallDamage = 500.0f; + const float MinCameraShake = 5f; + const float MaxCameraShake = 50.0f; + + if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth + CosmeticEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth + CosmeticEffectThreshold) { return; } - depthDamageTimer -= deltaTime; - if (depthDamageTimer > 0.0f) { return; } - -#if CLIENT - SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f); -#endif - - foreach (Structure wall in Structure.WallList) + damageSoundTimer -= deltaTime; + if (damageSoundTimer <= 0.0f) { - if (wall.Submarine != submarine) { continue; } - - float wallCrushDepth = wall.CrushDepth; - float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth; - if (pastCrushDepth > 0) - { - Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f); - } - if (Character.Controlled != null && Character.Controlled.Submarine == submarine) - { - GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Clamp(pastCrushDepth * 0.001f, 1.0f, 50.0f)); - } +#if CLIENT + SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f); +#endif + damageSoundTimer = Rand.Range(5.0f, 10.0f); } - depthDamageTimer = 10.0f; + depthDamageTimer -= deltaTime; + if (depthDamageTimer <= 0.0f) + { + foreach (Structure wall in Structure.WallList) + { + if (wall.Submarine != submarine) { continue; } + + float wallCrushDepth = wall.CrushDepth; + float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth; + float pastCrushDepthRatio = Math.Clamp(pastCrushDepth / MaxEffectThreshold, 0.0f, 1.0f); + + if (Rand.Range(0.0f, 1.0f) > MathHelper.Lerp(MinWallDamageProbability, MaxWallDamageProbability, pastCrushDepthRatio)) { continue; } + + float damage = MathHelper.Lerp(MinWallDamage, MaxWallDamage, pastCrushDepthRatio); + if (pastCrushDepth > 0) + { + Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, damage, levelWallDamage: 0.0f); +#if CLIENT + SoundPlayer.PlayDamageSound("StructureBlunt", Rand.Range(0.0f, 100.0f), wall.WorldPosition, 2000.0f); +#endif + } + if (Character.Controlled != null && Character.Controlled.Submarine == submarine) + { + GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Lerp(MinCameraShake, MaxCameraShake, pastCrushDepthRatio)); + } + } + depthDamageTimer = Rand.Range(5.0f, 10.0f); + } } public void FlipX() @@ -623,7 +735,7 @@ namespace Barotrauma attackMultiplier = enemyAI.ActiveAttack.SubmarineImpactMultiplier; } - if (impactMass * attackMultiplier > MinImpactLimbMass) + if (impactMass * attackMultiplier > MinImpactLimbMass && Body.BodyType != BodyType.Static) { Vector2 normal = Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ? @@ -641,20 +753,10 @@ namespace Barotrauma } //find all contacts between the limb and level walls - List levelContacts = new List(); - ContactEdge contactEdge = limb.body.FarseerBody.ContactList; - while (contactEdge?.Contact != null) - { - if (contactEdge.Contact.Enabled && - contactEdge.Contact.IsTouching && - contactEdge.Other?.UserData is VoronoiCell) - { - levelContacts.Add(contactEdge.Contact); - } - contactEdge = contactEdge.Next; - } + IEnumerable levelContacts = GetLevelContacts(limb.body); + int levelContactCount = levelContacts.Count(); - if (levelContacts.Count == 0) { return; } + if (levelContactCount == 0) { return; } //if the limb is in contact with the level, apply an artifical impact to prevent the sub from bouncing on top of it //not a very realistic way to handle the collisions (makes it seem as if the characters were made of reinforced concrete), @@ -677,9 +779,9 @@ namespace Barotrauma avgContactNormal += contactNormal; //apply impacts at the positions where this sub is touching the limb - ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContacts.Count, contactNormal, collision.ImpactPos, applyDamage: false); + ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContactCount, contactNormal, collision.ImpactPos, applyDamage: false); } - avgContactNormal /= levelContacts.Count; + avgContactNormal /= levelContactCount; float contactDot = Vector2.Dot(Body.LinearVelocity, -avgContactNormal); if (contactDot > 0.001f) @@ -718,6 +820,21 @@ namespace Barotrauma } } + private IEnumerable GetLevelContacts(PhysicsBody body) + { + ContactEdge contactEdge = body.FarseerBody.ContactList; + while (contactEdge?.Contact != null) + { + if (contactEdge.Contact.Enabled && + contactEdge.Contact.IsTouching && + contactEdge.Other?.UserData is VoronoiCell) + { + yield return contactEdge.Contact; + } + contactEdge = contactEdge.Next; + } + } + private void HandleLevelCollision(Impact impact, VoronoiCell cell = null) { if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration < 10) @@ -786,21 +903,9 @@ namespace Barotrauma } //find all contacts between this sub and level walls - List levelContacts = new List(); - ContactEdge contactEdge = Body?.FarseerBody?.ContactList; - while (contactEdge?.Next != null) - { - if (contactEdge.Contact.Enabled && - contactEdge.Other.UserData is VoronoiCell && - contactEdge.Contact.IsTouching) - { - levelContacts.Add(contactEdge.Contact); - } - - contactEdge = contactEdge.Next; - } - - if (levelContacts.Count == 0) { return; } + IEnumerable levelContacts = GetLevelContacts(Body); + int levelContactCount = levelContacts.Count(); + if (levelContactCount == 0) { return; } //if this sub is in contact with the level, apply artifical impacts //to both subs to prevent the other sub from bouncing on top of this one @@ -811,8 +916,7 @@ namespace Barotrauma levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2 temp); //if the contact normal is pointing from the sub towards the level cell we collided with, flip the normal - VoronoiCell cell = levelContact.FixtureB.UserData is VoronoiCell ? - ((VoronoiCell)levelContact.FixtureB.UserData) : ((VoronoiCell)levelContact.FixtureA.UserData); + VoronoiCell cell = levelContact.FixtureB.UserData as VoronoiCell ?? levelContact.FixtureA.UserData as VoronoiCell; var cellDiff = ConvertUnits.ToDisplayUnits(Body.SimPosition) - cell.Center; if (Vector2.Dot(contactNormal, cellDiff) < 0) @@ -823,9 +927,9 @@ namespace Barotrauma avgContactNormal += contactNormal; //apply impacts at the positions where this sub is touching the level - ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContacts.Count, contactNormal, impact.ImpactPos); + ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContactCount, contactNormal, impact.ImpactPos); } - avgContactNormal /= levelContacts.Count; + avgContactNormal /= levelContactCount; //apply an impact to the other sub float contactDot = Vector2.Dot(otherSub.PhysicsBody.LinearVelocity, -avgContactNormal); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index a5c3ffac7..a83578cfa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; #if DEBUG using System.IO; @@ -478,7 +479,6 @@ namespace Barotrauma hashTask = new Task(() => { hash = Md5Hash.CalculateForString(doc.ToString(), Md5Hash.StringHashOptions.IgnoreWhitespace); - Md5Hash.Cache.Add(FilePath, hash, DateTime.UtcNow); }); hashTask.Start(); } @@ -559,6 +559,14 @@ namespace Barotrauma } return structureCrushDepthsDefined; } + public void AddOutpostNPCIdentifierOrTag(Character npc, Identifier idOrTag) + { + if (!OutpostNPCs.ContainsKey(idOrTag)) + { + OutpostNPCs.Add(idOrTag, new List()); + } + OutpostNPCs[idOrTag].Add(npc); + } //saving/loading ---------------------------------------------------- public void SaveAs(string filePath, System.IO.MemoryStream previewImage = null) @@ -590,7 +598,6 @@ namespace Barotrauma } SaveUtil.CompressStringToFile(filePath, doc.ToString()); - Md5Hash.Cache.Remove(filePath); } public static void AddToSavedSubs(SubmarineInfo subInfo) @@ -752,6 +759,36 @@ namespace Barotrauma return doc; } + public int GetPrice(Location location = null, ImmutableHashSet characterList = null) + { + if (location is null) + { + if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation is { } currentLocation) + { + location = currentLocation; + } + else + { + + return Price; + } + } + + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); + + float price = Price; + if (characterList.Any()) + { + if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) + { + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated)); + } + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier)); + } + + return (int)price; + } + public static int GetDefaultTier(int price) => price > 20000 ? HighestTier : price > 10000 ? 2 : 1; public const int HighestTier = 3; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 1a99f675a..2bb863cef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -11,7 +11,7 @@ using Barotrauma.Extensions; namespace Barotrauma { [Flags] - public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8 }; + public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8, Submarine = 16, ExitPoint = 32 }; partial class WayPoint : MapEntity { @@ -29,7 +29,32 @@ namespace Barotrauma private HashSet tags; - public bool isObstructed; + public bool IsObstructed; + + public bool IsInWater => CurrentHull == null || CurrentHull.Surface > Position.Y; + + // Waypoints linked to doors are traversable, unless they are obstructed, because we filter them out in the setter of Gap.Open. + // The only way to add the open gaps should be by calling OnGapStateSchanged. + public bool IsTraversable => !IsObstructed && (openGaps == null || openGaps.Count == 0 || IsInWater); + + private HashSet openGaps; + /// + /// Only called by a Gap when the state changes. + /// So in practice used like an event callback, although technically just a method + /// (It would be cleaner to use an actual event in Gap.cs, but event registering and unregistering might cause an extra hassle) + /// + public void OnGapStateChanged(bool open, Gap gap) + { + openGaps ??= new HashSet(); + if (open) + { + openGaps.Add(gap); + } + else + { + openGaps.Remove(gap); + } + } private ushort gapId; public Gap ConnectedGap @@ -54,6 +79,12 @@ namespace Barotrauma set { spawnType = value; } } + public Point ExitPointSize { get; private set; } + + public Rectangle ExitPointWorldRect => new Rectangle( + (int)WorldPosition.X - ExitPointSize.X / 2, (int)WorldPosition.Y + ExitPointSize.Y / 2, + ExitPointSize.X, ExitPointSize.Y); + public Action OnLinksChanged { get; set; } public override string Name @@ -140,7 +171,9 @@ namespace Barotrauma { "Cargo", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) }, { "Corpse", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512,0,128,128)) }, { "Ladder", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,128,128,128)) }, - { "Door", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,128,128,128)) } + { "Door", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,128,128,128)) }, + { "Submarine", new Sprite("Content/UI/CommandUIBackground.png", new Rectangle(0,896,128,128)) }, + { "ExitPoint", new Sprite("Content/UI/CommandUIBackground.png", new Rectangle(0,896,128,128)) } }; } #endif @@ -981,6 +1014,12 @@ namespace Barotrauma public override void OnMapLoaded() { + if (Submarine == null) + { + // Don't try to connect waypoints that are not linked to any submarines to hulls, stairs, gaps etc. + // Used to cause weird pathfinding errors on some outpost modules, because the waypoints of the main path or side path got linked to a hull in the outpost. + return; + } InitializeLinks(); FindHull(); FindStairs(); @@ -1018,7 +1057,6 @@ namespace Barotrauma int.Parse(element.GetAttribute("y").Value), (int)Submarine.GridSize.X, (int)Submarine.GridSize.Y); - Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType); WayPoint w = new WayPoint(spawnType == SpawnType.Path ? Type.WayPoint : Type.SpawnPoint, rect, submarine, idRemap.GetOffsetId(element)) { @@ -1036,6 +1074,8 @@ namespace Barotrauma w.IdCardTags = idCardTagString.Split(','); } + w.ExitPointSize = element.GetAttributePoint("exitpointsize", Point.Zero); + w.tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); Identifier jobIdentifier = element.GetAttributeIdentifier("job", Identifier.Empty); @@ -1076,6 +1116,10 @@ namespace Barotrauma new XAttribute("x", (int)(rect.X - Submarine.HiddenSubPosition.X)), new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)), new XAttribute("spawn", spawnType)); + if (SpawnType == SpawnType.ExitPoint) + { + element.Add(new XAttribute("exitpointsize", XMLExtensions.PointToString(ExitPointSize))); + } if (!string.IsNullOrWhiteSpace(IdCardDesc)) element.Add(new XAttribute("idcarddesc", IdCardDesc)); if (idCardTags.Length > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index d453464f8..e88558001 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -87,6 +87,7 @@ namespace Barotrauma.Networking if (character != null) { HasSpawned = true; + UsingFreeCam = false; #if CLIENT GameMain.GameSession?.CrewManager?.SetPlayerVoiceIconState(this, muted, mutedLocally); @@ -100,6 +101,11 @@ namespace Barotrauma.Networking } } + /// + /// Is the client using the 'freecam' console command? + /// + public bool UsingFreeCam; + public UInt16 CharacterID; private Vector2 spectatePos; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index 8bde51456..b17701a49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -28,28 +28,30 @@ namespace Barotrauma.Networking ManageMap = 0x8000, ManageHires = 0x10000, ManageBotTalents = 0x20000, - All = 0x3FFFF + SpamImmunity = 0x40000, + All = 0x7FFFF } class PermissionPreset { public static readonly List List = new List(); - - public readonly LocalizedString Name; + + public readonly Identifier Identifier; + public readonly LocalizedString DisplayName; public readonly LocalizedString Description; public readonly ClientPermissions Permissions; public readonly HashSet PermittedCommands; public PermissionPreset(XElement element) { - string name = element.GetAttributeString("name", ""); - Name = TextManager.Get("permissionpresetname." + name).Fallback(name); - Description = TextManager.Get("permissionpresetdescription." + name) .Fallback(element.GetAttributeString("description", "")); + Identifier = element.GetAttributeIdentifier("name", Identifier.Empty); + DisplayName = TextManager.Get("permissionpresetname." + Identifier).Fallback(Identifier.ToString()); + Description = TextManager.Get("permissionpresetdescription." + Identifier) .Fallback(element.GetAttributeString("description", "")); string permissionsStr = element.GetAttributeString("permissions", ""); if (!Enum.TryParse(permissionsStr, out Permissions)) { - DebugConsole.ThrowError("Error in permission preset \"" + Name + "\" - " + permissionsStr + " is not a valid permission!"); + DebugConsole.ThrowError("Error in permission preset \"" + DisplayName + "\" - " + permissionsStr + " is not a valid permission!"); } PermittedCommands = new HashSet(); @@ -64,7 +66,7 @@ namespace Barotrauma.Networking if (command == null) { #if SERVER - DebugConsole.ThrowError("Error in permission preset \"" + Name + "\" - " + commandName + "\" is not a valid console command."); + DebugConsole.ThrowError("Error in permission preset \"" + DisplayName + "\" - " + commandName + "\" is not a valid console command."); #endif continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index 29855637a..cba0112e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -348,12 +348,7 @@ namespace Barotrauma } private static T? ReadNullable(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) where T : struct => - ReadOption(inc, attribute, bitField) switch - { - Some { Value: var value } => value, - None _ => null, - _ => throw new ArgumentOutOfRangeException() - }; + ReadOption(inc, attribute, bitField).TryUnwrap(out var value) ? value : null; private static void WriteNullable(T? value, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) where T : struct => WriteOption(value.HasValue ? Option.Some(value.Value) : Option.None(), attribute, msg, bitField); @@ -378,7 +373,7 @@ namespace Barotrauma { ToolBox.ThrowIfNull(option); - if (option.TryUnwrap(out T value)) + if (option.TryUnwrap(out T? value)) { bitField.WriteBoolean(true); if (TryFindBehavior(out ReadWriteBehavior behavior)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index d4b897c9c..0f33d9599 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Networking /// public OrderChatMessage(Order order, Character targetCharacter, Character sender, bool isNewOrder = true) : this(order, - order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), + order?.GetChatMessage(targetCharacter?.Name, (order.TargetEntity as Hull ?? sender?.CurrentHull)?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), targetCharacter, sender, isNewOrder) { @@ -110,7 +110,7 @@ namespace Barotrauma.Networking WriteOrder(msg, Order, TargetCharacter, IsNewOrder); } - public struct OrderMessageInfo + public readonly struct OrderMessageInfo { public Identifier OrderIdentifier { get; } public OrderPrefab OrderPrefab => OrderPrefab.Prefabs[OrderIdentifier]; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index fcc0bed07..6f5bf9235 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -39,6 +39,7 @@ namespace Barotrauma.Networking ServerMessage, ConsoleUsage, Money, + DoSProtection, Karma, Talent, Error, @@ -55,6 +56,7 @@ namespace Barotrauma.Networking { MessageType.ServerMessage, new Color(157, 225, 160) }, { MessageType.ConsoleUsage, new Color(0, 162, 232) }, { MessageType.Money, Color.Green }, + { MessageType.DoSProtection, Color.OrangeRed }, { MessageType.Karma, new Color(75, 88, 255) }, { MessageType.Talent, new Color(125, 125, 255) }, { MessageType.Error, Color.Red } @@ -71,6 +73,7 @@ namespace Barotrauma.Networking { MessageType.ServerMessage, "ServerMessage" }, { MessageType.ConsoleUsage, "ConsoleUsage" }, { MessageType.Money, "Money" }, + { MessageType.DoSProtection, "DoSProtection" }, { MessageType.Karma, "Karma" }, { MessageType.Talent, "Talent" }, { MessageType.Error, "Error" } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 01cbd8d0c..d4ed2e781 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -42,6 +42,11 @@ namespace Barotrauma.Networking partial class ServerSettings : ISerializableEntity { + public const int PacketLimitMin = 1200, + PacketLimitWarning = 2400, + PacketLimitDefault = 2400, + PacketLimitMax = 10000; + public const string SettingsFile = "serversettings.xml"; [Flags] @@ -111,27 +116,24 @@ namespace Barotrauma.Networking switch (typeString) { case "float": - if (!(a is float?)) return false; - if (!(b is float?)) return false; - return MathUtils.NearlyEqual((float)a, (float)b); + if (a is not float fa) { return false; } + if (b is not float fb) { return false; } + return MathUtils.NearlyEqual(fa, fb); case "int": - if (!(a is int?)) return false; - if (!(b is int?)) return false; - return (int)a == (int)b; + if (a is not int ia) { return false; } + if (b is not int ib) { return false; } + return ia == ib; case "bool": - if (!(a is bool?)) return false; - if (!(b is bool?)) return false; - return (bool)a == (bool)b; + if (a is not bool ba) { return false; } + if (b is not bool bb) { return false; } + return ba == bb; case "Enum": - if (!(a is Enum)) return false; - if (!(b is Enum)) return false; - return ((Enum)a).Equals((Enum)b); + if (a is not Enum ea) { return false; } + if (b is not Enum eb) { return false; } + return ea.Equals(eb); default: - if (a == null || b == null) - { - return (a == null) == (b == null); - } - return a.ToString().Equals(b.ToString(), StringComparison.OrdinalIgnoreCase); + return ReferenceEquals(a,b) + || string.Equals(a?.ToString(), b?.ToString(), StringComparison.OrdinalIgnoreCase); } } @@ -204,7 +206,7 @@ namespace Barotrauma.Networking public void Write(IWriteMessage msg, object overrideValue = null) { - if (overrideValue == null) { overrideValue = Value; } + overrideValue ??= Value; switch (typeString) { case "float": @@ -293,10 +295,7 @@ namespace Barotrauma.Networking var saveProperties = SerializableProperty.GetProperties(this); foreach (var property in saveProperties) { - object value = property.GetValue(this); - if (value == null) { continue; } - - string typeName = SerializableProperty.GetSupportedTypeName(value.GetType()); + string typeName = SerializableProperty.GetSupportedTypeName(property.PropertyType); if (typeName != null || property.PropertyType.IsEnum) { NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName); @@ -523,7 +522,7 @@ namespace Barotrauma.Networking } } - [Serialize(LosMode.Opaque, IsPropertySaveable.Yes)] + [Serialize(LosMode.Transparent, IsPropertySaveable.Yes)] public LosMode LosMode { get; @@ -694,6 +693,20 @@ namespace Barotrauma.Networking private set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool EnableDoSProtection + { + get; + private set; + } + + [Serialize(PacketLimitDefault, IsPropertySaveable.Yes)] + public int MaxPacketAmount + { + get; + private set; + } + [Serialize("", IsPropertySaveable.Yes)] public string SelectedSubmarine { @@ -754,6 +767,9 @@ namespace Barotrauma.Networking get; set; } + + [Serialize(defaultValue: "", IsPropertySaveable.Yes)] + public LanguageIdentifier Language { get; set; } private SelectionMode subSelectionMode; [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index 14ae70d17..72e237b6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using System; using System.Collections.Generic; namespace Barotrauma diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 8286eee68..c4873a2cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -68,7 +68,7 @@ namespace Barotrauma public void TransformInToOutside() { - var sub = Submarine.FindContaining(ConvertUnits.ToDisplayUnits(Position)); + var sub = Submarine.FindContainingInLocalCoordinates(ConvertUnits.ToDisplayUnits(Position)); if (sub != null) { Position += ConvertUnits.ToSimUnits(sub.Position); @@ -119,7 +119,9 @@ namespace Barotrauma } private Shape bodyShape; - public float height, width, radius; + public float Height { get; private set; } + public float Width { get; private set; } + public float Radius { get; private set; } private readonly float density; @@ -385,7 +387,7 @@ namespace Barotrauma float height = ConvertUnits.ToSimUnits(colliderParams.Height) * colliderParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(colliderParams.Width) * colliderParams.Ragdoll.LimbScale; density = Physics.NeutralDensity; - CreateBody(width, height, radius, density, BodyType.Dynamic, + CreateBody(width, height, radius, density, colliderParams.BodyType, Physics.CollisionCharacter, Physics.CollisionWall | Physics.CollisionLevel, findNewContacts); @@ -434,9 +436,17 @@ namespace Barotrauma float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale; density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", Physics.NeutralDensity), MinDensity); Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); - CreateBody(width, height, radius, density, bodyType, collisionCategory, collidesWith, findNewContacts); - _collisionCategories = collisionCategory; - _collidesWith = collidesWith; + if (element.GetAttributeBool("ignorecollision", false)) + { + _collisionCategories = Category.None; + _collidesWith = Category.None; + } + else + { + _collisionCategories = collisionCategory; + _collidesWith = collidesWith; + } + CreateBody(width, height, radius, density, bodyType, _collisionCategories, _collidesWith, findNewContacts); FarseerBody.Friction = element.GetAttributeFloat("friction", 0.5f); FarseerBody.Restitution = element.GetAttributeFloat("restitution", 0.05f); FarseerBody.UserData = this; @@ -472,9 +482,9 @@ namespace Barotrauma { DebugConsole.ThrowError("Invalid physics body dimensions (width: " + width + ", height: " + height + ", radius: " + radius + ")"); } - this.width = width; - this.height = height; - this.radius = radius; + Width = width; + Height = height; + Radius = radius; _collisionCategories = collisionCategory; _collidesWith = collidesWith; } @@ -492,16 +502,16 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - pos = new Vector2(0.0f, height / 2 + radius); + pos = new Vector2(0.0f, Height / 2 + Radius); break; case Shape.HorizontalCapsule: - pos = new Vector2(width / 2 + radius, 0.0f); + pos = new Vector2(Width / 2 + Radius, 0.0f); break; case Shape.Circle: - pos = new Vector2(0.0f, radius); + pos = new Vector2(0.0f, Radius); break; case Shape.Rectangle: - pos = height > width ? new Vector2(0, height / 2) : new Vector2(width / 2, 0); + pos = Height > Width ? new Vector2(0, Height / 2) : new Vector2(Width / 2, 0); break; default: throw new NotImplementedException(); @@ -514,13 +524,13 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - return height / 2 + radius; + return Height / 2 + Radius; case Shape.HorizontalCapsule: - return width / 2 + radius; + return Width / 2 + Radius; case Shape.Circle: - return radius; + return Radius; case Shape.Rectangle: - return new Vector2(width * 0.5f, height * 0.5f).Length(); + return new Vector2(Width * 0.5f, Height * 0.5f).Length(); default: throw new NotImplementedException(); } @@ -531,13 +541,13 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - return new Vector2(radius * 2, height + radius * 2); + return new Vector2(Radius * 2, Height + Radius * 2); case Shape.HorizontalCapsule: - return new Vector2(width + radius * 2, radius * 2); + return new Vector2(Width + Radius * 2, Radius * 2); case Shape.Circle: - return new Vector2(radius * 2); + return new Vector2(Radius * 2); case Shape.Rectangle: - return new Vector2(width, height); + return new Vector2(Width, Height); default: throw new NotImplementedException(); } @@ -548,24 +558,24 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - radius = Math.Max(size.X / 2, 0); - height = Math.Max(size.Y - size.X, 0); - width = 0; + Radius = Math.Max(size.X / 2, 0); + Height = Math.Max(size.Y - size.X, 0); + Width = 0; break; case Shape.HorizontalCapsule: - radius = Math.Max(size.Y / 2, 0); - width = Math.Max(size.X - size.Y, 0); - height = 0; + Radius = Math.Max(size.Y / 2, 0); + Width = Math.Max(size.X - size.Y, 0); + Height = 0; break; case Shape.Circle: - radius = Math.Max(Math.Min(size.X, size.Y) / 2, 0); - width = 0; - height = 0; + Radius = Math.Max(Math.Min(size.X, size.Y) / 2, 0); + Width = 0; + Height = 0; break; case Shape.Rectangle: - width = Math.Max(size.X, 0); - height = Math.Max(size.Y, 0); - radius = 0; + Width = Math.Max(size.X, 0); + Height = Math.Max(size.Y, 0); + Radius = 0; break; default: throw new NotImplementedException(); @@ -830,7 +840,7 @@ namespace Barotrauma Vector2 velDir = LinearVelocity / speed; float vel = speed * 2.0f; - float drag = vel * vel * Math.Max(height + radius * 2, height); + float drag = vel * vel * Math.Max(Height + Radius * 2, Height); dragForce = Math.Min(drag, Mass * 500.0f) * -velDir; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs index e5bf41bca..aa877db24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -10,6 +12,8 @@ namespace Barotrauma { public Identifier VariantOf { get; } + public T? ParentPrefab { get; set; } + public void InheritFrom(T parent); } @@ -20,8 +24,10 @@ namespace Barotrauma #warning TODO: fix %ModDir% instances in the base element such that they become %ModDir:BaseMod% if necessary return variantElement.Element.CreateVariantXML(baseElement.Element).FromPackage(variantElement.ContentPackage); } - - public static XElement CreateVariantXML(this XElement variantElement, XElement baseElement) + + public delegate void VariantXMLChecker(XElement originalElement, XElement? variantElement, XElement result); + + public static XElement CreateVariantXML(this XElement variantElement, XElement baseElement, VariantXMLChecker? checker = null) { XElement newElement = new XElement(variantElement.Name); newElement.Add(baseElement.Attributes()); @@ -31,6 +37,9 @@ namespace Barotrauma void ReplaceElement(XElement element, XElement replacement) { + XElement originalElement = new XElement(element); + + List newElementsFromBase = new List(element.Elements()); List elementsToRemove = new List(); foreach (XAttribute attribute in replacement.Attributes()) { @@ -48,6 +57,7 @@ namespace Barotrauma if (replacementSubElement.Name.ToString().Equals("clear", StringComparison.OrdinalIgnoreCase)) { matchingElementFound = true; + newElementsFromBase.Clear(); elementsToRemove.AddRange(element.Elements()); break; } @@ -65,6 +75,7 @@ namespace Barotrauma ReplaceElement(subElement, replacementSubElement); } matchingElementFound = true; + newElementsFromBase.Remove(subElement); break; } i++; @@ -75,11 +86,16 @@ namespace Barotrauma } } elementsToRemove.ForEach(e => e.Remove()); + checker?.Invoke(originalElement, replacement, element); + foreach (XElement newElement in newElementsFromBase) + { + checker?.Invoke(newElement, null, newElement); + } } void ReplaceAttribute(XElement element, XAttribute newAttribute) { - XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + XAttribute? existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); if (existingAttribute == null) { element.Add(newAttribute); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 4b3b7edc9..e11ea36db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -125,7 +125,7 @@ namespace Barotrauma public Node? AddNodeAndInheritors(Identifier id) { - if (!prefabCollection.TryGet(id, out T? prefab)) { return null; } + if (!prefabCollection.TryGet(id, out T? _, requireInheritanceValid: false)) { return null; } if (!IdToNode.TryGetValue(id, out var node)) { @@ -139,24 +139,25 @@ namespace Barotrauma //all inheritors so let's just return this immediately return node; } - - prefabCollection - .Cast>() - .Where(p => p.VariantOf == id) - .Cast() - .ForEach(p => - { - var inheritorNode = AddNodeAndInheritors(p.Identifier); - if (inheritorNode is null) { return; } - RootNodes.Remove(inheritorNode); - inheritorNode.Parent = node; - node.Inheritors.Add(inheritorNode); - }); + var enumerator = prefabCollection.GetEnumerator(requireInheritanceValid: false); + while (enumerator.MoveNext()) + { + T p = enumerator.Current; + if (p is not IImplementsVariants implementsVariants || implementsVariants.VariantOf != id) + { + continue; + } + var inheritorNode = AddNodeAndInheritors(p.Identifier); + if (inheritorNode is null) { continue; } + RootNodes.Remove(inheritorNode); + inheritorNode.Parent = node; + node.Inheritors.Add(inheritorNode); + } return node; } - private void FindCycles(in Node node, HashSet uncheckedNodes) + private static void FindCycles(in Node node, HashSet uncheckedNodes) { HashSet checkedNodes = new HashSet(); List hierarchyPositions = new List(); @@ -183,24 +184,45 @@ namespace Barotrauma public void InvokeCallbacks() { HashSet uncheckedNodes = IdToNode.Values.ToHashSet(); - IdToNode.Values.ForEach(v => FindCycles(v, uncheckedNodes)); + IdToNode.Values.ForEach(v => PrefabCollection.InheritanceTreeCollection.FindCycles(v, uncheckedNodes)); void invokeCallbacksForNode(Node node) { - if (!prefabCollection.TryGet(node.Identifier, out var p) || - !(p is IImplementsVariants prefab)) { return; } - if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent)) { prefab.InheritFrom(parent!); } + if (!prefabCollection.TryGet(node.Identifier, out var p, requireInheritanceValid: false) || + p is not IImplementsVariants prefab) { return; } + if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent, requireInheritanceValid: false)) + { + prefab.InheritFrom(parent); + prefab.ParentPrefab = parent; + } node.Inheritors.ForEach(invokeCallbacksForNode); } RootNodes.ForEach(invokeCallbacksForNode); } } + private static bool IsInheritanceValid(T? prefab) + { + if (prefab == null) { return false; } + return + prefab is not IImplementsVariants implementsVariants || + (implementsVariants.VariantOf.IsEmpty || (implementsVariants.ParentPrefab != null && IsInheritanceValid(implementsVariants.ParentPrefab))); + } + private void HandleInheritance(Identifier prefabIdentifier) => HandleInheritance(prefabIdentifier.ToEnumerable()); private void HandleInheritance(IEnumerable identifiers) { if (!implementsVariants) { return; } + foreach (var id in identifiers) + { + if (!TryGet(id, out T? prefab, requireInheritanceValid: false)) { continue; } + if (prefab is IImplementsVariants implementsVariants && !implementsVariants.VariantOf.IsEmpty) + { + //reset parent prefab, it'll get set in InvokeCallbacks if the inheritance is valid + implementsVariants.ParentPrefab = null; + } + } InheritanceTreeCollection inheritanceTreeCollection = new InheritanceTreeCollection(this); inheritanceTreeCollection.AddNodesAndInheritors(identifiers); inheritanceTreeCollection.InvokeCallbacks(); @@ -213,9 +235,11 @@ namespace Barotrauma { get { - foreach (var prefab in prefabs) + foreach (var kvp in prefabs) { - yield return prefab; + var prefab = kvp.Value.ActivePrefab; + if (!IsInheritanceValid(prefab)) { continue; } + yield return kvp; } } } @@ -231,7 +255,8 @@ namespace Barotrauma { Prefab.DisallowCallFromConstructor(); var prefab = prefabs[identifier].ActivePrefab; - if (prefab != null && !IsPrefabOverriddenByFile(prefab)) + if (prefab != null && !IsPrefabOverriddenByFile(prefab) && + IsInheritanceValid(prefab)) { return prefab; } @@ -258,12 +283,17 @@ namespace Barotrauma /// The matching prefab (if one is found) /// Whether a prefab with the identifier exists or not public bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result) + { + return TryGet(identifier, out result, requireInheritanceValid: true); + } + + private bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result, bool requireInheritanceValid) { Prefab.DisallowCallFromConstructor(); - if (prefabs.TryGetValue(identifier, out PrefabSelector? selector)) + if (prefabs.TryGetValue(identifier, out PrefabSelector? selector) && selector.ActivePrefab != null) { result = selector!.ActivePrefab; - return true; + return !requireInheritanceValid || IsInheritanceValid(result); } else { @@ -304,7 +334,7 @@ namespace Barotrauma public bool ContainsKey(Identifier identifier) { Prefab.DisallowCallFromConstructor(); - return prefabs.ContainsKey(identifier); + return TryGet(identifier, out _); } public bool ContainsKey(string k) => prefabs.ContainsKey(k.ToIdentifier()); @@ -460,6 +490,19 @@ namespace Barotrauma topMostOverrideFile = overrideFiles.Any() ? overrideFiles.First(f1 => overrideFiles.All(f2 => f1.ContentPackage.Index >= f2.ContentPackage.Index)) : null; OnSort?.Invoke(); HandleInheritance(this.Select(p => p.Identifier)); + + var enumerator = GetEnumerator(requireInheritanceValid: false); + while (enumerator.MoveNext()) + { + T p = enumerator.Current; + if (p is IImplementsVariants implementsVariants && !IsInheritanceValid(p)) + { + DebugConsole.ThrowError( + $"Error in content package \"{p.ContentFile.ContentPackage.Name}\": " + + $"could not find the prefab \"{implementsVariants.VariantOf}\" the prefab \"{p.Identifier}\" is configured as a variant of."); + continue; + } + } } /// @@ -467,15 +510,19 @@ namespace Barotrauma /// /// IEnumerator public IEnumerator GetEnumerator() + { + return GetEnumerator(requireInheritanceValid: true); + } + + private IEnumerator GetEnumerator(bool requireInheritanceValid) { Prefab.DisallowCallFromConstructor(); - foreach (var kpv in prefabs) + foreach (var kvp in prefabs) { - var prefab = kpv.Value.ActivePrefab; - if (prefab != null && !IsPrefabOverriddenByFile(prefab)) - { - yield return prefab; - } + var prefab = kvp.Value.ActivePrefab; + if (prefab == null || IsPrefabOverriddenByFile(prefab)) { continue; } + if (requireInheritanceValid && !IsInheritanceValid(prefab)) { continue; } + yield return prefab; } } @@ -485,7 +532,7 @@ namespace Barotrauma /// IEnumerator IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); + return GetEnumerator(requireInheritanceValid: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index 62b8c8056..557860b69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -201,17 +201,28 @@ namespace Voronoi2 public bool IsPointInside(Vector2 point) { + if (!IsPointInsideAABB(point, margin: 0.0f)) { return false; } Vector2 transformedPoint = point - Translation; - if (Edges.All(e => e.Point1.X < transformedPoint.X && e.Point2.X < transformedPoint.X)) { return false; } - if (Edges.All(e => e.Point1.Y < transformedPoint.Y && e.Point2.Y < transformedPoint.Y)) { return false; } - if (Edges.All(e => e.Point1.X > transformedPoint.X && e.Point2.X > transformedPoint.X)) { return false; } - if (Edges.All(e => e.Point1.Y > transformedPoint.Y && e.Point2.Y > transformedPoint.Y)) { return false; } foreach (GraphEdge edge in Edges) { if (MathUtils.LinesIntersect(transformedPoint, Center - Translation, edge.Point1, edge.Point2)) { return false; } } return true; } + + public bool IsPointInsideAABB(Vector2 point2, float margin) + { + Vector2 transformedPoint = point2 - Translation; + Vector2 max = transformedPoint + Vector2.One * margin; + Vector2 min = transformedPoint - Vector2.One * margin; + + if (Edges.All(e => e.Point1.X < min.X && e.Point2.X < min.X)) { return false; } + if (Edges.All(e => e.Point1.Y < min.Y && e.Point2.Y < min.Y)) { return false; } + if (Edges.All(e => e.Point1.X > max.X && e.Point2.X > max.X)) { return false; } + if (Edges.All(e => e.Point1.Y > max.Y && e.Point2.Y > max.Y)) { return false; } + + return true; + } } public class GraphEdge diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 7b97635be..9b8bc37d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -61,10 +61,7 @@ namespace Barotrauma GameMain.GameSession?.CrewManager?.AutoShowCrewList(); #endif - foreach (MapEntity entity in MapEntity.mapEntityList) - { - entity.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); #if RUN_PHYSICS_IN_SEPARATE_THREAD var physicsThread = new Thread(ExecutePhysics) @@ -140,10 +137,7 @@ namespace Barotrauma { if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) { body.Update(); } } - foreach (MapEntity e in MapEntity.mapEntityList) - { - e.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); #if CLIENT var sw = new System.Diagnostics.Stopwatch(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index c78758bf3..5e5317022 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -154,6 +154,7 @@ namespace Barotrauma { typeof(float), "float" }, { typeof(string), "string" }, { typeof(Identifier), "identifier" }, + { typeof(LanguageIdentifier), "languageidentifier" }, { typeof(LocalizedString), "localizedstring" }, { typeof(Point), "point" }, { typeof(Vector2), "vector2" }, @@ -240,7 +241,7 @@ namespace Barotrauma switch (typeName) { case "bool": - bool boolValue = value == "true" || value == "True"; + bool boolValue = value.ToIdentifier() == "true"; if (TrySetBoolValueWithoutReflection(parentObject, boolValue)) { return true; } PropertyInfo.SetValue(parentObject, boolValue, null); break; @@ -290,6 +291,9 @@ namespace Barotrauma case "identifier": PropertyInfo.SetValue(parentObject, value.ToIdentifier()); break; + case "languageidentifier": + PropertyInfo.SetValue(parentObject, value.ToLanguageIdentifier()); + break; case "localizedstring": PropertyInfo.SetValue(parentObject, new RawLString(value)); break; @@ -373,6 +377,9 @@ namespace Barotrauma case "identifier": PropertyInfo.SetValue(parentObject, new Identifier((string)value)); return true; + case "languageidentifier": + PropertyInfo.SetValue(parentObject, ((string)value).ToLanguageIdentifier()); + return true; case "localizedstring": PropertyInfo.SetValue(parentObject, new RawLString((string)value)); return true; @@ -556,7 +563,7 @@ namespace Barotrauma public static string GetSupportedTypeName(Type type) { - if (type.IsEnum) return "Enum"; + if (type.IsEnum) { return "Enum"; } if (!supportedTypes.TryGetValue(type, out string typeName)) { return null; @@ -693,6 +700,29 @@ namespace Barotrauma case nameof(Character.SpeedMultiplier): { if (parentObject is Character character) { value = character.SpeedMultiplier; return true; } } break; + case nameof(Character.PropulsionSpeedMultiplier): + { if (parentObject is Character character) { value = character.PropulsionSpeedMultiplier; return true; } } + break; + case nameof(Character.LowPassMultiplier): + { if (parentObject is Character character) { value = character.LowPassMultiplier; return true; } } + break; + case nameof(Character.HullOxygenPercentage): + { + if (parentObject is Character character) + { + value = character.HullOxygenPercentage; + return true; + } + else if (parentObject is Item item) + { + value = item.HullOxygenPercentage; + return true; + } + } + break; + case nameof(Door.Stuck): + { if (parentObject is Door door) { value = door.Stuck; return true; } } + break; } return false; } @@ -740,6 +770,23 @@ namespace Barotrauma case nameof(Controller.State): if (parentObject is Controller controller) { value = controller.State; return true; } break; + case nameof(Character.InWater): + { + if (parentObject is Character character) + { + value = character.InWater; + return true; + } + else if (parentObject is Item item) + { + value = item.InWater; + return true; + } + } + break; + case nameof(Rope.Snapped): + if (parentObject is Rope rope) { value = rope.Snapped; return true; } + break; } return false; } @@ -769,7 +816,7 @@ namespace Barotrauma switch (Name) { case nameof(Item.Condition): - if (parentObject is Item item) { item.Condition = value; return true; } + { if (parentObject is Item item) { item.Condition = value; return true; } } break; case nameof(Powered.Voltage): if (parentObject is Powered powered) { powered.Voltage = value; return true; } @@ -801,6 +848,9 @@ namespace Barotrauma case nameof(Character.PropulsionSpeedMultiplier): { if (parentObject is Character character) { character.PropulsionSpeedMultiplier = value; return true; } } break; + case nameof(Item.Scale): + { if (parentObject is Item item) { item.Scale = value; return true; } } + break; } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 9e662479a..ebd748ba2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -213,6 +213,16 @@ namespace Barotrauma return splitValue; } + public static Identifier[] GetAttributeIdentifierArray(this XElement element, Identifier[] defaultValue, params string[] matchingAttributeName) + { + if (element == null) { return defaultValue; } + foreach (string name in matchingAttributeName) + { + var value = element.GetAttributeIdentifierArray(name, defaultValue); + if (value != defaultValue) { return value; } + } + return defaultValue; + } public static Identifier[] GetAttributeIdentifierArray(this XElement element, string name, Identifier[] defaultValue, bool trim = true) { @@ -484,9 +494,18 @@ namespace Barotrauma { var attr = element?.GetAttribute(name); if (attr == null) { return defaultValue; } - return Enum.TryParse(attr.Value, true, out T result) ? result : - int.TryParse(attr.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out int resultInt) ? Unsafe.As(ref resultInt) : - defaultValue; + + if (Enum.TryParse(attr.Value, true, out T result)) + { + return result; + } + else if (int.TryParse(attr.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out int resultInt)) + { + return Unsafe.As(ref resultInt); + } + DebugConsole.ThrowError($"Error in {attr}! \"{attr}\" is not a valid {typeof(T).Name} value"); + return default; + } public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) @@ -608,10 +627,18 @@ namespace Barotrauma return mouseButton; } else if (int.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out int mouseButtonInt) && - (Enum.GetValues(typeof(MouseButton)) as MouseButton[]).Contains((MouseButton)mouseButtonInt)) + Enum.GetValues().Contains((MouseButton)mouseButtonInt)) { return (MouseButton)mouseButtonInt; } + else if (string.Equals(strValue, "LeftMouse", StringComparison.OrdinalIgnoreCase)) + { + return !PlayerInput.MouseButtonsSwapped() ? MouseButton.PrimaryMouse : MouseButton.SecondaryMouse; + } + else if (string.Equals(strValue, "RightMouse", StringComparison.OrdinalIgnoreCase)) + { + return !PlayerInput.MouseButtonsSwapped() ? MouseButton.SecondaryMouse : MouseButton.PrimaryMouse; + } return defaultValue; } #endif @@ -807,7 +834,15 @@ namespace Barotrauma #endif return Color.White; } - + if (stringColor.StartsWith("faction.", StringComparison.OrdinalIgnoreCase)) + { + Identifier factionId = stringColor.Substring(8).ToIdentifier(); + if (FactionPrefab.Prefabs.TryGet(factionId, out var faction)) + { + return faction.IconColor; + } + return Color.White; + } string[] strComponents = stringColor.Split(','); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs index 5886c8ba3..617058963 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs @@ -1,13 +1,134 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Linq; +using Barotrauma.IO; +using XmlWriterSettings = System.Xml.XmlWriterSettings; +#nullable enable namespace Barotrauma { - public class CreatureMetrics + public static class CreatureMetrics { - public readonly HashSet RecentlyEncountered = new HashSet(); - public readonly HashSet Encountered = new HashSet(); - public readonly HashSet Killed = new HashSet(); + private const string path = "creature_metrics.xml"; - public readonly static CreatureMetrics Instance = new CreatureMetrics(); + /// + /// Resets every round. + /// + public static HashSet RecentlyEncountered { get; private set; } = new HashSet(); + public static HashSet Encountered { get; private set; } = new HashSet(); + public static HashSet Unlocked { get; private set; } = new HashSet(); + public static HashSet Killed { get; private set; } = new HashSet(); + public static bool IsInitialized { get; private set; } + public static bool UnlockAll { get; set; } + + public static void Init() + { + IsInitialized = true; + if (File.Exists(path)) + { + Load(); + } + Save(); + } + + private static void Load() + { + XDocument doc = XMLExtensions.TryLoadXml(path); + XElement? root = doc?.Root; + if (root == null) + { + DebugConsole.AddWarning($"Failed to load creature metrics from {path}!"); + return; + } + UnlockAll = root.GetAttributeBool(nameof(UnlockAll), UnlockAll); + Unlocked = new HashSet(root.GetAttributeIdentifierArray(nameof(Unlocked), Array.Empty())); + Encountered = new HashSet(root.GetAttributeIdentifierArray(nameof(Encountered), Array.Empty())); + Killed = new HashSet(root.GetAttributeIdentifierArray(nameof(Killed), Array.Empty())); + SyncSets(); + } + + public static void Save() + { + if (!IsInitialized) + { + throw new Exception("Creature Metrics not yet initialized!"); + } + SyncSets(); + XDocument configDoc = new XDocument(); + XElement root = new XElement("CreatureMetrics"); + configDoc.Add(root); + root.SetAttributeValue(nameof(UnlockAll), UnlockAll); + root.SetAttributeValue(nameof(Unlocked), string.Join(",", Unlocked).Trim().ToLowerInvariant()); + root.SetAttributeValue(nameof(Encountered), string.Join(",", Encountered).Trim().ToLowerInvariant()); + root.SetAttributeValue(nameof(Killed), string.Join(",", Killed).Trim().ToLowerInvariant()); + configDoc.SaveSafe(path); + XmlWriterSettings settings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true, + NewLineOnAttributes = true + }; + try + { + using var writer = XmlWriter.Create(path, settings); + configDoc.WriteTo(writer); + writer.Flush(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving creature metrics failed.", e); + GameAnalyticsManager.AddErrorEventOnce("CreatureMetrics.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error, + "Saving creature metrics failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); + } + } + + public static void RecordKill(Identifier species) + { + AddEncounter(species); + if (!Killed.Contains(species)) + { + Killed.Add(species); + } + } + + public static void AddEncounter(Identifier species) + { + if (species == CharacterPrefab.HumanSpeciesName) { return; } + if (Encountered.Contains(species)) { return; } + Encountered.Add(species); + RecentlyEncountered.Add(species); + UnlockInEditor(species); + } + + private static IEnumerable? vanillaCharacters; + public static void UnlockInEditor(Identifier species) + { + if (species == CharacterPrefab.HumanSpeciesName) { return; } + if (Unlocked.Contains(species)) { return; } + vanillaCharacters ??= GameMain.VanillaContent.GetFiles(); + var contentFile = CharacterPrefab.FindBySpeciesName(species); + if (contentFile == null) { return; } + if (!vanillaCharacters.Contains(contentFile.ContentFile)) + { + // Don't try to unlock custom characters. They are always unlocked. + return; + } + Unlocked.Add(species); + } + + private static void SyncSets() + { + // Ensure that all killed are also encountered and both unlocked. + // Otherwise we could permanently hide some creatures by manually adding them to the encountered or by removing from unlocked in the xml file. + foreach (var species in Killed) + { + Encountered.Add(species); + } + foreach (var species in Encountered) + { + Unlocked.Add(species); + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 5776bc9e0..2e3444126 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -25,7 +25,8 @@ namespace Barotrauma { None = 0, Transparent = 1, - Opaque = 2 + Opaque = 2, + BlockOutsideView = 3 } public enum VoiceMode @@ -110,6 +111,7 @@ namespace Barotrauma #if CLIENT retVal.KeyMap = new KeyMapping(element.GetChildElements("keymapping"), retVal.KeyMap); retVal.InventoryKeyMap = new InventoryKeyMapping(element.GetChildElements("inventorykeymapping"), retVal.InventoryKeyMap); + retVal.SavedCampaignSettings = element.GetChildElement("campaignsettings"); LoadSubEditorImages(element); #endif @@ -139,6 +141,9 @@ namespace Barotrauma public bool DisableInGameHints; public bool EnableSubmarineAutoSave; public Identifier QuickStartSub; +#if CLIENT + public XElement SavedCampaignSettings; +#endif #if DEBUG public bool UseSteamMatchmaking; public bool RequireSteamAuthentication; @@ -230,7 +235,7 @@ namespace Barotrauma SoundVolume = 0.5f, UiVolume = 0.3f, VoiceChatVolume = 0.5f, - VoiceChatCutoffPrevention = 0, + VoiceChatCutoffPrevention = 200, MicrophoneVolume = 5, MuteOnFocusLost = false, DynamicRangeCompressionEnabled = true, @@ -618,6 +623,8 @@ namespace Barotrauma root.Add(inventoryKeyMappingElement); SubEditorScreen.ImageManager.Save(root); + + root.Add(CampaignSettings.CurrentSettings.Save()); #endif configDoc.SaveSafe(PlayerConfigPath); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs new file mode 100644 index 000000000..55c5892e3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma; + +static class ServerLanguageOptions +{ + public readonly record struct LanguageOption( + string Label, + LanguageIdentifier Identifier, + ImmutableArray MapsFrom) + { + public static LanguageOption FromXElement(XElement element) + => new LanguageOption( + Label: + element.GetAttributeString("label", ""), + Identifier: + element.GetAttributeIdentifier("identifier", LanguageIdentifier.None.Value) + .ToLanguageIdentifier(), + MapsFrom: + element.GetAttributeIdentifierArray("mapsFrom", Array.Empty()) + .Select(id => id.ToLanguageIdentifier()).ToImmutableArray()); + } + + public static readonly ImmutableArray Options; + + static ServerLanguageOptions() + { + var languageOptionElements + = XMLExtensions.TryLoadXml("Data/languageoptions.xml")?.Root?.Elements() + ?? Enumerable.Empty(); + Options = languageOptionElements + // Convert the XElements into LanguageOptions immediately since they can be worked with more directly + .Select(LanguageOption.FromXElement) + // Remove options with duplicate identifiers + .DistinctBy(p => p.Identifier) + // Remove options where the label is empty or the identifier is missing + .Where(p => !p.Label.IsNullOrWhiteSpace() && p.Identifier != LanguageIdentifier.None) + // Sort the options based on the lexicographical order of the labels + .OrderBy(p => p.Label) + .ToImmutableArray(); + } + + public static LanguageIdentifier PickLanguage(LanguageIdentifier id) + { + if (id == LanguageIdentifier.None) + { + id = GameSettings.CurrentConfig.Language; + } + + foreach (var (_, identifier, mapsFrom) in Options) + { + if (id == identifier || mapsFrom.Contains(id)) + { + return identifier; + } + } + + return TextManager.DefaultLanguage; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs index 9cc87f0cc..d2e3f5467 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; -using System.Xml.Linq; using System.Linq; -using System; namespace Barotrauma { @@ -10,7 +8,7 @@ namespace Barotrauma public readonly List conditionals = new List(); public bool IsActive { get; private set; } = true; - public readonly PropertyConditional.Comparison Comparison; + public readonly PropertyConditional.LogicalOperatorType LogicalOperator; public readonly bool Exclusive; public ISerializableEntity Target { get; private set; } public Sprite Sprite { get; private set; } @@ -21,23 +19,14 @@ namespace Barotrauma { Target = target; Exclusive = element.GetAttributeBool("exclusive", Exclusive); - string comparison = element.GetAttributeString("comparison", null); - if (comparison != null) - { - Enum.TryParse(comparison, ignoreCase: true, out Comparison); - } + LogicalOperator = element.GetAttributeEnum(nameof(LogicalOperator), + element.GetAttributeEnum("comparison", LogicalOperator)); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - conditionals.Add(new PropertyConditional(attribute)); - } - } + conditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; case "sprite": Sprite = new Sprite(subElement, file: file, lazyLoad: lazyLoad); @@ -57,7 +46,7 @@ namespace Barotrauma } else { - IsActive = Comparison == PropertyConditional.Comparison.And ? conditionals.All(c => c.Matches(Target)) : conditionals.Any(c => c.Matches(Target)); + IsActive = LogicalOperator == PropertyConditional.LogicalOperatorType.And ? conditionals.All(c => c.Matches(Target)) : conditionals.Any(c => c.Matches(Target)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 60b0aca6d..b8e5e84f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -39,16 +39,10 @@ namespace Barotrauma public DelayedEffect(ContentXElement element, string parentDebugName) : base(element, parentDebugName) { - string delayTypeStr = element.GetAttributeString("delaytype", "timer"); - if (!Enum.TryParse(typeof(DelayTypes), delayTypeStr, ignoreCase: true, out var delayType)) + DelayTypes delayTypeAttr = element.GetAttributeEnum("delaytype", DelayTypes.Timer); + if (delayTypeAttr is DelayTypes.Timer) { - DebugConsole.ThrowError("Invalid delay type \"" + delayTypeStr + "\" in StatusEffect (" + parentDebugName + ")"); - } - switch (delayType) - { - case DelayTypes.Timer: - delay = element.GetAttributeFloat("delay", 1.0f); - break; + delay = element.GetAttributeFloat("delay", 1.0f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index b241816f3..cd9a41b1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -1,71 +1,246 @@ -using System; +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { - // TODO: This class should be refactored: - // - Use XElement instead of XAttribute in the constructor - // - Simplify, remove unnecessary conversions - // - Improve the flow so that the logic is undestandable. - // - Maybe add some test cases for the operators? - class PropertyConditional + /// + /// Conditionals are used by some in-game mechanics to require one + /// or more conditions to be met for those mechanics to be active. + /// For example, some StatusEffects use Conditionals to only trigger + /// if the affected character is alive. + /// + sealed class PropertyConditional { + // TODO: Make this testable and add tests + + /// + /// Category of properties to check against + /// public enum ConditionType { - Uncertain, - PropertyValue, + /// + /// Depending on what's available, check against either one + /// of the target object's properties or the strength of an + /// affliction. + /// + /// The target object's available properties depend on how that + /// object is defined in the [source code](https://github.com/Regalis11/Barotrauma). + /// + /// This is not applicable if the element contains the attribute + /// `SkillRequirement="true"`. + /// + /// + /// + /// + /// + PropertyValueOrAffliction, + + /// + /// Check against the target character's skill with the same name as the attribute. + /// + /// This is only applicable if the element contains the attribute + /// `SkillRequirement="true"`. + /// + /// + /// + /// + /// + SkillRequirement, + + /// + /// Check against the name of the target. + /// Name, + + /// + /// Check against the species identifier of the target. Only works on characters. + /// SpeciesName, + + /// + /// Check against the species group of the target. Only works on characters. + /// SpeciesGroup, + + /// + /// Check against the target's tags. Only works on items. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasTag, + + /// + /// Check against the tags of the target's active status effects. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasStatusTag, + + /// + /// Check against the target's specifier tags. In the vanilla game, these are the head index + /// and gender. See human.xml for more details. + /// + /// Several tags can be checked against by using a comma-separated list. + /// HasSpecifierTag, - Affliction, + + /// + /// Check against the target's entity type. + /// + /// The currently supported values are "character", "limb", "item", "structure" and "null". + /// EntityType, - LimbType, - SkillRequirement + + /// + /// Check against the target's limb type. See . + /// + LimbType } - public enum Comparison + public enum LogicalOperatorType { And, Or } - public enum OperatorType + /// + /// There are several ways to compare properties to values. The comparison operator + /// to use can be specified by placing one of the following before the value to compare + /// against. + /// + public enum ComparisonOperatorType { None, + + /// + /// Require that the property being checked equals the given value. + /// + /// This is the default operator used if none is specified. + /// Equals, + + /// + /// Require that the property being checked doesn't equal the given value. + /// NotEquals, + + /// + /// Require that the property being checked is less than the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// LessThan, + + /// + /// Require that the property being checked is less than or equal to the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// LessThanEquals, + + /// + /// Require that the property being checked is greater than the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// GreaterThan, + + /// + /// Require that the property being checked is greater than or equal to the given value. + /// + /// This can only be used to compare with numeric object properties, + /// affliction strengths and skill levels. + /// GreaterThanEquals } public readonly ConditionType Type; - public readonly OperatorType Operator; + public readonly ComparisonOperatorType ComparisonOperator; public readonly Identifier AttributeName; public readonly string AttributeValue; - public readonly string[] SplitAttributeValue; + public readonly ImmutableArray AttributeValueAsTags; public readonly float? FloatValue; - public readonly string TargetItemComponentName; + /// + /// If set to the name of one of the target's ItemComponents, the conditionals defined by this element check against the properties of that component. + /// Only works on items. + /// + public readonly string TargetItemComponent; - // Only used by attacks + /// + /// If set to true, the conditionals defined by this element check against the attacking character instead of the attacked character + /// public readonly bool TargetSelf; - // Only used by conditionals targeting an item (makes the conditional check the item/character whose inventory this item is inside) + /// + /// If set to true, the conditionals defined by this element check against the entity containing the target. + /// public readonly bool TargetContainer; - // Only used by conditionals targeting an item. By default, containers check the parent item. This allows you to check the grandparent instead. + + /// + /// If this and TargetContainer are set to true, the conditionals defined by this element check against the entity containing the target's container. + /// public readonly bool TargetGrandParent; + /// + /// If set to true, the conditionals defined by this element check against the items contained by the target. Only works with items. + /// public readonly bool TargetContainedItem; - // Remove this after refactoring - public static bool IsValid(XAttribute attribute) + public static IEnumerable FromXElement(XElement element, Predicate? predicate = null) + { + var targetItemComponent = element.GetAttributeString(nameof(TargetItemComponent), ""); + var targetContainer = element.GetAttributeBool(nameof(TargetContainer), false); + var targetSelf = element.GetAttributeBool(nameof(TargetSelf), false); + var targetGrandParent = element.GetAttributeBool(nameof(TargetGrandParent), false); + var targetContainedItem = element.GetAttributeBool(nameof(TargetContainedItem), false); + + ConditionType? overrideConditionType = null; + if (element.GetAttributeBool(nameof(ConditionType.SkillRequirement), false)) + { + overrideConditionType = ConditionType.SkillRequirement; + } + + foreach (var attribute in element.Attributes()) + { + if (!IsValid(attribute)) { continue; } + if (predicate != null && !predicate(attribute)) { continue; } + + var (comparisonOperator, attributeValueString) = ExtractComparisonOperatorFromConditionString(attribute.Value); + if (string.IsNullOrWhiteSpace(attributeValueString)) + { + DebugConsole.ThrowError($"Conditional attribute value is empty: {element}"); + continue; + } + + var conditionType = overrideConditionType ?? + (Enum.TryParse(attribute.Name.LocalName, ignoreCase: true, out ConditionType type) + ? type + : ConditionType.PropertyValueOrAffliction); + + yield return new PropertyConditional( + attributeName: attribute.NameAsIdentifier(), + comparisonOperator: comparisonOperator, + attributeValue: attributeValueString, + targetItemComponent: targetItemComponent, + targetSelf: targetSelf, + targetContainer: targetContainer, + targetGrandParent: targetGrandParent, + targetContainedItem: targetContainedItem, + conditionType: conditionType); + } + } + + private static bool IsValid(XAttribute attribute) { switch (attribute.Name.ToString().ToLowerInvariant()) { @@ -82,60 +257,63 @@ namespace Barotrauma } } - // TODO: use XElement instead of XAttribute (how to do without breaking the existing content?) - public PropertyConditional(XAttribute attribute) + private PropertyConditional( + Identifier attributeName, + ComparisonOperatorType comparisonOperator, + string attributeValue, + string targetItemComponent, + bool targetSelf, + bool targetContainer, + bool targetGrandParent, + bool targetContainedItem, + ConditionType conditionType) { - AttributeName = attribute.NameAsIdentifier(); - string attributeValueString = attribute.Value; - if (string.IsNullOrWhiteSpace(attributeValueString)) - { - DebugConsole.ThrowError($"Conditional attribute value is empty: {attribute.Parent}"); - return; - } - string valueString = attributeValueString; - string[] splitString = valueString.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (splitString.Length > 1) { valueString = string.Join(' ', splitString.Skip(1)); } - Operator = GetOperatorType(splitString[0]); + AttributeName = attributeName; - if (Operator == OperatorType.None) - { - Operator = OperatorType.Equals; - valueString = attributeValueString; - } + TargetItemComponent = targetItemComponent; + TargetSelf = targetSelf; + TargetContainer = targetContainer; + TargetGrandParent = targetGrandParent; + TargetContainedItem = targetContainedItem; - TargetItemComponentName = attribute.Parent.GetAttributeString("targetitemcomponent", ""); - TargetContainer = attribute.Parent.GetAttributeBool("targetcontainer", false); - TargetSelf = attribute.Parent.GetAttributeBool("targetself", false); - TargetGrandParent = attribute.Parent.GetAttributeBool("targetgrandparent", false); - TargetContainedItem = attribute.Parent.GetAttributeBool("targetcontaineditem", false); + Type = conditionType; - if (!Enum.TryParse(AttributeName.Value, true, out Type)) - { - Type = ConditionType.Uncertain; - } - - if (attribute.Parent.GetAttributeBool("skillrequirement", false)) - { - Type = ConditionType.SkillRequirement; - } - - AttributeValue = valueString; - SplitAttributeValue = valueString.Split(','); + ComparisonOperator = comparisonOperator; + AttributeValue = attributeValue; + AttributeValueAsTags = AttributeValue.Split(',') + //, options: StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(s => s.ToIdentifier()) + .ToImmutableArray(); if (float.TryParse(AttributeValue, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) { FloatValue = value; } } - public static OperatorType GetOperatorType(string op) + public static (ComparisonOperatorType ComparisonOperator, string ConditionStr) ExtractComparisonOperatorFromConditionString(string str) + { + str ??= ""; + + ComparisonOperatorType op = ComparisonOperatorType.Equals; + string conditionStr = str; + if (str.IndexOf(' ') is var i and >= 0) + { + op = GetComparisonOperatorType(str[..i]); + if (op != ComparisonOperatorType.None) { conditionStr = str[(i + 1)..]; } + else { op = ComparisonOperatorType.Equals; } + } + return (op, conditionStr); + } + + public static ComparisonOperatorType GetComparisonOperatorType(string op) { //thanks xml for not letting me use < or > in attributes :( - switch (op) + switch (op.ToLowerInvariant()) { case "e": case "eq": case "equals": - return OperatorType.Equals; + return ComparisonOperatorType.Equals; case "ne": case "neq": case "notequals": @@ -143,311 +321,280 @@ namespace Barotrauma case "!e": case "!eq": case "!equals": - return OperatorType.NotEquals; + return ComparisonOperatorType.NotEquals; case "gt": case "greaterthan": - return OperatorType.GreaterThan; + return ComparisonOperatorType.GreaterThan; case "lt": case "lessthan": - return OperatorType.LessThan; + return ComparisonOperatorType.LessThan; case "gte": case "gteq": case "greaterthanequals": - return OperatorType.GreaterThanEquals; + return ComparisonOperatorType.GreaterThanEquals; case "lte": case "lteq": case "lessthanequals": - return OperatorType.LessThanEquals; + return ComparisonOperatorType.LessThanEquals; default: - return OperatorType.None; + return ComparisonOperatorType.None; } } + private bool ComparisonOperatorIsNotEquals => ComparisonOperator == ComparisonOperatorType.NotEquals; - public bool Matches(ISerializableEntity target) + public bool Matches(ISerializableEntity? target) { - return Matches(target, TargetContainedItem); + return TargetContainedItem + ? MatchesContained(target) + : MatchesDirect(target); } - public bool Matches(ISerializableEntity target, bool checkContained) + private bool MatchesContained(ISerializableEntity? target) { - var type = Type; - if (type == ConditionType.Uncertain) + var containedItems = target switch { - type = AfflictionPrefab.Prefabs.ContainsKey(AttributeName) - ? ConditionType.Affliction - : ConditionType.PropertyValue; - } - - if (checkContained) + Item item + => item.ContainedItems, + ItemComponent ic + => ic.Item.ContainedItems, + Character {Inventory: { } characterInventory} + => characterInventory.AllItems, + _ + => Enumerable.Empty() + }; + foreach (var containedItem in containedItems) { - if (target is Item item) - { - foreach (var containedItem in item.ContainedItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } - else if (target is Items.Components.ItemComponent ic) - { - foreach (var containedItem in ic.Item.ContainedItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } - else if (target is Character character) - { - if (character.Inventory == null) { return false; } - foreach (var containedItem in character.Inventory.AllItems) - { - if (Matches(containedItem, checkContained: false)) { return true; } - } - return false; - } + if (MatchesDirect(containedItem)) { return true; } } + return false; + } - switch (type) + private bool MatchesDirect(ISerializableEntity? target) + { + Character? targetChar = target as Character; + if (target is Limb limb) { targetChar = limb.character; } + switch (Type) { - case ConditionType.PropertyValue: - SerializableProperty property; - if (target?.SerializableProperties == null) { return Operator == OperatorType.NotEquals; } - if (target.SerializableProperties.TryGetValue(AttributeName, out property)) + case ConditionType.PropertyValueOrAffliction: + // First try checking for a property belonging to the target + if (target?.SerializableProperties != null + && target.SerializableProperties.TryGetValue(AttributeName, out var property)) { - return Matches(target, property); + return PropertyMatchesRequirement(target, property); } - return false; - case ConditionType.Name: - if (target == null) { return Operator == OperatorType.NotEquals; } - return (Operator == OperatorType.Equals) == (target.Name == AttributeValue); + // Then try checking for an affliction affecting the target + if (targetChar is { CharacterHealth: { } health }) + { + var affliction = health.GetAffliction(AttributeName.ToIdentifier()); + float afflictionStrength = affliction?.Strength ?? 0f; + + return NumberMatchesRequirement(afflictionStrength); + } + return ComparisonOperatorIsNotEquals; + case ConditionType.SkillRequirement: + if (targetChar != null) + { + float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); + + return NumberMatchesRequirement(skillLevel); + } + return ComparisonOperatorIsNotEquals; case ConditionType.HasTag: - if (target == null) { return Operator == OperatorType.NotEquals; } - return MatchesTagCondition(target); + return ItemMatchesTagCondition(target); case ConditionType.HasStatusTag: - if (target == null) { return Operator == OperatorType.NotEquals; } + if (target == null) { return ComparisonOperatorIsNotEquals; } + + // TODO: revisit this. As written, the current behavior is: + // - ComparisonOperatorType.Equals: true when any effects have all tags + // - ComparisonOperatorType.NotEquals: true when none of the effects have any of the tags int matches = 0; - foreach (DurationListElement durationEffect in StatusEffect.DurationList) + + foreach (var durationEffect in StatusEffect.DurationList) { if (!durationEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) - { - if (durationEffect.Parent.HasTag(tag)) - { - matches++; - } - } + if (StatusEffectMatchesTagCondition(durationEffect.Parent)) { matches++; } } - foreach (DelayedListElement delayedEffect in DelayedEffect.DelayList) + + foreach (var delayedEffect in DelayedEffect.DelayList) { if (!delayedEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) - { - if (delayedEffect.Parent.HasTag(tag)) - { - matches++; - } - } + if (StatusEffectMatchesTagCondition(delayedEffect.Parent)) { matches++; } } - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; - case ConditionType.HasSpecifierTag: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character { Info: { } characterInfo })) { return false; } - return (Operator == OperatorType.Equals) == - SplitAttributeValue.All(v => characterInfo.Head.Preset.TagSet.Contains(v)); - } - case ConditionType.SpeciesName: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == (targetCharacter.SpeciesName == AttributeValue); - } - case ConditionType.SpeciesGroup: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == targetCharacter.Params.CompareGroup(AttributeValue.ToIdentifier()); - } - case ConditionType.EntityType: - switch (AttributeValue) - { - case "character": - case "Character": - return (Operator == OperatorType.Equals) == target is Character; - case "limb": - case "Limb": - return (Operator == OperatorType.Equals) == target is Limb; - case "item": - case "Item": - return (Operator == OperatorType.Equals) == target is Item; - case "structure": - case "Structure": - return (Operator == OperatorType.Equals) == target is Structure; - case "null": - return (Operator == OperatorType.Equals) == (target == null); - default: - return false; - } - case ConditionType.LimbType: - { - if (!(target is Limb limb)) - { - return false; - } - else - { - return limb.type.ToString().Equals(AttributeValue, StringComparison.OrdinalIgnoreCase); - } - } - case ConditionType.Affliction: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - - Character targetChar = target as Character; - if (target is Limb limb) { targetChar = limb.character; } - if (targetChar != null) - { - var health = targetChar.CharacterHealth; - if (health == null) { return false; } - var affliction = health.GetAffliction(AttributeName.ToIdentifier()); - float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; - - return ValueMatchesRequirement(afflictionStrength); - } - } - return false; - case ConditionType.SkillRequirement: - { - if (target == null) { return Operator == OperatorType.NotEquals; } - - if (target is Character targetChar) - { - float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); - - return ValueMatchesRequirement(skillLevel); - } - } - return false; + return ComparisonOperatorIsNotEquals + ? matches >= StatusEffect.DurationList.Count + DelayedEffect.DelayList.Count + : matches > 0; default: - return false; + bool equals = CheckOnlyEquality(target); + return ComparisonOperatorIsNotEquals + ? !equals + : equals; } } - private bool ValueMatchesRequirement(float testedValue) + private bool CheckOnlyEquality(ISerializableEntity? target) { - if (FloatValue.HasValue) + switch (Type) { - float value = FloatValue.Value; - switch (Operator) + case ConditionType.Name: + if (target == null) { return false; } + + return target.Name == AttributeValue; + case ConditionType.HasSpecifierTag: { - case OperatorType.Equals: - return testedValue == value; - case OperatorType.GreaterThan: - return testedValue > value; - case OperatorType.GreaterThanEquals: - return testedValue >= value; - case OperatorType.LessThan: - return testedValue < value; - case OperatorType.LessThanEquals: - return testedValue <= value; - case OperatorType.NotEquals: - return testedValue != value; + if (target is not Character {Info: { } characterInfo}) + { + return false; + } + + return AttributeValueAsTags.All(characterInfo.Head.Preset.TagSet.Contains); + } + case ConditionType.SpeciesName: + { + if (target is not Character targetCharacter) + { + return false; + } + + return targetCharacter.SpeciesName == AttributeValue; + } + case ConditionType.SpeciesGroup: + { + if (target is not Character targetCharacter) + { + return false; + } + + return CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Params.Group); + } + case ConditionType.EntityType: + return AttributeValue.ToLowerInvariant() switch + { + "character" + => target is Character, + "limb" + => target is Limb, + "item" + => target is Item, + "structure" + => target is Structure, + "null" + => target == null, + _ + => false + }; + case ConditionType.LimbType: + { + return target is Limb limb + && Enum.TryParse(AttributeValue, ignoreCase: true, out LimbType attributeLimbType) + && attributeLimbType == limb.type; } } return false; } - private bool MatchesTagCondition(ISerializableEntity target) + private bool SufficientTagMatches(int matches) { - if (!(target is Item item)) { return Operator == OperatorType.NotEquals; } - - int matches = 0; - foreach (string tag in SplitAttributeValue) - { - if (item.HasTag(tag)) - { - matches++; - } - } - //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + return ComparisonOperatorIsNotEquals + ? matches <= 0 + : matches >= AttributeValueAsTags.Length; } - public bool MatchesTagCondition(Identifier targetTag) + private bool ItemMatchesTagCondition(ISerializableEntity? target) + { + if (target is not Item item) { return ComparisonOperatorIsNotEquals; } + + int matches = 0; + foreach (var tag in AttributeValueAsTags) + { + if (item.HasTag(tag)) { matches++; } + } + return SufficientTagMatches(matches); + } + + public bool TargetTagMatchesTagCondition(Identifier targetTag) { if (targetTag.IsEmpty || Type != ConditionType.HasTag) { return false; } int matches = 0; - foreach (string tag in SplitAttributeValue) + foreach (var tag in AttributeValueAsTags) { - if (targetTag == tag) - { - matches++; - } + if (targetTag == tag) { matches++; } } - //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + return SufficientTagMatches(matches); } - // TODO: refactor and add tests - private bool Matches(ISerializableEntity target, SerializableProperty property) + private bool StatusEffectMatchesTagCondition(StatusEffect statusEffect) + { + int matches = 0; + foreach (var tag in AttributeValueAsTags) + { + if (statusEffect.HasTag(tag.Value)) { matches++; } + } + return SufficientTagMatches(matches); + } + + private bool NumberMatchesRequirement(float testedValue) + { + if (!FloatValue.HasValue) { return ComparisonOperatorIsNotEquals; } + float value = FloatValue.Value; + + return ComparisonOperator switch + { + ComparisonOperatorType.Equals + => MathUtils.NearlyEqual(testedValue, value), + ComparisonOperatorType.NotEquals + => !MathUtils.NearlyEqual(testedValue, value), + ComparisonOperatorType.GreaterThan + => testedValue > value, + ComparisonOperatorType.GreaterThanEquals + => testedValue >= value, + ComparisonOperatorType.LessThan + => testedValue < value, + ComparisonOperatorType.LessThanEquals + => testedValue <= value, + _ + => false + }; + } + + private bool PropertyMatchesRequirement(ISerializableEntity target, SerializableProperty property) { Type type = property.PropertyType; if (type == typeof(float) || type == typeof(int)) { float floatValue = property.GetFloatValue(target); - switch (Operator) - { - case OperatorType.Equals: - return MathUtils.NearlyEqual(floatValue, FloatValue.Value); - case OperatorType.NotEquals: - return !MathUtils.NearlyEqual(floatValue, FloatValue.Value); - case OperatorType.GreaterThan: - return floatValue > FloatValue.Value; - case OperatorType.LessThan: - return floatValue < FloatValue.Value; - case OperatorType.GreaterThanEquals: - return floatValue >= FloatValue.Value; - case OperatorType.LessThanEquals: - return floatValue <= FloatValue.Value; - } - return false; + return NumberMatchesRequirement(floatValue); } - switch (Operator) + switch (ComparisonOperator) { - case OperatorType.Equals: + case ComparisonOperatorType.Equals: + case ComparisonOperatorType.NotEquals: + bool equals; + if (type == typeof(bool)) { - if (type == typeof(bool)) - { - return property.GetBoolValue(target) == (AttributeValue == "true" || AttributeValue == "True"); - } - var value = property.GetValue(target); - return Equals(value, AttributeValue); + bool attributeValueBool = AttributeValue.IsTrueString(); + equals = property.GetBoolValue(target) == attributeValueBool; } - case OperatorType.NotEquals: + else { - if (type == typeof(bool)) - { - return property.GetBoolValue(target) != (AttributeValue == "true" || AttributeValue == "True"); - } var value = property.GetValue(target); - return !Equals(value, AttributeValue); + equals = AreValuesEquivalent(value, AttributeValue); } - case OperatorType.GreaterThan: - case OperatorType.LessThanEquals: - case OperatorType.LessThan: - case OperatorType.GreaterThanEquals: + + return ComparisonOperatorIsNotEquals + ? !equals + : equals; + default: DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " + "Make sure the type of the value set in the config files matches the type of the property."); - break; + return false; } - return false; - static bool Equals(object value, string desiredValue) + static bool AreValuesEquivalent(object? value, string desiredValue) { if (value == null) { @@ -455,7 +602,7 @@ namespace Barotrauma } else { - return value.ToString().Equals(desiredValue); + return (value.ToString() ?? "").Equals(desiredValue); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 9f3d59506..292976e65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -42,129 +42,183 @@ namespace Barotrauma } } - class AITrigger : ISerializableEntity - { - public string Name => "ai trigger"; - - public Dictionary SerializableProperties { get; set; } - - [Serialize(AIState.Idle, IsPropertySaveable.No)] - public AIState State { get; private set; } - - [Serialize(0f, IsPropertySaveable.No)] - public float Duration { get; private set; } - - [Serialize(1f, IsPropertySaveable.No)] - public float Probability { get; private set; } - - [Serialize(0f, IsPropertySaveable.No)] - public float MinDamage { get; private set; } - - [Serialize(true, IsPropertySaveable.No)] - public bool AllowToOverride { get; private set; } - - [Serialize(true, IsPropertySaveable.No)] - public bool AllowToBeOverridden { get; private set; } - - public bool IsTriggered { get; private set; } - - public float Timer { get; private set; } - - public bool IsActive { get; private set; } - - public bool IsPermanent { get; private set; } - - public void Launch() - { - IsTriggered = true; - IsActive = true; - IsPermanent = Duration <= 0; - if (!IsPermanent) - { - Timer = Duration; - } - } - - public void Reset() - { - IsTriggered = false; - IsActive = false; - Timer = 0; - } - - public void UpdateTimer(float deltaTime) - { - if (IsPermanent) { return; } - Timer -= deltaTime; - if (Timer < 0) - { - Timer = 0; - IsActive = false; - } - } - - public AITrigger(XElement element) - { - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - } - } - + /// + /// StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in some way, spawning things, playing sounds, + /// emitting particles, creating fire and explosions, increasing a characters' skill. They are a crucial part of modding Barotrauma: all kinds of + /// custom behaviors of an item or a creature for example are generally created using StatusEffects. + /// + /// + /// + /// Can be used to delay the execution of the effect. For example, you could have an effect that triggers when a character receives damage, + /// but takes 5 seconds before it starts to do anything. + /// + /// + /// An arbitrary tag (or a list of tags) that describe the status effect and can be used by Conditionals to check whether some StatusEffect is running. + /// For example, an item could execute a StatusEffect with the tag "poisoned" on some character, and the character could have an effect that makes + /// the character do something when an effect with that tag is active. + /// + /// + /// And/Or. Do all of the Conditionals defined in the effect be true for the effect to execute, or should the effect execute when any of them is true? + /// + /// + /// These are the meat of the StatusEffects. You can set, increment or decrement any value of the target, be it an item, character, limb or hull. + /// By default, the value is added to the existing value. If you want to instead set the value, use the setValue attribute. + /// For example, Condition="-5" would decrease the condition of the item the effect is targeting by 5 per second. If the target has no property + /// with the specified name, the attribute does nothing. + /// + /// partial class StatusEffect { + private static readonly ImmutableHashSet FieldNames; + static StatusEffect() + { + FieldNames = typeof(StatusEffect).GetFields().AsEnumerable().Select(f => f.Name.ToIdentifier()).ToImmutableHashSet(); + } + [Flags] public enum TargetType { + /// + /// The entity (item, character, limb) the StatusEffect is defined in. + /// This = 1, + /// + /// In the context of items, the container the item is inside (if any). In the context of limbs, the character the limb belongs to. + /// Parent = 2, + /// + /// The character the StatusEffect is defined in. In the context of items and attacks, the character using the item/attack. + /// Character = 4, + /// + /// The item(s) contained in the inventory of the entity the StatusEffect is defined in. + /// Contained = 8, + /// + /// Characters near the entity the StatusEffect is defined in. The range is defined using . + /// NearbyCharacters = 16, + /// + /// Items near the entity the StatusEffect is defined in. The range is defined using . + /// NearbyItems = 32, + /// + /// The entity the item/attack is being used on. + /// UseTarget = 64, + /// + /// The hull the entity is inside. + /// Hull = 128, + /// + /// The entity the item/attack is being used on. In the context of characters, one of the character's limbs (specify which one using ). + /// Limb = 256, + /// + /// All limbs of the character the effect is being used on. + /// AllLimbs = 512, + /// + /// Last limb of the character the effect is being used on. + /// LastLimb = 1024 } + /// + /// Defines items spawned by the effect, and where and how they're spawned. + /// class ItemSpawnInfo { public enum SpawnPositionType { + /// + /// The position of the StatusEffect's target. + /// This, - //the inventory of the StatusEffect's target entity + /// + /// The inventory of the StatusEffect's target. + /// ThisInventory, - //the same inventory the StatusEffect's target entity is in (only valid if the target is an Item) + /// + /// The same inventory the StatusEffect's target entity is in. Only valid if the target is an Item. + /// SameInventory, - //the inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory) + /// + /// The inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory) + /// ContainedInventory } public enum SpawnRotationType { + /// + /// Fixed rotation specified using the Rotation attribute. + /// Fixed, + /// + /// The rotation of the entity executing the StatusEffect + /// Target, + /// + /// The rotation of the limb executing the StatusEffect, or the limb the StatusEffect is targeting + /// Limb, + /// + /// The rotation of the main limb (usually torso) of the character executing the StatusEffect + /// MainLimb, + /// + /// The rotation of the collider of the character executing the StatusEffect + /// Collider, + /// + /// Random rotation between 0 and 360 degrees. + /// Random } public readonly ItemPrefab ItemPrefab; + /// + /// Where should the item spawn? + /// public readonly SpawnPositionType SpawnPosition; + + /// + /// Should the item spawn even if the container is already full? + /// public readonly bool SpawnIfInventoryFull; /// - /// Should the item spawn even if the container can't contain items of this type + /// Should the item spawn even if the container can't contain items of this type or if it's already full? /// public readonly bool SpawnIfCantBeContained; + /// + /// Impulse applied to the item when it spawns (i.e. how fast the item launched off). + /// public readonly float Impulse; public readonly float RotationRad; + /// + /// How many items to spawn. + /// public readonly int Count; + /// + /// Random offset added to the spawn position in pixels. + /// public readonly float Spread; + /// + /// What should the initial rotation of the item be? + /// public readonly SpawnRotationType RotationType; + /// + /// Amount of random variance in the initial rotation of the item (in degrees). + /// public readonly float AimSpreadRad; + /// + /// Should the item be automatically equipped when it spawns? Only valid if the item spawns in a character's inventory. + /// public readonly bool Equip; - + /// + /// Condition of the item when it spawns (1.0 = max). + /// public readonly float Condition; public ItemSpawnInfo(XElement element, string parentDebugName) @@ -208,19 +262,19 @@ namespace Barotrauma AimSpreadRad = MathHelper.ToRadians(element.GetAttributeFloat("aimspread", 0f)); Equip = element.GetAttributeBool("equip", false); - string spawnTypeStr = element.GetAttributeString("spawnposition", "This"); - if (!Enum.TryParse(spawnTypeStr, ignoreCase: true, out SpawnPosition)) - { - DebugConsole.ThrowError("Error in StatusEffect config - \"" + spawnTypeStr + "\" is not a valid spawn position."); - } - string rotationTypeStr = element.GetAttributeString("rotationtype", RotationRad != 0 ? "Fixed" : "Target"); - if (!Enum.TryParse(rotationTypeStr, ignoreCase: true, out RotationType)) - { - DebugConsole.ThrowError("Error in StatusEffect config - \"" + rotationTypeStr + "\" is not a valid rotation type."); - } + SpawnPosition = element.GetAttributeEnum("spawnposition", SpawnPositionType.This); + RotationType = element.GetAttributeEnum("rotationtype", RotationRad != 0 ? SpawnRotationType.Fixed : SpawnRotationType.Target); } } + /// + /// Can be used by to check whether some specific StatusEffect is running. + /// + /// + /// + /// An arbitrary identifier the Ability can check for. + /// + /// public class AbilityStatusEffectIdentifier : AbilityObject { public AbilityStatusEffectIdentifier(Identifier effectIdentifier) @@ -230,9 +284,18 @@ namespace Barotrauma public Identifier EffectIdentifier { get; set; } } + /// + /// Unlocks a talent, or multiple talents when the effect executes. Only valid if the target is a character or a limb. + /// public class GiveTalentInfo { + /// + /// The identifier(s) of the talents that should be unlocked. + /// public Identifier[] TalentIdentifiers; + /// + /// If true and there's multiple identifiers defined, a random one will be chosen instead of unlocking all of them. + /// public bool GiveRandom; public GiveTalentInfo(XElement element, string _) @@ -242,10 +305,22 @@ namespace Barotrauma } } + /// + /// Increases a character's skills when the effect executes. Only valid if the target is a character or a limb. + /// public class GiveSkill { + /// + /// The identifier of the skill to increase. + /// public readonly Identifier SkillIdentifier; + /// + /// How much to increase the skill. + /// public readonly float Amount; + /// + /// Should the talents that trigger when the character gains skills be triggered by the effect? + /// public readonly bool TriggerTalents; public GiveSkill(XElement element, string parentDebugName) @@ -261,52 +336,66 @@ namespace Barotrauma } } + /// + /// Defines characters spawned by the effect, and where and how they're spawned. + /// public class CharacterSpawnInfo : ISerializableEntity { public string Name => $"Character Spawn Info ({SpeciesName})"; public Dictionary SerializableProperties { get; set; } - [Serialize(false, IsPropertySaveable.No)] - public bool TransferBuffs { get; private set; } - - [Serialize(false, IsPropertySaveable.No)] - public bool TransferAfflictions { get; private set; } - - [Serialize(false, IsPropertySaveable.No)] - public bool TransferInventory { get; private set; } - - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "The species name (identifier) of the character to spawn.")] public Identifier SpeciesName { get; private set; } - [Serialize(1, IsPropertySaveable.No)] + [Serialize(1, IsPropertySaveable.No, description: "How many characters to spawn.")] public int Count { get; private set; } - /// - /// The maximum amount of creatures of the same species in the same team that are allowed to be spawned via this status effect. - /// Also the creatures spawned by other means are counted in the check. - /// - [Serialize(0, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "Should the buffs of the character executing the effect be transferred to the spawned character?"+ + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] + public bool TransferBuffs { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description: + "Should the afflictions of the character executing the effect be transferred to the spawned character?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] + public bool TransferAfflictions { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description: + "Should the the items from the character executing the effect be transferred to the spawned character?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] + public bool TransferInventory { get; private set; } + + [Serialize(0, IsPropertySaveable.No, description: + "The maximum number of creatures of the given species and team that can exist in the current level before this status effect stops spawning any more.")] public int TotalMaxCount { get; private set; } - [Serialize(0, IsPropertySaveable.No)] + [Serialize(0, IsPropertySaveable.No, description: "Amount of stun to apply on the spawned character.")] public int Stun { get; private set; } - [Serialize("", IsPropertySaveable.No)] + [Serialize("", IsPropertySaveable.No, description: "An affliction to apply on the spawned character.")] public Identifier AfflictionOnSpawn { get; private set; } - [Serialize(1, IsPropertySaveable.No)] + [Serialize(1, IsPropertySaveable.No, description: + $"The strength of the affliction applied on the spawned character. Only relevant if {nameof(AfflictionOnSpawn)} is defined.")] public int AfflictionStrength { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "Should the player controlling the character that executes the effect gain control of the spawned character?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] public bool TransferControl { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, description: + "Should the character that executes the effect be removed when the effect executes?" + + " Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")] public bool RemovePreviousCharacter { get; private set; } - [Serialize(0f, IsPropertySaveable.No)] + [Serialize(0f, IsPropertySaveable.No, description: "Amount of random spread to add to the spawn position. " + + "Can be used to prevent all the characters from spawning at the exact same position if the effect spawns multiple ones.")] public float Spread { get; private set; } - [Serialize("0,0", IsPropertySaveable.No)] + [Serialize("0,0", IsPropertySaveable.No, description: + "Offset added to the spawn position. " + + "Can be used to for example spawn a character a bit up from the center of an item executing the effect.")] public Vector2 Offset { get; private set; } public CharacterSpawnInfo(XElement element, string parentDebugName) @@ -319,43 +408,142 @@ namespace Barotrauma } } + /// + /// Can be used to trigger a behavior change of some kind on an AI character. Only applicable for enemy characters, not humans. + /// + public class AITrigger : ISerializableEntity + { + public string Name => "ai trigger"; + + public Dictionary SerializableProperties { get; set; } + + [Serialize(AIState.Idle, IsPropertySaveable.No, description: "The AI state the character should switch to.")] + public AIState State { get; private set; } + + [Serialize(0f, IsPropertySaveable.No, description: "How long should the character stay in the specified state? If 0, the effect is permanent (unless overridden by another AITrigger).")] + public float Duration { get; private set; } + + [Serialize(1f, IsPropertySaveable.No, description: "How likely is the AI to change the state when this effect executes? 1 = always, 0.5 = 50% chance, 0 = never.")] + public float Probability { get; private set; } + + [Serialize(0f, IsPropertySaveable.No, description: + "How much damage the character must receive for this AITrigger to become active? " + + "Checks the amount of damage the latest attack did to the character.")] + public float MinDamage { get; private set; } + + [Serialize(true, IsPropertySaveable.No, description: "Can this AITrigger override other active AITriggers?")] + public bool AllowToOverride { get; private set; } + + [Serialize(true, IsPropertySaveable.No, description: "Can this AITrigger be overridden by other AITriggers?")] + public bool AllowToBeOverridden { get; private set; } + + public bool IsTriggered { get; private set; } + + public float Timer { get; private set; } + + public bool IsActive { get; private set; } + + public bool IsPermanent { get; private set; } + + public void Launch() + { + IsTriggered = true; + IsActive = true; + IsPermanent = Duration <= 0; + if (!IsPermanent) + { + Timer = Duration; + } + } + + public void Reset() + { + IsTriggered = false; + IsActive = false; + Timer = 0; + } + + public void UpdateTimer(float deltaTime) + { + if (IsPermanent) { return; } + Timer -= deltaTime; + if (Timer < 0) + { + Timer = 0; + IsActive = false; + } + } + + public AITrigger(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + } + + + /// + /// What should this status effect be applied on? + /// private readonly TargetType targetTypes; /// - /// Index of the slot the target must be in when targeting a Contained item + /// Index of the slot the target must be in. Only valid when targeting a Contained item. /// public int TargetSlot = -1; - private readonly List requiredItems; + private readonly List requiredItems = new List(); - public readonly Identifier[] propertyNames; - public readonly object[] propertyEffects; + public readonly ImmutableArray<(Identifier propertyName, object value)> PropertyEffects; - private readonly PropertyConditional.Comparison conditionalComparison = PropertyConditional.Comparison.Or; + private readonly PropertyConditional.LogicalOperatorType conditionalLogicalOperator = PropertyConditional.LogicalOperatorType.Or; private readonly List propertyConditionals; public bool HasConditions => propertyConditionals != null && propertyConditionals.Any(); + /// + /// If set to true, the effect will set the properties of the target to the given values, instead of incrementing them by the given value. + /// private readonly bool setValue; + /// + /// If set to true, the values will not be multiplied by the elapsed time. + /// In other words, the values are treated as an increase per frame, as opposed to an increase per second. + /// Useful for effects that are intended to just run for one frame (e.g. firing a gun, an explosion). + /// private readonly bool disableDeltaTime; + /// + /// Can be used in conditionals to check if a StatusEffect with a specific tag is currently running. Only relevant for effects with a non-zero duration. + /// private readonly HashSet tags; - private readonly float duration; + /// + /// How long _can_ the event run (in seconds). The difference to is that + /// lifetime doesn't force the effect to run for the given amount of time, only restricts how + /// long it can run in total. For example, you could have an effect that makes a projectile + /// emit particles for 1 second when it's active, and not do anything after that. + /// private readonly float lifeTime; private float lifeTimer; public Dictionary intervalTimers = new Dictionary(); + /// + /// Makes the effect only execute once. After it has executed, it'll never execute again (during the same round). + /// + private readonly bool oneShot; + public static readonly List DurationList = new List(); /// - /// Always do the conditional checks for the duration/delay. If false, only check conditional on apply. + /// Only applicable for StatusEffects with a duration or delay. Should the conditional checks only be done when the effect triggers, + /// or for the whole duration it executes / when the delay runs out and the effect executes? In other words, if false, the conditionals + /// are only checked once when the effect triggers, but after that it can keep running for the whole duration, or is + /// guaranteed to execute after the delay. /// public readonly bool CheckConditionalAlways; /// - /// Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s)s if the effect is already being applied? + /// Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s) if the effect is already being applied? /// public readonly bool Stackable = true; @@ -367,6 +555,9 @@ namespace Barotrauma public readonly float Interval; #if CLIENT + /// + /// Should the sound(s) configured in the effect be played if the required items aren't found? + /// private readonly bool playSoundOnRequiredItemFailure = false; #endif @@ -377,40 +568,79 @@ namespace Barotrauma public readonly ActionType type = ActionType.OnActive; - public readonly List Explosions; + public readonly List Explosions = new List(); - private readonly List spawnItems; + private readonly List spawnItems = new List(); + + /// + /// If enabled, one of the items this effect is configured to spawn is selected randomly, as opposed to spawning all of them. + /// private readonly bool spawnItemRandomly; - private readonly List spawnCharacters; + private readonly List spawnCharacters = new List(); - public readonly List giveTalentInfos; + public readonly List giveTalentInfos = new List(); - private readonly List aiTriggers; + private readonly List aiTriggers = new List(); - private readonly List triggeredEvents; - private readonly Identifier triggeredEventTargetTag = "statuseffecttarget".ToIdentifier(), - triggeredEventEntityTag = "statuseffectentity".ToIdentifier(); + private readonly List triggeredEvents = new List(); + + /// + /// If the effect triggers a scripted event, the target of this effect is added as a target for the event using the specified tag. + /// For example, an item could have an effect that executes when used on some character, and triggers an event that makes said character say something. + /// + private readonly Identifier triggeredEventTargetTag; + + /// + /// If the effect triggers a scripted event, the entity executing this effect is added as a target for the event using the specified tag. + /// For example, a character could have an effect that executes when the character takes damage, and triggers an event that makes said character say something. + /// + private readonly Identifier triggeredEventEntityTag; + + /// + /// If the effect triggers a scripted event, the user of the StatusEffect (= the character who caused it to happen, e.g. a character who used an item) is added as a target for the event using the specified tag. + /// For example, a gun could have an effect that executes when a character uses it, and triggers an event that makes said character say something. + /// + private readonly Identifier triggeredEventUserTag; private Character user; public readonly float FireSize; + /// + /// Which types of limbs this effect can target? Only valid when targeting characters or limbs. + /// public readonly LimbType[] targetLimbs; + /// + /// The probability of severing a limb damaged by this status effect. Only valid when targeting characters or limbs. + /// public readonly float SeverLimbsProbability; public PhysicsBody sourceBody; + /// + /// If enabled, this effect can only execute inside a hull. + /// public readonly bool OnlyInside; + /// + /// If enabled, this effect can only execute outside hulls. + /// public readonly bool OnlyOutside; - // Currently only used for OnDamaged. TODO: is there a better, more generic way to do this? - public readonly bool OnlyPlayerTriggered; /// - /// Can the StatusEffect be applied when the item applying it is broken + /// If enabled, the effect only executes when the entity receives damage from a player character + /// (a character controlled by a human player). Only valid for characters, and effects of the type . + /// + public readonly bool OnlyWhenDamagedByPlayer; + + /// + /// Can the StatusEffect be applied when the item applying it is broken? /// public readonly bool AllowWhenBroken = false; + /// + /// Identifier(s), tag(s) or species name(s) of the entity the effect can target. Null if there's no identifiers. + /// public readonly ImmutableHashSet TargetIdentifiers; /// @@ -424,8 +654,13 @@ namespace Barotrauma { get; private set; - } + } = new List(); + /// + /// Should the affliction strength be directly proportional to the maximum vitality of the character? + /// In other words, when enabled, the strength of the affliction(s) caused by this effect is higher on higher-vitality characters. + /// Can be used to make characters take the same relative amount of damage regardless of their maximum vitality. + /// private readonly bool? multiplyAfflictionsByMaxVitality; public IEnumerable SpawnCharacters @@ -433,21 +668,33 @@ namespace Barotrauma get { return spawnCharacters; } } - public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction; + public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction = new List<(Identifier affliction, float amount)>(); - private readonly List talentTriggers; - private readonly List giveExperiences; - private readonly List giveSkills; + private readonly List talentTriggers = new List(); + private readonly List giveExperiences = new List(); + private readonly List giveSkills = new List(); - public float Duration => duration; + /// + /// How long the effect runs (in seconds). Note that if is true, + /// there can be multiple instances of the effect running at a time. + /// In other words, if the effect has a duration and executes every frame, you probably want + /// to make it non-stackable or it'll lead to a large number of overlapping effects running at the same time. + /// + public readonly float Duration; - //only applicable if targeting NearbyCharacters or NearbyItems + /// + /// How close to the entity executing the effect the targets must be. Only applicable if targeting NearbyCharacters or NearbyItems. + /// public float Range { get; private set; } + /// + /// An offset added to the position of the effect is executed at. Only relevant if the effect does something where position matters, + /// for example emitting particles or explosions, spawning something or playing sounds. + /// public Vector2 Offset { get; private set; } public string Tags @@ -467,6 +714,8 @@ namespace Barotrauma } } + public bool Disabled { get; private set; } + public static StatusEffect Load(ContentXElement element, string parentDebugName) { if (element.GetAttribute("delay") != null || element.GetAttribute("delaytype") != null) @@ -479,34 +728,21 @@ namespace Barotrauma protected StatusEffect(ContentXElement element, string parentDebugName) { - requiredItems = new List(); - spawnItems = new List(); - spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); - spawnCharacters = new List(); - giveTalentInfos = new List(); - aiTriggers = new List(); - Afflictions = new List(); - Explosions = new List(); - triggeredEvents = new List(); - ReduceAffliction = new List<(Identifier affliction, float amount)>(); - talentTriggers = new List(); - giveExperiences = new List(); - giveSkills = new List(); - var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality)); - if (multiplyAfflictionsElement != null) - { - multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false); - } - tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); OnlyInside = element.GetAttributeBool("onlyinside", false); OnlyOutside = element.GetAttributeBool("onlyoutside", false); - OnlyPlayerTriggered = element.GetAttributeBool("onlyplayertriggered", false); + OnlyWhenDamagedByPlayer = element.GetAttributeBool("onlyplayertriggered", element.GetAttributeBool("onlywhendamagedbyplayer", false)); AllowWhenBroken = element.GetAttributeBool("allowwhenbroken", false); - TargetSlot = element.GetAttributeInt("targetslot", -1); - Interval = element.GetAttributeFloat("interval", 0.0f); + Duration = element.GetAttributeFloat("duration", 0.0f); + disableDeltaTime = element.GetAttributeBool("disabledeltatime", false); + setValue = element.GetAttributeBool("setvalue", false); + Stackable = element.GetAttributeBool("stackable", true); + lifeTime = lifeTimer = element.GetAttributeFloat("lifetime", 0.0f); + CheckConditionalAlways = element.GetAttributeBool("checkconditionalalways", false); + + TargetSlot = element.GetAttributeInt("targetslot", -1); Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); @@ -521,9 +757,7 @@ namespace Barotrauma if (targetLimbs.Count > 0) { this.targetLimbs = targetLimbs.ToArray(); } } - IEnumerable attributes = element.Attributes(); - List propertyAttributes = new List(); - propertyConditionals = new List(); + SeverLimbsProbability = MathHelper.Clamp(element.GetAttributeFloat(0.0f, "severlimbs", "severlimbsprobability"), 0.0f, 1.0f); string[] targetTypesStr = element.GetAttributeStringArray("target", null) ?? @@ -532,7 +766,7 @@ namespace Barotrauma { if (!Enum.TryParse(s, true, out TargetType targetType)) { - DebugConsole.ThrowError("Invalid target type \"" + s + "\" in StatusEffect (" + parentDebugName + ")"); + DebugConsole.ThrowError($"Invalid target type \"{s}\" in StatusEffect ({parentDebugName})"); } else { @@ -540,37 +774,55 @@ namespace Barotrauma } } - foreach (XAttribute attribute in attributes) + var targetIdentifiers = element.GetAttributeIdentifierArray(Array.Empty(), "targetnames", "targets", "targetidentifiers", "targettags"); + if (targetIdentifiers.Any()) + { + TargetIdentifiers = targetIdentifiers.ToImmutableHashSet(); + } + + triggeredEventTargetTag = element.GetAttributeIdentifier("eventtargettag", Identifier.Empty); + triggeredEventEntityTag = element.GetAttributeIdentifier("evententitytag", Identifier.Empty); + triggeredEventUserTag = element.GetAttributeIdentifier("eventusertag", Identifier.Empty); + + spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); + + var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality)); + if (multiplyAfflictionsElement != null) + { + multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false); + } + +#if CLIENT + playSoundOnRequiredItemFailure = element.GetAttributeBool("playsoundonrequireditemfailure", false); +#endif + + List propertyAttributes = new List(); + propertyConditionals = new List(); + foreach (XAttribute attribute in element.Attributes()) { switch (attribute.Name.ToString().ToLowerInvariant()) { case "type": if (!Enum.TryParse(attribute.Value, true, out type)) { - DebugConsole.ThrowError("Invalid action type \"" + attribute.Value + "\" in StatusEffect (" + parentDebugName + ")"); + DebugConsole.ThrowError($"Invalid action type \"{attribute.Value}\" in StatusEffect ({parentDebugName})"); } break; case "targettype": case "target": - break; - case "disabledeltatime": - disableDeltaTime = attribute.GetAttributeBool(false); - break; - case "setvalue": - setValue = attribute.GetAttributeBool(false); - break; - case "severlimbs": - case "severlimbsprobability": - SeverLimbsProbability = MathHelper.Clamp(attribute.GetAttributeFloat(0.0f), 0.0f, 1.0f); - break; case "targetnames": case "targets": case "targetidentifiers": case "targettags": - TargetIdentifiers = attribute.Value.Split(',').ToIdentifiers().ToImmutableHashSet(); + case "severlimbs": + case "targetlimb": + case "delay": + case "interval": + //aliases for fields we're already reading above, and which shouldn't be interpreted as values we're trying to set break; case "allowedafflictions": case "requiredafflictions": + //backwards compatibility, should be defined as child elements instead string[] types = attribute.Value.Split(','); requiredAfflictions ??= new HashSet<(Identifier, float)>(); for (int i = 0; i < types.Length; i++) @@ -578,43 +830,15 @@ namespace Barotrauma requiredAfflictions.Add((types[i].Trim().ToIdentifier(), 0.0f)); } break; - case "duration": - duration = attribute.GetAttributeFloat(0.0f); - break; - case "stackable": - Stackable = attribute.GetAttributeBool(true); - break; - case "lifetime": - lifeTime = attribute.GetAttributeFloat(0); - lifeTimer = lifeTime; - break; - case "eventtargettag": - triggeredEventTargetTag = attribute.Value.ToIdentifier(); - break; - case "evententitytag": - triggeredEventEntityTag = attribute.Value.ToIdentifier(); - break; - case "checkconditionalalways": - CheckConditionalAlways = attribute.GetAttributeBool(false); - break; case "conditionalcomparison": case "comparison": - if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalComparison)) + if (!Enum.TryParse(attribute.Value, ignoreCase: true, out conditionalLogicalOperator)) { - DebugConsole.ThrowError("Invalid conditional comparison type \"" + attribute.Value + "\" in StatusEffect (" + parentDebugName + ")"); + DebugConsole.ThrowError($"Invalid conditional comparison type \"{attribute.Value}\" in StatusEffect ({parentDebugName})"); } break; -#if CLIENT - case "playsoundonrequireditemfailure": - playSoundOnRequiredItemFailure = attribute.GetAttributeBool(false); - break; -#endif case "sound": - DebugConsole.ThrowError("Error in StatusEffect " + element.Parent.Name.ToString() + - " - sounds should be defined as child elements of the StatusEffect, not as attributes."); - break; - case "delay": - case "interval": + DebugConsole.ThrowError($"Error in StatusEffect ({parentDebugName}): sounds should be defined as child elements of the StatusEffect, not as attributes."); break; case "range": if (!HasTargetType(TargetType.NearbyCharacters) && !HasTargetType(TargetType.NearbyItems)) @@ -622,32 +846,30 @@ namespace Barotrauma propertyAttributes.Add(attribute); } break; + case "tags": + if (Duration <= 0.0f || setValue) + { + //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: + //if the status effect doesn't have a duration, assume tags mean an item's tags, not this status effect's tags + propertyAttributes.Add(attribute); + } + break; + case "oneshot": + oneShot = attribute.GetAttributeBool(false); + break; default: + if (FieldNames.Contains(attribute.Name.ToIdentifier())) { continue; } propertyAttributes.Add(attribute); break; } } - if (duration > 0.0f && !setValue) - { - //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: - //if the status effect has a duration, assume tags mean this status effect's tags and leave item tags untouched. - propertyAttributes.RemoveAll(a => a.Name.ToString().Equals("tags", StringComparison.OrdinalIgnoreCase)); - } - - int count = propertyAttributes.Count; - - propertyNames = new Identifier[count]; - propertyEffects = new object[count]; - - int n = 0; + List<(Identifier propertyName, object value)> propertyEffects = new List<(Identifier propertyName, object value)>(); foreach (XAttribute attribute in propertyAttributes) { - - propertyNames[n] = attribute.NameAsIdentifier(); - propertyEffects[n] = XMLExtensions.GetAttributeObject(attribute); - n++; + propertyEffects.Add((attribute.NameAsIdentifier(), XMLExtensions.GetAttributeObject(attribute))); } + PropertyEffects = propertyEffects.ToImmutableArray(); foreach (var subElement in element.Elements()) { @@ -704,13 +926,7 @@ namespace Barotrauma } break; case "conditional": - foreach (XAttribute attribute in subElement.Attributes()) - { - if (PropertyConditional.IsValid(attribute)) - { - propertyConditionals.Add(new PropertyConditional(attribute)); - } - } + propertyConditionals.AddRange(PropertyConditional.FromXElement(subElement)); break; case "affliction": AfflictionPrefab afflictionPrefab; @@ -817,13 +1033,11 @@ namespace Barotrauma public bool ReducesItemCondition() { - for (int i = 0; i < propertyNames.Length; i++) + foreach (var (propertyName, value) in PropertyEffects) { - if (propertyNames[i] != "condition") { continue; } - object propertyEffect = propertyEffects[i]; - if (propertyEffect.GetType() == typeof(float)) + if (propertyName == "condition" && value.GetType() == typeof(float)) { - return (float)propertyEffect < 0.0f || (setValue && (float)propertyEffect <= 0.0f); + return (float)value < 0.0f || (setValue && (float)value <= 0.0f); } } return false; @@ -831,13 +1045,11 @@ namespace Barotrauma public bool IncreasesItemCondition() { - for (int i = 0; i < propertyNames.Length; i++) + foreach (var (propertyName, value) in PropertyEffects) { - if (propertyNames[i] != "condition") { continue; } - object propertyEffect = propertyEffects[i]; - if (propertyEffect.GetType() == typeof(float)) + if (propertyName == "condition" && value.GetType() == typeof(float)) { - return (float)propertyEffect > 0.0f || (setValue && (float)propertyEffect > 0.0f); + return (float)value > 0.0f || (setValue && (float)value > 0.0f); } } return false; @@ -851,7 +1063,7 @@ namespace Barotrauma } else { - return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.MatchesTagCondition(t))); + return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.TargetTagMatchesTagCondition(t))); } } @@ -899,7 +1111,8 @@ namespace Barotrauma if (HasTargetType(TargetType.NearbyItems)) { //optimization for powered components that can be easily fetched from Powered.PoweredList - if (TargetIdentifiers.Count == 1 && + if (TargetIdentifiers != null && + TargetIdentifiers.Count == 1 && (TargetIdentifiers.Contains("powered") || TargetIdentifiers.Contains("junctionbox") || TargetIdentifiers.Contains("relaycomponent"))) { foreach (Powered powered in Powered.PoweredList) @@ -946,97 +1159,56 @@ namespace Barotrauma { if (conditionals.Count == 0) { return true; } if (targets.Count == 0 && requiredItems.Count > 0 && requiredItems.All(ri => ri.MatchOnEmpty)) { return true; } - switch (conditionalComparison) + + bool shortCircuitValue = conditionalLogicalOperator switch { - case PropertyConditional.Comparison.Or: - for (int i = 0; i < conditionals.Count; i++) + PropertyConditional.LogicalOperatorType.Or => true, + PropertyConditional.LogicalOperatorType.And => false, + _ => throw new NotImplementedException() + }; + + for (int i = 0; i < conditionals.Count; i++) + { + var pc = conditionals[i]; + if (!pc.TargetContainer || targetingContainer) + { + if (AnyTargetMatches(targets, pc.TargetItemComponent, pc) == shortCircuitValue) { return shortCircuitValue; } + continue; + } + + var target = FindTargetItemOrComponent(targets); + var targetItem = target as Item ?? (target as ItemComponent)?.Item; + if (targetItem?.ParentInventory == null) + { + //if we're checking for inequality, not being inside a valid container counts as success + //(not inside a container = the container doesn't have a specific tag/value) + bool comparisonIsNeq = pc.ComparisonOperator == PropertyConditional.ComparisonOperatorType.NotEquals; + if (comparisonIsNeq == shortCircuitValue) { - var pc = conditionals[i]; - if (pc.TargetContainer && !targetingContainer) - { - var target = FindTargetItemOrComponent(targets); - var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) - { - //if we're checking for inequality, not being inside a valid container counts as success - //(not inside a container = the container doesn't have a specific tag/value) - if (pc.Operator == PropertyConditional.OperatorType.NotEquals) - { - return true; - } - continue; - } - var owner = targetItem.ParentInventory.Owner; - if (pc.TargetGrandParent && owner is Item ownerItem) - { - owner = ownerItem.ParentInventory?.Owner; - } - if (owner is Item container) - { - if (pc.Type == PropertyConditional.ConditionType.HasTag) - { - //if we're checking for tags, just check the Item object, not the ItemComponents - if (pc.Matches(container)) { return true; } - } - else - { - if (AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return true; } - } - } - if (owner is Character character && pc.Matches(character)) { return true; } - } - else - { - if (AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return true; } - } + return shortCircuitValue; } - return false; - case PropertyConditional.Comparison.And: - for (int i = 0; i < conditionals.Count; i++) + continue; + } + var owner = targetItem.ParentInventory.Owner; + if (pc.TargetGrandParent && owner is Item ownerItem) + { + owner = ownerItem.ParentInventory?.Owner; + } + if (owner is Item container) + { + if (pc.Type == PropertyConditional.ConditionType.HasTag) { - var pc = conditionals[i]; - if (pc.TargetContainer && !targetingContainer) - { - var target = FindTargetItemOrComponent(targets); - var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) - { - //if we're checking for inequality, not being inside a valid container counts as success - //(not inside a container = the container doesn't have a specific tag/value) - if (pc.Operator == PropertyConditional.OperatorType.NotEquals) - { - continue; - } - return false; - } - var owner = targetItem.ParentInventory.Owner; - if (pc.TargetGrandParent && owner is Item ownerItem) - { - owner = ownerItem.ParentInventory?.Owner; - } - if (owner is Item container) - { - if (pc.Type == PropertyConditional.ConditionType.HasTag) - { - //if we're checking for tags, just check the Item object, not the ItemComponents - if (!pc.Matches(container)) { return false; } - } - else - { - if (!AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return false; } - } - } - if (owner is Character character && !pc.Matches(character)) { return false; } - } - else - { - if (!AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return false; } - } + //if we're checking for tags, just check the Item object, not the ItemComponents + if (pc.Matches(container) == shortCircuitValue) { return shortCircuitValue; } } - return true; - default: - throw new NotImplementedException(); + else + { + if (AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponent, pc) == shortCircuitValue) { return shortCircuitValue; } + } + } + if (owner is Character character && pc.Matches(character) == shortCircuitValue) { return shortCircuitValue; } } + return !shortCircuitValue; static bool AnyTargetMatches(IReadOnlyList targets, string targetItemComponentName, PropertyConditional conditional) { @@ -1148,17 +1320,18 @@ namespace Barotrauma public virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null) { + if (Disabled) { return; } if (this.type != type || !HasRequiredItems(entity)) { return; } if (!IsValidTarget(target)) { return; } - if (duration > 0.0f && !Stackable) + if (Duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target); if (existingEffect != null) { - existingEffect.Reset(Math.Max(existingEffect.Timer, duration), user); + existingEffect.Reset(Math.Max(existingEffect.Timer, Duration), user); return; } } @@ -1172,6 +1345,7 @@ namespace Barotrauma protected readonly List currentTargets = new List(); public virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { + if (Disabled) { return; } if (this.type != type) { return; } if (ShouldWaitForInterval(entity, deltaTime)) { return; } @@ -1196,13 +1370,13 @@ namespace Barotrauma return; } - if (duration > 0.0f && !Stackable) + if (Duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.SequenceEqual(currentTargets)); if (existingEffect != null) { - existingEffect?.Reset(Math.Max(existingEffect.Timer, duration), user); + existingEffect?.Reset(Math.Max(existingEffect.Timer, Duration), user); return; } } @@ -1271,6 +1445,7 @@ namespace Barotrauma protected void Apply(float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { + if (Disabled) { return; } if (lifeTime > 0) { lifeTimer -= deltaTime; @@ -1395,16 +1570,16 @@ namespace Barotrauma } } - if (duration > 0.0f) + if (Duration > 0.0f) { - DurationList.Add(new DurationListElement(this, entity, targets, duration, user)); + DurationList.Add(new DurationListElement(this, entity, targets, Duration, user)); } else { for (int i = 0; i < targets.Count; i++) { var target = targets[i]; - if (target == null) { continue; } + if (target?.SerializableProperties == null) { continue; } if (target is Entity targetEntity) { if (targetEntity.Removed) { continue; } @@ -1414,14 +1589,13 @@ namespace Barotrauma if (limb.Removed) { continue; } position = limb.WorldPosition + Offset; } - - for (int j = 0; j < propertyNames.Length; j++) + foreach (var (propertyName, value) in PropertyEffects) { - if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[j], out SerializableProperty property)) + if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } - ApplyToProperty(target, property, j, deltaTime); + ApplyToProperty(target, property, value, deltaTime); } } } @@ -1437,11 +1611,10 @@ namespace Barotrauma { var target = targets[i]; //if the effect has a duration, these will be done in the UpdateAll method - if (duration > 0) { break; } + if (Duration > 0) { break; } if (target == null) { continue; } foreach (Affliction affliction in Afflictions) { - if (Rand.Value(Rand.RandSync.Unsynced) > affliction.Probability) { continue; } Affliction newAffliction = affliction; if (target is Character character) { @@ -1506,7 +1679,7 @@ namespace Barotrauma { targetCharacter.TryAdjustHealerSkill(user, healthChange); #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, healthChange, 0.0f); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, -healthChange, 0.0f); #endif } } @@ -1635,18 +1808,20 @@ namespace Barotrauma { if (!triggeredEventTargetTag.IsEmpty) { - List eventTargets = targets.Where(t => t is Entity).Cast().ToList(); - - if (eventTargets.Count > 0) + IEnumerable eventTargets = targets.Where(t => t is Entity); + if (eventTargets.Any()) { - scriptedEvent.Targets.Add(triggeredEventTargetTag, eventTargets); + scriptedEvent.Targets.Add(triggeredEventTargetTag, eventTargets.Cast().ToList()); } } - if (!triggeredEventEntityTag.IsEmpty && entity != null) { scriptedEvent.Targets.Add(triggeredEventEntityTag, new List { entity }); } + if (!triggeredEventUserTag.IsEmpty && user != null) + { + scriptedEvent.Targets.Add(triggeredEventUserTag, new List { user }); + } } } } @@ -1745,7 +1920,10 @@ namespace Barotrauma if (spawnItemRandomly) { - SpawnItem(spawnItems.GetRandomUnsynced()); + if (spawnItems.Count > 0) + { + SpawnItem(spawnItems.GetRandomUnsynced()); + } } else { @@ -1886,9 +2064,15 @@ namespace Barotrauma } else if (entity is Item item) { - var itemContainer = item.GetComponent(); - inventory = itemContainer?.Inventory; - if (!chosenItemSpawnInfo.SpawnIfCantBeContained && !itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + foreach (ItemContainer itemContainer in item.GetComponents()) + { + if (itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + { + inventory = itemContainer?.Inventory; + break; + } + } + if (!chosenItemSpawnInfo.SpawnIfCantBeContained && inventory == null) { return; } @@ -1971,6 +2155,10 @@ namespace Barotrauma ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); + if (oneShot) + { + Disabled = true; + } if (Interval > 0.0f && entity != null) { intervalTimers[entity] = Interval; @@ -1992,16 +2180,15 @@ namespace Barotrauma partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList targets, Hull currentHull, Vector2 worldPosition, bool playSound); - private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, int effectIndex, float deltaTime) + private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) { if (disableDeltaTime || setValue) { deltaTime = 1.0f; } - object propertyEffect = propertyEffects[effectIndex]; - if (propertyEffect is int || propertyEffect is float) + if (value is int || value is float) { float propertyValueF = property.GetFloatValue(target); if (property.PropertyType == typeof(float)) { - float floatValue = propertyEffect is float single ? single : (int)propertyEffect; + float floatValue = value is float single ? single : (int)value; floatValue *= deltaTime; if (!setValue) { @@ -2012,7 +2199,7 @@ namespace Barotrauma } else if (property.PropertyType == typeof(int)) { - int intValue = (int)(propertyEffect is float single ? single * deltaTime : (int)propertyEffect * deltaTime); + int intValue = (int)(value is float single ? single * deltaTime : (int)value * deltaTime); if (!setValue) { intValue += (int)propertyValueF; @@ -2021,12 +2208,12 @@ namespace Barotrauma return; } } - else if (propertyEffect is bool propertyValueBool) + else if (value is bool propertyValueBool) { property.TrySetValue(target, propertyValueBool); return; } - property.TrySetValue(target, propertyEffect); + property.TrySetValue(target, value); } public static void UpdateAll(float deltaTime) @@ -2055,15 +2242,16 @@ namespace Barotrauma foreach (ISerializableEntity target in element.Targets) { - for (int n = 0; n < element.Parent.propertyNames.Length; n++) + if (target?.SerializableProperties != null) { - if (target == null || - target.SerializableProperties == null || - !target.SerializableProperties.TryGetValue(element.Parent.propertyNames[n], out SerializableProperty property)) + foreach (var (propertyName, value) in element.Parent.PropertyEffects) { - continue; + if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) + { + continue; + } + element.Parent.ApplyToProperty(target, property, value, CoroutineManager.DeltaTime); } - element.Parent.ApplyToProperty(target, property, n, CoroutineManager.DeltaTime); } foreach (Affliction affliction in element.Parent.Afflictions) @@ -2120,7 +2308,7 @@ namespace Barotrauma { targetCharacter.TryAdjustHealerSkill(element.User, healthChange); #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, healthChange, 0.0f); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, -healthChange, 0.0f); #endif } } @@ -2144,18 +2332,23 @@ namespace Barotrauma private float GetAfflictionMultiplier(Entity entity, Character targetCharacter, float deltaTime) { - float multiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; - if (entity is Item sourceItem && sourceItem.HasTag("medical")) + float afflictionMultiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; + if (entity is Item sourceItem) { - multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); - - if (user is not null) + if (sourceItem.HasTag("medical")) { - multiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); + afflictionMultiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); + if (user is not null) + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); + } + } + else if (sourceItem.HasTag(AfflictionPrefab.PoisonType) && user is not null) + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); } } - - return multiplier * AfflictionMultiplier; + return afflictionMultiplier * AfflictionMultiplier; } private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool? multiplyByMaxVitality) @@ -2165,19 +2358,17 @@ namespace Barotrauma { afflictionMultiplier *= targetCharacter.MaxVitality / 100f; } - if (user is not null) { if (affliction.Prefab.IsBuff) { - afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemDurationMultiplier); + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.BuffItemApplyingMultiplier); } - else if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis") + else if (affliction.Prefab.Identifier == "organdamage" && targetCharacter.CharacterHealth.GetActiveAfflictionTags().Any(t => t == "poisoned")) { afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); } } - if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) { return affliction.CreateMultiplied(afflictionMultiplier, affliction); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index 0cd9799f9..08658ad68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -26,7 +26,7 @@ namespace Barotrauma.Steam { "language", 5 } }; - public static bool IsInitialized { get; private set; } + public static bool IsInitialized => IsInitializedProjectSpecific; private static readonly List popularTags = new List(); public static IEnumerable PopularTags @@ -189,7 +189,6 @@ namespace Barotrauma.Steam if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.Shutdown(); } if (Steamworks.SteamServer.IsValid) { Steamworks.SteamServer.Shutdown(); } - IsInitialized = false; } public static IEnumerable ParseWorkshopIds(string workshopIdData) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 78cd9451d..5b6f6f15f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -194,7 +194,7 @@ namespace Barotrauma.Steam { try { - System.IO.Directory.Delete(item.Directory, recursive: true); + System.IO.Directory.Delete(item.Directory ?? "", recursive: true); } catch { @@ -312,7 +312,7 @@ namespace Barotrauma.Steam public static bool IsItemDirectoryUpToDate(in Steamworks.Ugc.Item item) { - string itemDirectory = item.Directory; + string itemDirectory = item.Directory ?? ""; return Directory.Exists(itemDirectory) && File.GetLastWriteTime(itemDirectory).ToUniversalTime() >= item.LatestUpdateTime; } @@ -403,7 +403,7 @@ namespace Barotrauma.Steam var ids = items.Select(it => it.Id.Value).ToHashSet(); var toUninstall = ContentPackageManager.WorkshopPackages .Where(pkg - => !pkg.UgcId.TryUnwrap(out SteamWorkshopId workshopId) + => !pkg.UgcId.TryUnwrap(out var workshopId) || !ids.Contains(workshopId.Value)) .ToArray(); if (toUninstall.Any()) @@ -432,9 +432,9 @@ namespace Barotrauma.Steam if (!(itemNullable is { } item)) { return; } await Task.Yield(); - string itemTitle = item.Title.Trim(); + string itemTitle = item.Title?.Trim() ?? ""; UInt64 itemId = item.Id; - string itemDirectory = item.Directory; + string itemDirectory = item.Directory ?? ""; DateTime updateTime = item.LatestUpdateTime; if (!CanBeInstalled(item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 09ebf269f..6fae4f6cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -13,7 +13,7 @@ namespace Barotrauma { private const float UpdateInterval = 1.0f; - private static HashSet unlockedAchievements = new HashSet(); + private static readonly HashSet unlockedAchievements = new HashSet(); public static bool CheatsEnabled = false; @@ -219,10 +219,10 @@ namespace Barotrauma UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier()); } - public static void OnCampaignMetadataSet(Identifier identifier, object value) + public static void OnCampaignMetadataSet(Identifier identifier, object value, bool unlockClients = false) { if (identifier.IsEmpty || value is null) { return; } - UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier()); + UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier(), unlockClients); } public static void OnItemRepaired(Item item, Character fixer) @@ -236,6 +236,15 @@ namespace Barotrauma UnlockAchievement(fixer, $"repair{item.Prefab.Identifier}".ToIdentifier()); } + public static void OnAfflictionReceived(Affliction affliction, Character character) + { + if (affliction.Prefab.AchievementOnReceived.IsEmpty) { return; } +#if CLIENT + if (GameMain.Client != null) { return; } +#endif + UnlockAchievement(character, affliction.Prefab.AchievementOnReceived); + } + public static void OnAfflictionRemoved(Affliction affliction, Character character) { if (affliction.Prefab.AchievementOnRemoved.IsEmpty) { return; } @@ -433,7 +442,7 @@ namespace Barotrauma var charactersInSub = Character.CharacterList.FindAll(c => !c.IsDead && c.TeamID != CharacterTeamType.FriendlyNPC && - !(c.AIController is EnemyAIController) && + c.AIController is not EnemyAIController && (c.Submarine == gameSession.Submarine || gameSession.Submarine.GetConnectedSubs().Contains(c.Submarine) || (Level.Loaded?.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost))); if (charactersInSub.Count == 1) @@ -515,7 +524,10 @@ namespace Barotrauma public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func conditions = null) { if (CheatsEnabled) { return; } - + if (Screen.Selected is { IsEditor: true }) { return; } +#if CLIENT + if (GameMain.GameSession?.GameMode is TestGameMode) { return; } +#endif #if SERVER if (unlockClients && GameMain.Server != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs index 06f36cc62..67b03d501 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs @@ -11,6 +11,7 @@ namespace Barotrauma left = l; right = r; } + // TODO: should this be && instead of ||? public override bool Loaded => left.Loaded || right.Loaded; public override void RetrieveValue() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs index 60a8364ca..4d7ca053c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs @@ -37,8 +37,9 @@ namespace Barotrauma public static float InterpolateRotation(float previous, float current) { + //use a somewhat high epsilon - very small differences aren't visible + if (MathUtils.NearlyEqual(previous, current, epsilon: 0.02f)) { return current; } float angleDiff = MathUtils.GetShortestAngle(previous, current); - return previous + angleDiff * (float)alpha; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index f7b33b9aa..9114c6f6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -43,15 +43,32 @@ namespace Barotrauma } } - public int GetBuyPrice(int level, Location? location = null) + public int GetBuyPrice(int level, Location? location = null, ImmutableHashSet? characterList = null) { - int maxLevel = Prefab.GetMaxLevelForCurrentSub(); + float price = BasePrice; - if (level > maxLevel) { maxLevel = level; } + int maxLevel = Prefab.MaxLevel; - int price = BasePrice; - price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, level / (float)maxLevel) / 100); - return location?.GetAdjustedMechanicalCost(price) ?? price; + float lerpAmount = maxLevel is 0 + ? level // avoid division by 0 + : level / (float)maxLevel; + + float priceMultiplier = MathHelper.Lerp(IncreaseLow, IncreaseHigh, lerpAmount); + price += price * (priceMultiplier / 100f); + + price = location?.GetAdjustedMechanicalCost((int)price) ?? price; + + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); + + if (characterList.Any()) + { + if (location?.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) + { + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated)); + } + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier)); + } + return (int)price; } } @@ -194,11 +211,10 @@ namespace Barotrauma _ => throw new ArgumentOutOfRangeException() }; - public bool AppliesTo(SubmarineInfo sub) + public bool AppliesTo(SubmarineClass subClass, int subTier) { if (type is MaxLevelModType.Invalid) { return false; } - int subTier = sub.Tier; if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) { int modifier = metadata.GetInt(new Identifier("tiermodifieroverride"), 0); @@ -211,9 +227,9 @@ namespace Barotrauma return subTier == tier; } - if (tierOrClass.TryGet(out SubmarineClass subClass)) + if (tierOrClass.TryGet(out SubmarineClass targetClass)) { - return sub.SubmarineClass == subClass; + return subClass == targetClass; } return false; @@ -492,15 +508,19 @@ namespace Barotrauma { int level = MaxLevel; - foreach (UpgradeMaxLevelMod mod in MaxLevelsMods) - { - if (mod.AppliesTo(info)) { level = mod.GetLevelAfter(level); } - } + int tier = info.Tier; if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) { int modifier = metadata.GetInt(new Identifier($"tiermodifiers.{Identifier}"), 0); - level += modifier; + tier += modifier; + } + + tier = Math.Clamp(tier, 1, SubmarineInfo.HighestTier); + + foreach (UpgradeMaxLevelMod mod in MaxLevelsMods) + { + if (mod.AppliesTo(info.SubmarineClass, tier)) { level = mod.GetLevelAfter(level); } } return level; @@ -518,23 +538,25 @@ namespace Barotrauma if (character is null) { return false; } if (!ResourceCosts.Any()) { return true; } - List allItems = character.Inventory.FindAllItems(recursive: true); + var allItems = CargoManager.FindAllItemsOnPlayerAndSub(character); + return ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)).All(cost => cost.Amount <= allItems.Count(cost.MatchesItem)); } + // ReSharper disable PossibleMultipleEnumeration public bool TryTakeResources(Character character, int currentLevel) { - IEnumerable costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)); + var costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)); if (!costs.Any()) { return true; } - List allItems = character.Inventory.FindAllItems(recursive: true); + var inventoryItems = CargoManager.FindAllItemsOnPlayerAndSub(character); HashSet itemsToRemove = new HashSet(); foreach (UpgradeResourceCost cost in costs) { int amountNeeded = cost.Amount; - foreach (Item item in allItems.Where(cost.MatchesItem)) + foreach (Item item in inventoryItems.Where(cost.MatchesItem)) { itemsToRemove.Add(item); amountNeeded--; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs index c1225eb7b..a7dedde89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs @@ -25,7 +25,7 @@ namespace Barotrauma if (!Done) { Mre.WaitOne(); } } } - private static List enqueuedTasks; + private static readonly List enqueuedTasks; static CrossThread() { enqueuedTasks = new List(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 8b736ad74..76671aea5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -155,7 +155,6 @@ namespace Barotrauma public static float CurveAngle(float from, float to, float step) { - from = WrapAngleTwoPi(from); to = WrapAngleTwoPi(to); @@ -189,13 +188,7 @@ namespace Barotrauma { return 0.0f; } - - while (angle < 0) - angle += MathHelper.TwoPi; - while (angle >= MathHelper.TwoPi) - angle -= MathHelper.TwoPi; - - return angle; + return PositiveModulo(angle, MathHelper.TwoPi); } /// @@ -207,13 +200,9 @@ namespace Barotrauma { return 0.0f; } - // Ensure that -pi <= angle < pi for both "from" and "to" - while (angle < -MathHelper.Pi) - angle += MathHelper.TwoPi; - while (angle >= MathHelper.Pi) - angle -= MathHelper.TwoPi; - - return angle; + float min = -MathHelper.Pi; + float diffFromMin = angle - min; + return diffFromMin - (MathF.Floor(diffFromMin / MathHelper.TwoPi) * MathHelper.TwoPi) + min; } public static float GetShortestAngle(float from, float to) @@ -342,13 +331,13 @@ namespace Barotrauma if (axisAligned1.Y < axisAligned2.Y) { - if (y < axisAligned1.Y) return false; - if (y > axisAligned2.Y) return false; + if (y < axisAligned1.Y) { return false; } + if (y > axisAligned2.Y) { return false; } } else { - if (y > axisAligned1.Y) return false; - if (y < axisAligned2.Y) return false; + if (y > axisAligned1.Y) { return false; } + if (y < axisAligned2.Y) { return false; } } intersection = new Vector2(axisAligned1.X, y); @@ -364,13 +353,13 @@ namespace Barotrauma if (axisAligned1.X < axisAligned2.X) { - if (x < axisAligned1.X) return false; - if (x > axisAligned2.X) return false; + if (x < axisAligned1.X) { return false; } + if (x > axisAligned2.X) { return false; } } else { - if (x > axisAligned1.X) return false; - if (x < axisAligned2.X) return false; + if (x > axisAligned1.X) { return false; } + if (x < axisAligned2.X) { return false; } } intersection = new Vector2(x, axisAligned1.Y); @@ -901,23 +890,30 @@ namespace Barotrauma // https://stackoverflow.com/questions/3874627/floating-point-comparison-functions-for-c-sharp public static bool NearlyEqual(float a, float b, float epsilon = 0.0001f) { - float diff = Math.Abs(a - b); if (a == b) { - // shortcut, handles infinities + //shortcut, handles infinities return true; } - else if (a == 0 || b == 0 || diff < float.Epsilon) + + if (a == 0 || b == 0) { - // a or b is zero or both are extremely close to it - // relative error is less meaningful here - return diff < epsilon; + //if a or b is zero, relative error is less meaningful + return Math.Abs(a - b) < epsilon; } - else + + float absA = Math.Abs(a); + float absB = Math.Abs(b); + float absAB = absA + absB; + if (absAB < epsilon) { - // use relative error - return diff / (Math.Abs(a) + Math.Abs(b)) < epsilon; + // a and b extremely close to zero, relative error is less meaningful + return true; } + + float diff = Math.Abs(a - b); + // use relative error + return diff / absAB < epsilon; } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs index c4ce599c0..9bc971557 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs @@ -1,9 +1,7 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; using Barotrauma.IO; +using System; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -12,52 +10,6 @@ namespace Barotrauma { public class Md5Hash { - public static class Cache - { - private const string cachePath = "Data/hashcache.txt"; - - private readonly static List<(string Path, Md5Hash Hash, DateTime DateTime)> Entries - = new List<(string Path, Md5Hash Hash, DateTime DateTime)>(); - - public static void Load() - { - if (!File.Exists(cachePath)) { return; } - var lines = File.ReadAllLines(cachePath); - if (Version.TryParse(lines[0], out var cacheVersion) && cacheVersion == GameMain.Version) - { - for (int i = 1; i < lines.Length; i++) - { - string[] split = lines[i].Split('|'); - string path = split[0].CleanUpPathCrossPlatform(); - Md5Hash hash = Md5Hash.StringAsHash(split[1]); - DateTime? dateTime = null; - if (long.TryParse(split[2], out long dateTimeUlong)) - { - dateTime = DateTime.FromBinary(dateTimeUlong); - } - - if (File.Exists(path) && dateTime.HasValue && dateTime >= File.GetLastWriteTime(path)) - { - Entries.Add((path, hash, dateTime.Value)); - } - } - } - } - - public static void Add(string path, Md5Hash hash, DateTime dateTime) - { - path = path.CleanUpPathCrossPlatform(); - Remove(path); - Entries.Add((path, hash, dateTime)); - } - - public static void Remove(string path) - { - path = path.CleanUpPathCrossPlatform(); - Entries.RemoveAll(e => e.Path == path); - } - } - public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32)); private static string RemoveWhitespace(string s) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs deleted file mode 100644 index db6e813b9..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Barotrauma -{ - public sealed class None : Option - { - private None() { } - - public static Option Create() => new None(); - - public override Option Fallback(Option fallback) => fallback; - public override T Fallback(T fallback) => fallback; - - public override bool ValueEquals(T value) => false; - - public override string ToString() - => $"None<{typeof(T).Name}>"; - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 9aff08c3f..112281e50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -1,60 +1,83 @@ #nullable enable using System; +using System.Diagnostics.CodeAnalysis; namespace Barotrauma { - /// - /// Implementation of Option type. - /// - /// - /// Credit Jlobblet - /// - public abstract class Option + public readonly struct Option where T : notnull { - public static Option Some(T value) => Some.Create(value); - public static Option None() => None.Create(); - public bool IsNone() => this is None; - public bool IsSome() => this is Some; + private readonly bool hasValue; + private readonly T? value; - public bool TryUnwrap(out T outValue) => TryUnwrap(out outValue); - - public bool TryUnwrap(out T1 outValue) where T1 : T + private Option(bool hasValue, T? value) { - switch (this) - { - case Some { Value: T1 value }: - outValue = value; - return true; - default: - outValue = default!; - return false; - } + this.hasValue = hasValue; + this.value = value; } - public Option Select(Func selector) => - this switch + public bool IsSome() => hasValue; + public bool IsNone() => !IsSome(); + + public bool TryUnwrap([NotNullWhen(returnValue: true)] out T1? outValue) where T1 : T + { + bool hasValueOfGivenType = false; + outValue = default; + + if (hasValue && value is T1 t1) { - Some { Value: var value } => Option.Some(selector.Invoke(value)), - None _ => Option.None(), - _ => throw new ArgumentOutOfRangeException() + hasValueOfGivenType = true; + outValue = t1; + } + + return hasValueOfGivenType; + } + + public bool TryUnwrap([NotNullWhen(returnValue: true)] out T? outValue) + => TryUnwrap(out outValue); + + public Option Select(Func selector) where TType : notnull + => TryUnwrap(out T? selfValue) ? Option.Some(selector(selfValue)) : Option.None; + + public Option Bind(Func> binder) where TType : notnull + => TryUnwrap(out T? selfValue) ? binder(selfValue) : Option.None; + + public T Fallback(T fallback) + => TryUnwrap(out var v) ? v : fallback; + + public Option Fallback(Option fallback) + => IsSome() ? this : fallback; + + public static Option Some(T value) + => typeof(T) switch + { + var t when t == typeof(bool) + => throw new Exception("Option type rejects booleans"), + {IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Option<>) + => throw new Exception("Option type rejects nested Option"), + {IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Nullable<>) + => throw new Exception("Option type rejects Nullable"), + _ + => new Option(hasValue: true, value: value ?? throw new Exception("Option type rejects null")) }; - public abstract Option Fallback(Option fallback); - public abstract T Fallback(T fallback); - - public abstract bool ValueEquals(T value); - public override bool Equals(object? obj) => obj switch { - Some { Value: var value } => this is Some { Value: { } selfValue } && selfValue.Equals(value), - None _ => IsNone(), - T value => this is Some { Value: { } selfValue } && selfValue.Equals(value), - _ => false + Option otherOption when otherOption.IsNone() + => IsNone(), + Option otherOption when otherOption.TryUnwrap(out var otherValue) + => ValueEquals(otherValue), + T otherValue + => ValueEquals(otherValue), + _ + => false }; + public bool ValueEquals(T otherValue) + => TryUnwrap(out T? selfValue) && selfValue.Equals(otherValue); + public override int GetHashCode() - => this is Some { Value: { } value } ? value.GetHashCode() : 0; + => TryUnwrap(out T? selfValue) ? selfValue.GetHashCode() : 0; public static bool operator ==(Option a, Option b) => a.Equals(b); @@ -62,22 +85,28 @@ namespace Barotrauma public static bool operator !=(Option a, Option b) => !(a == b); - public abstract override string ToString(); - - public static implicit operator Option(Option.UnspecifiedNone _) + public static Option None() + => default; + + public static implicit operator Option(in Option.UnspecifiedNone _) => None(); + + public override string ToString() + => TryUnwrap(out var selfValue) + ? $"Some<{typeof(T).Name}>({selfValue})" + : $"None<{typeof(T).Name}>"; } public static class Option { - public sealed class UnspecifiedNone + public static Option Some(T value) where T : notnull + => Option.Some(value); + + public static UnspecifiedNone None + => default; + + public readonly ref struct UnspecifiedNone { - private UnspecifiedNone() { } - internal static readonly UnspecifiedNone Instance = new(); } - - public static UnspecifiedNone None => UnspecifiedNone.Instance; - - public static Option Some(T value) => Option.Some(value); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs deleted file mode 100644 index 5fd1dc3b0..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace Barotrauma -{ - public sealed class Some : Option - { - public readonly T Value; - - private Some(T value) - { - if (value is null) { throw new ArgumentNullException(nameof(value), "Some cannot contain null"); } - Value = value; - } - - public static Option Create(T value) => new Some(value); - - public override Option Fallback(Option fallback) => this; - public override T Fallback(T fallback) => Value; - - public override bool ValueEquals(T value) => Value.Equals(value); - - public override string ToString() - => $"Some<{typeof(T).Name}>({Value})"; - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index 3c6b11b64..9429dcf93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -23,7 +23,7 @@ namespace Barotrauma return cachedNonAbstractTypes[assembly].Where(t => t.IsSubclassOf(typeof(T))); } - public static Option ParseDerived(TInput input) where TInput : notnull + public static Option ParseDerived(TInput input) where TInput : notnull where TBase : notnull { static Option none() => Option.None(); @@ -54,10 +54,10 @@ namespace Barotrauma f.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArgs); return constructedConverter.Invoke(null, new[] { parseFunc.Invoke(null, new object[] { input }) }) - as Option ?? none(); + as Option? ?? none(); } - return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none(); + return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()); } public static string NameWithGenerics(this Type t) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs index 9f65e0ef8..49c50dc1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs @@ -141,9 +141,8 @@ namespace Barotrauma public SerializableDateTime ToLocal() => new SerializableDateTime( - DateTime.SpecifyKind( - value - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value, - DateTimeKind.Local)); + new DateTime(ticks: value.Ticks) - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value, + SerializableTimeZone.LocalTimeZone); public long Ticks => value.Ticks; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 08254fa0d..9cbc56614 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -394,7 +394,6 @@ namespace Barotrauma return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync)); } - public static T SelectWeightedRandom(IEnumerable objects, Func weightMethod, Random random) { List objectList = objects.ToList(); @@ -409,7 +408,7 @@ namespace Barotrauma public static T SelectWeightedRandom(IList objects, IList weights, Random random) { - if (objects.Count == 0) return default(T); + if (objects.Count == 0) { return default(T); } if (objects.Count != weights.Count) { diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 8f63361d2..be94e2df8 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,418 @@ +--------------------------------------------------------------------------------------------------------- +v1.1.4.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +- Fixed Tormsdale mission not completing unless you bring the item to the sub. Now retrieving the item counts as "bringing it to the sub" in friendly outposts. + +WIP countermeasures against multiplayer exploits (feedback appreciated!): +- Fixed an exploit that allowed you to equip 2-handed weapons in only one hand. +- Added protection against deliberately lagging the server. + - Added option to enable "DoS Protection" in the server settings under "Anti-Griefing". + - Enabled by default. + - When enabled, the server will automatically kick players who are causing the server to perform poorly. + - Added a new "Max Packet Auto-Kick" in the server settings under "Anti-Griefing". + - Enabled and set to 2400 by default. + - Can be disabled by setting the limit to 1200 or below. + - When enabled, the server will automatically kick players who are sending a certain amount of network packets in a minute. +- Added "Spam Immunity" server permission. + - Gives immunity to getting kicked from DoS protection, chat spam and from sending too many packets. +- Added a rate limit to console commands in multiplayer. +- Added a rate limit to creating new characters in multiplayer. + +Unstable only: +- Fixed frequent "collection was modified" errors when loading/removing/copying structures. +- Fixed light textures being misaligned, causing many areas to look unintentionally bright. +- Fixed artifact transport case not having a background behind the fans, allowing you to see through the edges of the fans. + Added the fans to the inventory icon. +- Fixed character sinking downwards in "steps" when close to the surface of water and when the water level is going down. + +--------------------------------------------------------------------------------------------------------- +v1.1.3.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed broken "dialoglowrepcampaigninteraction" NPC line. +- Fixed messed up water effect and lighting. + +--------------------------------------------------------------------------------------------------------- +v1.1.2.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed most StatusEffects doing nothing. + +--------------------------------------------------------------------------------------------------------- +v1.1.1.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed RelatedItem not assigning the Identifiers or ExcludedIdentifiers properties, causing various kinds of crashes in various places. +- Fixed a race condition that sometimes caused "collection was modified" exceptions when rendering lights. + +--------------------------------------------------------------------------------------------------------- +v1.1.0.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Line of sight rework: +- Improved the LOS effect to get rid of weird, jagged geometry in spots with intersecting walls. +- Improved the wall damage effect to make leaks easier to see. +- Added a LOS setting that only obstructs visibility through the sub's exterior walls. + +Changes and additions: +- Oxygen generator sprite and animation fixes. +- Optimized/simplified exosuit and FB3000 status effects. +- Optimized flashlights (and other "spotlight" type of light sources): previously they'd calculate shadows all around them, even though the light is only visible in front of the light source. +- Partially multithreaded lighting: FindRaycastHits (the heaviest individual part of the lighting algorithm) is now handled in a separate thread. +- Spawn abyss and combat suits in enemy subs and wrecks instead of normal ones in later biomes. +- Outpost NPCs don't allow players to grab them for longer than 10 seconds to prevent being able to drag them around the outpost. +- Unconscious players can't end the round. +- Submarine upgrades that require materials can be purchased with materials in the sub, instead of having to carry the materials on you. +- Added biome-specific outpost level generation parameters (i.e. outpost levels look different in different biomes). +- Minor visual improvements to Hydrothermal Wastes and The Great Sea. +- Faction reputation is reset after finishing the campaign. +- Adjusted FB3000 fabrication recipe (the previous required so many materials they don't fit in the fabricator's input slots). +- Fixed misplaced sonar beacon around the end of the campaign, causing the sonar to display an "emergency signal" outside the level. +- Added an event on the round after rescuing Subra (a sort of escort mission, it's weird if he just disappears after the rescue mission). +- Changed sonar flora sprite depths to prevent them from being obscured by the edge chunk objects. +- Restrict the maximum reputation loss from damaging NPCs and walls to 10 per round. Could use some further adjustment (feedback welcome!), but now a trigger-happy player can't cause too much permanent damage, but 10 is still a sizable penalty. +- Halved the reputation losses from damaging NPCs and walls. The previous values seemed too punishing now that the reputation loss has much more long-lasting consequences. +- Adjusted music intensity ranges to make the actual "music" (as opposed to the intensity tracks) play more frequently. +- Artifact transport cases require batteries to nullify the effects of the artifacts. The batteries last a little over 8 minutes. Also added some animations and lights to the case when powered. +- Allow stealing items in trash cans. +- Made sulphurite shards explode when thrown. +- Outpost NPCs who offer services don't get turned hostile by low reputation (but have some special dialog lines when you interact with them). + +Fixes: +- Fixed crashing when you exit Steam while the Workshop menu is open. +- Fixed crashing with an error message about OutpostGenerationParams.CanHaveCampaignInteraction when some outpost NPCs defined in outpost generation params can't be found. +- Fixed outpost NPCs never attacking you if your reputation is not low. +- Fixed characters holding and eating bananas weirdly. :) +- Fixed inability to hire Jacov Subra if you miss/ignore the event the first time you get it. +- Fixed all lights always going through walls in outposts. +- Fixed water detector in Dugong's oxygen generator room not being connected to the flood alarm circuit. +- Fixes to UI layout on ultrawide resolutions. +- Fixed ragdolls sometimes getting stuck on corners near platforms. +- Fixed characters not falling through holes in the floor below them until they move. +- Fixed inaccurate "Steady Tune" description. +- Fixed autoinjectors doing nothing. +- Fixed characters sometimes dying from barotrauma despite the pressure icon not being visible when in a partially pressurized hull. +- Outpost generator only takes walls with a collider into account when determining the bounds of the modules. Fixes husk modules being placed unnecessarily far from the outpost due to the decorative structures outside the door, leaving a very short hallway between the modules. +- Fixed character interaction texts (like "[H] Heal") not changing when you change the language. +- Fixed arc emitter briefly stunning the user client-side when fired. +- Fixed CustomInterface not showing the labels correctly in the sub editor (displaying the text tag instead of the actual text). +- Fixed operate order category not getting highlighted for the turret objective in the campaign tutorial. +- Fixed characters sometimes becoming immobile for a moment (as if they were briefly stunned) when the surface of the water raises above their chest. +- Fixed outpost NPCs who were supposed to stay in the room they spawn in (e.g. NPCs offering outpost services) occasionally wandering out of the room. +- Fixed pirates / bot crews being unable to swap between weapons and improved how they decide which turret to use (allowing them to swap to a turret with better visibility to the target). +- Fixed monsters sometimes spawning right next to pirate subs. +- Fixed "Gene Harvester" talent spawning genetic materials on pets. +- Fixed rebinding the use key not working in the sub editor. +- Fixed Ctrl+A not selecting connected wires in the sub editor. +- Fixed cancelling fabrication always enabling the amount slider, even if you can only fabricate one. +- Made beds secondary items. Fixes being able to climb ladders while in bed. +- Fabricator input slot tooltips don't show duplicate item names when the item can be crafted from multiple different items with the same name (e.g. petraptor egg can be crafted from 3 different egg items, all called "mudraptor egg"). +- Fixed attacking others with the husk appendage not healing the user. +- Fixed headsets being drawn in front of helmets when you equip the helmet 1st and then the headset. +- Dying due to a disconnect doesn't trigger talents (like "Revenge Squad") or get recorded as a kill. +- Fixed pet name tag getting stuck mid-air when equipped. +- Fixed next round's missions not being displayed in the round summary when leaving a location that has missions (e.g. outpost with a jailbreak mission). +- Fixed turrets being able to launch projectiles inside enemy subs if you poke the turret through the sub's hull. +- Fixed an issue that sometimes caused some levels to always display as unvisited and unlocked (more specifically, the level that happened to generate 1st during the campaign map generation). +- Fixed treatment suggestion for husk infection being shown when wearing zealot robes. +- Fixed all crates and ammo boxes having slightly too large colliders, making them float above ground. +- Fixed misaligned pulse laser sprites. +- Fixed fabricators not being linked to the cabinet next to them in Azimuth, Berilia, Kastrull, Orca 2, Typhon, Typhon 2 and Winterhalter. +- Fixed inability to fabricate high-quality nuclear depth charges. +- Fixed exosuits getting autofilled with batteries despite being powered by fuel rods now. +- Fixed exosuits' lights not turning off when the wearer dies. +- Fixed exosuit not muffling sounds when worn. +- Fixed cultist hood overlapping with the exosuit. +- Fixed escort missions sometimes unlocking in a level leading to an abandoned outpost even if there's an inhabited one available. +- Fixed campaign's end boss moving away from you if you attack it with melee weapons. +- Fixed projectile spread not working properly, causing the projectiles to be launched at the same angle too often. +- Fixed some of the fonts not working properly in Japanese, displaying roughly similar Chinese symbols instead of the correct Japanese symbols. +- Fixed pirate captain hats peeking through PUCS's helmet. +- Fixed escorted characters being hostile if they belong to a faction you have a low reputation with. +- Reduce the minimum mass required for a character to be visible with thermal goggles, always show at least the main limb regardless of the mass. Fixes thresher hatchlings being invisible to the goggles. +- Fixed medical clinic sometimes displaying healths as 99% despite the character having seemingly no afflictions. +- Fixed colony docking modules spawning with a bit of water in them. +- Fixed artifacts being slightly off-center and at an incorrect sprite depth in artifact holders. +- Fixed minerals sometimes spawning in normal caves in abyss mining missions. Happened if no abyss islands with caves happened to generate - now we always generate a cave in at least one of the islands. +- Fixed lights on the items the character is wearing being visible when inside a clown crate. +- Fixed mudraptor eggs (or other items set to be damaged by repair tools) not being damaged by flamer. +- Fixed wikiimage_sub not sorting the entities the same way as the sub editor and game screen, causing e.g. doors to render in front of walls. +- Fixed only main subs's sonar working properly in the end levels. +- Fixed "enablecheats" resetting when you save and reload a campaign save, meaning you could enable cheats, use them to e.g. spawn some weapons, save and reload, and then continue unlocking achievements in that same save. +- Fixed achievements being unlockable in editors. +- Fixed docking ports sometimes becoming impassable when the sub undocks and docks when there's an obstacle right outside it. Happened, for example, with certain kinds of elevators built using linked submarines. +- Fixed orders persisting even if the target no longer exists after a sub switch. +- Fixed reputation reward text sometimes overflowing in the round summary (e.g. when the mission modifies both the husk cult and clown rep). +- Fixed all friendly characters using the "hostage" dialog and all hostile characters the "bandit" dialog in abandoned outposts. +- Fixed "fight intruders" order causing bots to attack enemies in abandoned outposts again. +- A safeguard against getting pinned under a flooded pirate sub: if your sub is under an enemy sub with no living enemies inside it, and heading upwards, the submarine on top will gradually move up. +- Fixed subs sometimes getting stuck if they try to squeeze through a too small passage in the level. +- Fixed monsters sometimes spawning inside the sub during beacon missions. +- Fixed monsters spawned by nest missions still sometimes spawning inside a wall. +- Fixed hanging end of the wire sometimes being "at the wrong end" of the wire after copying entities or saving/reloading. +- Fixed artifacts never spawning randomly in ruins: we tried to spawn them in containers with the tag "ruintreasure", but those were all small chests which couldn't hold artifacts. I made artifacts containable in any alien chest now, and also have a 5% chance to spawning an artifact in a large chest. +- Fixed water moving erratically in rooms with lots of small, connected hulls. +- Fixed Medical Expertise not increasing bandage effectiveness. +- Wearing a clown suit without a mask gives you the "clown" status tag (making you play circus music on instruments and giving you immunity to to banana peels). + +Multiplayer: +- Changes to make starting a round more robust: fixes various equality check errors ("submarine/mission doesn't match") if starting a multiplayer round takes a long time. +- Fixed clients not seeing wall damage in outposts if the server has made outpost walls damageable, and the client doesn't have permissions to manage server settings. +- Fixed dedicated servers' content package info getting truncated to 255 bytes, causing the content package list to just display "unknown" if the server has lots of mods enabled. +- Fixed sonar beacon tickbox flickering on and off when interacted with in multiplayer. +- Fixes to oddities/inconsistencies when trying to heal someone while climbing ladders: dropping off ladders when opening your own health interface in MP, and "heal [H]" hint being visible when focusing on other characters while climbing ladders and the health interface opening for one frame if you attempt to heal. +- Fixed console errors when a client disconnects while a vote is running, and purchases being free if the vote goes through. +- Fixed "none" permission preset not working as it should when the language is set to something else than English (clients would not get assigned the "none" preset by default). +- Fixed 0% condition items in the character's inventory executing the OnBroken effects at the start of a round (i.e. empty stun and fixfoam grenades detonating in your inventory). +- Fixed pre-unlocked talents not being visible client-side until the next round when hiring one of the special faction NPCs. +- Fixed minerals spawning with their rotation set to 0 in multiplayer in mineral missions. +- Fixed ragdolls sometimes getting stuck on the wrong side of a door client-side, and not getting corrected until the client moves in the opposite direction or opens the door. +- Don't force campaign rounds to stop when the only client on the server is using freecam. +- Fixed spectators not hearing others before their character despawns when dead. +- Fixed light sprites being slightly too small on wrecked items. +- Fixed characters getting removed at the end of the round if they've died and then been revived with the "revive" console command. +- Mission unlock notifications aren't shown to people in the server lobby when a round is running. +- Fixed local voice chat icon switching to radio icon (from yellow to gray) at the end of conversations when you release the push-to-talk key. + +Modding: +- Fixed ability to "relaunch" a projectile that has already been launched or that's stuck to some target using status effects. +- Fixed LevelTrigger's OtherTrigger type not doing anything. +- Improvements to LevelObject culling to make objects less likely to disappear when they're in view and there's a too large number of objects visible. +- Fixed NearbyItems effects causing a crash if TargetIdentifiers haven't been set. +- Fixed LevelTrigger statuseffects not doing anything when triggered by a submarine. +- Fixed submarine spawning docked to a random docking port of a custom outpost module with multiple ports, even if one of them is marked as a main docking port. + +--------------------------------------------------------------------------------------------------------- +v1.0.8.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed loading screens sometimes getting stuck when playing in Chinese, Japanese or Korean. +- Fixed certain mods that override outpost generation parameters causing crashes due to missing outpost NPC prefabs. +- Fixed outpost NPCs never attacking you (just aiming their guns at you) if you attack them, but your reputation is not low enough to turn the outpost hostile. +- Fixed broken dialog line in the waytoascension1 event. +- Fixed healing your crewmates causing your karma to decrease. + +--------------------------------------------------------------------------------------------------------- +v1.0.7.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed mechanic tutorial getting stuck at the point where you need to weld a leak. +- Fixed treatment suggestions not showing up in the naloxone part of the medic tutorial. +- Fixed patient spawning dead in the CPR part of the medic tutorial. +- Fixed bots being unable to move through Herja's airlock due to an unlinked waypoint. +- Fixed tutorial Dugong spawning with empty ammo boxes. + +--------------------------------------------------------------------------------------------------------- +v1.0.6.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed some new Steam achievements not unlocking. + +--------------------------------------------------------------------------------------------------------- +v1.0.5.0 +--------------------------------------------------------------------------------------------------------- + +- Fixes to Japanese translations. +- Implemented support for some upcoming Steam achievements. +- Improved backwards compatibility: fixed outpost managers no longer spawning in mods that override outpost generation parameters due to the generic non-faction-specific outpost manager prefab being removed. + +--------------------------------------------------------------------------------------------------------- +v1.0.4.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed outpost NPCs getting randomized every time you re-enter an outpost. +- Fixed inability to gain more than 15 talent points in the multiplayer campaign. + +--------------------------------------------------------------------------------------------------------- +v1.0.3.0 +--------------------------------------------------------------------------------------------------------- + +- Adjusted plant spawn rates in caves. +- Made lead more common in stores. +- Fixed mission listing shown on the top of the screen spoiling enemy faction ambushes. +- Fixed enemy faction ambushes being possible everywhere on the map (they should only happen within 3 steps of outposts belonging to an enemy faction). +- Fixes bots sometimes running towards doors that are closed by something else (another person or an automatic logic). +- Fixes bots ordered to wait outside of the submarine not being able to switch their oxygen tanks. +- Added a couple of endgame-foreshadowing lore bits. + +--------------------------------------------------------------------------------------------------------- +v1.0.2.0 +--------------------------------------------------------------------------------------------------------- + +- Exosuits are powered by fuel rods instead of batteries. +- Fixed affliction probabilities being evaluated twice, meaning that e.g. 50% probability of getting some affliction from an attack was actually a 25% probability. +- Fixed item highlights from the previous round remaining visible the next round. +- Fixed swapping items in a container sometimes causing too many items to be visible in it. +- Fix the vitality modifiers on husk not working properly, because health indices on the limbs were not defined. Effectively husks always took 2x damage. +- Fixed characters still spawning inside outposts that have turned hostile due to low reputation. +- Fixed all special faction hire events getting stuck if you say you need to "think about it", return, and say you still need to think about it. +- Fixed missing "place in ceiling" text in beacon station save dialog. +- Fixed basic depth charges being cheaper than intended (only 30 mk). +- Fixed inability to make lights blink at a high frequency by rapidly turning them on and off with e.g. oscillators. +- Fixed ranged weapons emitting particles in the wrong direction. There haven't been any changes to this code in years, so it must've been an issue for a long time, I guess we just never noticed because no gun before the scrap cannon emitted particles with a noticeable velocity? +- Fixed a pathfinding issue that often made bots swim against cave walls. +- Fixed inability to join servers with a submarine switch/purchase vote running. +- Fixed votes passing if the client who initiated them disconnects before anyone else votes. +- Fixed follow orders not being persistent between singleplayer rounds. + +--------------------------------------------------------------------------------------------------------- +v1.0.1.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed Mailman talent giving you the 150 mk bonus every time you open the campaign map or mission menu and there's a cargo mission visible. +- Fixed missions available from the destination location to some other location being listed as "outpost missions" in the campaign map's mission selection. +- Fixed outposts that faction missions take place in being allowed to turn into abandoned outposts when next to hunting grounds, making the missions impossible to complete. +- Fixed hidden items (e.g. Separatist deco that's disabled in Coalition outposts) sometimes getting chosen as targets for scripted events, resulting in non-interactable, glowing "ghost items". +- Fixed mysterious floating status monitor in AdminModule_02_Colony. +- Miscellaneous optimizations. +- Fixed Separatist jailbreak mission causing a crash. +- Fixed ranged weapons (most noticeably, scrap cannon) emitting particles in the wrong direction. +- Fixed acid burns not having a cause of death text. +- Fixed "skedaddle" not giving a 10% movement boost like the description says. +- Fixed odd fabrication list sorting: the items that require a recipe to fabricate were split into ones you have the skills to fabricate and ones you don't, even though that isn't visible in the UI, making the list just seem out of order. +- Fixed red glow around the light switch's green button. +- Fixed banana being held weirdly. +- Fixed inability to hold a captain's pipe or cigar in your left hand. +- Fixed ready checks not working. + +--------------------------------------------------------------------------------------------------------- +v1.0.0.0 +--------------------------------------------------------------------------------------------------------- + +Faction overhaul: +- Outposts are controlled by the Europa Coalition or Jovian Separatists, and some of them include a module belonging to the Church of Husk or Children of the Honkmother. +- Got rid of location-specific reputation. Now all the events/missions give faction reputation instead (excluding missions that aren't related to or given by a faction, e.g. abandoned outpost missions). +- Lots of new outpost events, and a longer "event chain" for the secondary factions. +- Lots of new faction-specific missions: some variants of existing missions, some new. +- Faction-specific hires: "generic" high-level characters with more experience points and better gear than normal hireable NPCs. Available for hiring when Coalition or Separatist reputation is high enough. +- Special, named characters who can be hired via scripted events after reaching a high enough reputation. +- Faction-specific vendors (separatists, husks, clowns) who sell special items (many of which are completely new) if your reputation is high enough. +- If your Coalition/Separatist reputation is low enough, you may get attacked by their vessel during missions. +- There's now always two paths from biome to another, one controlled by the Coalition and one by the Separatists. +- Improvements to the campaign map. +- Added a 3rd talent tree, "Politician", for the Captain. Focused around faction relations and reputation. + +Endgame: +- Completely remade the ending of the campaign. Now you'll get to see what's beyond the Eye of Europa and perhaps uncover the cause for the increasing levels of radiation. +- New types of enemies/bosses. +- Some new events to foreshadow the ending during the course of the campaign. + +Misc changes: +- New loading screen / location portraits. +- Two new music tracks. +- Items' skill requirements are shown in their tooltips (the same way as damage resistances). +- Tweaks to poisons. +- Adjusted Europan Handshake to work better with the overhauled morbusine poisoning. +- Acid Grenades and 40mm Acid Grenades are now properly affected by talents +- Acid Grenades and 40mm Acid Grenades deal more damage and slow enemies down, making them more viable against fast monsters. +- Made regular 40mm grenades penetrate armor more efficiently. +- Made Diving Suits resist Acid Burns a bit more. +- Europa Brew's Acid Vulnerability is now double as effective (200% damage taken instead of 100%). +- Adjustment to throwable items (shorter throw distance and reduced speed in water). +- Made flares float in place to make them more useful. +- Made high-quality stun guns more effective (stunning the target faster). +- The health scanner always shows poisons and paralysis on monsters to make it easier to determine whether the poisoning is progressing or wearing off. +- A pass on sound ranges: the ranges should now be more consistent and sensible. +- Made moloch shell fragment and riot shield medium items instead of small to fix them going inside e.g. toolbelts. +- Made husk eggs consumable. +- Made it more difficult to repeatedly enter an abandoned outpost and re-loot the bandits: now the bandits immediately attack you if you re-enter the outpost. +- Monsters you haven't encountered yet are now hidden by default in the character editor. Can be enabled using the command "showmonsters" and re-hidden using "hidemonsters". The value is saved in creaturemetrics.xml. Doesn't affect custom creatures. +- Fixed character crush depths behaving inconsistently (varying between levels, e.g. sometimes crushing the character at the depth of 2000 meters, sometimes 3000). +- Improvements to submarine crush depth effects: previously the breaches were easy to deal with because pressure did small amounts of damage to all walls, now it instead does heavier damage to some walls (and the amount of damage and walls to damage increases with depth). +- Added a round light component variant. +- Increased the hard-coded max mission count back from 3 to 10. It'd be preferable to not change the value above 3 in the vanilla game, but since campaign settings are not moddable, we shouldn't be too strict about it (because it can be useful for a mod that this value can be adjusted). +- Miscellaneous optimizations. +- New slot indicator icons (= the icons that show what can go inside some items, like tanks/ammo). +- Made outpost hull repair service cheaper. +- Doors can now be damaged by melee weapons and ranged (handheld) weapons. (They were already destructible by submarine mounted weapons and explosives) +- Adjusted and rebalanced item damage for most items, to take into account doors being destructible. +- Reduced time needed for a crowbar to open doors, 7.5s for regular doors, 6s for wrecked doors (down from 10 s). +- Boosted Plasma Cutter damage against doors and items (walls not touched). +- Made galena more common in order to make lead easier to get. +- Added Auto Operate option for all turrets. Can be enabled in the submarine editor. Not currently used on vanilla submarines. Auto operated turrets don't require a person to operate them, but they still require power and ammunition (-> someone needs to reload them). + +Multiplayer: +- Added a language filter to the server browser. +- Fixed reports given by dragging and dropping them on the status monitor always targeting the room the character is inside. +- Improvements to medical clinic syncing (should fix some of the afflictions a character has sometimes not being visible on the list). +- Fixed crashing if you close a server when mod downloads are disabled. +- Improved projectile syncing: spread now behaves the same client-side as it does server-side (as opposed to being completely random). +- Improvements/fixes to dialogs that are shown to multiple clients: disable the option buttons when another client chooses an option, and highlight the option that was chosen. +- Fixed server randomizing the game mode at the end of the round when playing a campaign with the game mode selection set to Random. + +AI: +- Fixed bots considering certain multi-hull rooms flooded when they are not. +- Fixed bots deciding prematurely that they can't fix an item when it's deteriorating (e.g. when it's submerged). +- Fixed bots removing battery cells from exosuits when ordered to charge batteries. +- Fixed bots sometimes getting stuck in automatic doors and/or double doors, because they didn't wait for the door to open entirely before pressing the button again. +- Improved bot ‘extinguish fires’ behavior. Fixes bots sometimes not being able to extinguish larger fires, because they stopped too far and didn't keep advancing towards the target. +- Fixed bots claiming that they can't return back to the sub and then following the order anyway. +- Improved the ‘find safety’ calculations so that the bots give more preference to the distance of the room. +- Fixed some remaining issues and edge cases in the logic over when the bot needs diving gear and when it can be taken off. +- Fixed captains (and some NPCs) idling in the airlock if they equip a diving suit. +- Bot can now target items (like projectiles) with turrets and have different targeting priorities on different monsters. +- Fixed bots being allowed to reach items that are too far to be interacted with. + +Fixes: +- Fixes and improvements to translations (Japanese and Chinese in particular). +- Fixed light components with a range of 0 and a hidden sprite being invisible against dark backgrounds. +- Various fixes to Typhon 1: most notably, adjusting the hulls to prevent some rooms from being impossible to drain fully. +- Fixed "kill" command not killing characters under the influence of "Miracle Worker". +- Fixed some lights becoming invisible when their range is set to 0 and they're against a dark background. +- Fixed lights turning on without power when they receive a toggle or set_color input. +- Fixed changing the amount of items to fabricate inadvertently starting(or activating) fabrication in MP if you've previously started fabricating something +- Fixed campaign settings resetting in the campaign setup menu every time you relaunch the game (meaning you'd always need to e.g. remember to toggle the tutorial off if you want to play without it). +- Fixed inverted mouse buttons not working properly since the last update: the left mouse button was considered the primary mouse button regardless of your OS settings. +- Fixed status monitor not properly displaying condition on tinkered items. +- Fixed machines smoking when above 100% condition with tinkering. +- Fixed inventory overlapping with the chatbox on low aspect ratios (small width, large height). +- Fixed some layering issues in abandoned outposts. +- Fixed water-sensitive items sometimes spawning as loot in wrecks. +- Fixed radio static still playing even if you don't have a headset. +- Fixed rifle grenade sounds not working. +- Fixed crashing on startup if the MD5 hash cache file is empty. +- Fixed research stations and loaders not being visible on the status monitor's electrical view. +- Fixed artifact missions sometimes choosing the same artifact as a target if you happen to have multiple missions active at a time, which would lead to console errors when the round ends. +- Fixed exosuit playing the warning beep if there's empty or almost empty tanks in any of its slots. +- Fixed oxygen generators deteriorating in some of the outpost modules. +- Fixed reputation loss when a character other than the player (e.g. crawlers in the 'crawleroutbreak' event) damages the outpost walls. +- Fixed outpost modules sometimes being placed in a way that makes them overlap with the sub. +- Fixed characters trying to walk in flooded spaces that are too low to stand in (like some of the tight passages in alien ruins). +- Genetic material backwards compatibility to fix old unidentified genetic materials disappearing from saves prior to v0.21.6.0. +- Fixed genetic materials being too rare in outposts now. +- Fixed hunting grounds affecting outposts 2 steps away, not just ones in adjacent locations. +- Fixed "residual waste" talent duplicating genetic materials. +- Fixed monsters sometimes spawning inside destructible ice chunks in caves. +- Fixed respawn shuttle sometimes spawning inside floating ice chunks. +- Fixed equipping a ranged weapon setting its reload timer to 1, making it possible to reduce some weapons' loading times by unequipping and equipping them. +- Fixed partially consumed items not staying on top of the stack they're in. +- Fixed submarine tier and class affecting the prices of the submarine upgrades: e.g. a tier 2 upgrade would cost more on a submarine where tier 2 is the maximum than on a submarine with a higher maximum. +- Fixed reputation loss when you steal items from bandits in a beacon station. +- Fixed equipped flares igniting when you click on the inventory. +- Fixed "Quickdraw" talent not affecting Alien Pistols. +- Fixed toolbelts and other items worn on the torso getting hidden when wearing a safety harness. +- Fixed advanced syringe gun and slipsuit fabrication recipes. +- Fixed floating pumps and ladder layering issues in Herja. +- Limited the number of makeshift shelves per sub to 3 (similar to portable pumps). Otherwise you can use them to expand the sub's cargo capacity indefinitely. +- Fixed dementonite and hardened crowbars spawning in respawn containers (= respawn shuttle cabinets). +- Fixed alien blood no longer causing psychosis, + made it slightly less effective to make fabricating blood packs more worthwhile +- Fixed fire extinguisher spray getting blocked by characters. +- Venture: Fixed the battery room not flooding properly (again), fixed the two hulls in the airlock not being linked, adjusted the waypoints a bit. +- Selkie: Disconnect the outer nodes from the ladder/door nodes, because the docking ports can't be opened manually. +- Fixed Thalamus AI not running properly when there's no player characters or submarines around (e.g. when all the players are in the freecam mode). +- Fixed the ammo indicator not showing correctly on the advanced syringe gun. +- Fixed bots sometimes getting confused by outside waypoints while being inside an outpost. +- Fixed item relocation logic running also on NPCs that are not in the player team, which could cause diving suits dropped by NPCs to get spawned in the player sub. + +Modding: +- Fixed crashing if a StatusEffect is configured to SpawnItemRandomly but doesn't configure anything to spawn. +- Improved the error handling of item/character variants. Previously if the parent prefab wasn't found, there was no error message, but the variant was still created, causing crashes in various situations. +- Added AITurretPriority and AISlowTurretPriority on items and characters. Setting the priority to 0 can be used for telling the bots to ignore the target entirely. Items also need to have IsAITurretTarget="True" enabled to make them a valid target. +- Added ItemDamageMultiplier on items. Can be used for increasing the damage caused by other items, like weapons. Works like the existing ExplosionDamageMultiplier. + --------------------------------------------------------------------------------------------------------- v0.21.6.0 --------------------------------------------------------------------------------------------------------- @@ -5,6 +420,7 @@ v0.21.6.0 - Minor localization fixes. - Fixed some occasional crashes in the character editor. + --------------------------------------------------------------------------------------------------------- v0.21.5.0 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaTest/EndpointParseTests.cs b/Barotrauma/BarotraumaTest/EndpointParseTests.cs index c8ccb4e43..0038d952b 100644 --- a/Barotrauma/BarotraumaTest/EndpointParseTests.cs +++ b/Barotrauma/BarotraumaTest/EndpointParseTests.cs @@ -14,8 +14,7 @@ public class EndpointParseTests { Endpoint.Parse("127.0.0.1:27015") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option.Some(new LidgrenEndpoint(IPAddress.Loopback, 27015)), options => options.RespectingRuntimeTypes()); } @@ -25,8 +24,7 @@ public class EndpointParseTests { Endpoint.Parse("localhost:27015") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option.Some(new LidgrenEndpoint(IPAddress.Loopback, 27015)), options => options.RespectingRuntimeTypes()); } @@ -36,8 +34,7 @@ public class EndpointParseTests { Address.Parse("127.0.0.1") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option
.Some(new LidgrenAddress(IPAddress.Loopback)), options => options.RespectingRuntimeTypes()); } @@ -47,8 +44,7 @@ public class EndpointParseTests { Endpoint.Parse("STEAM_1:1:508792388") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option.Some(new SteamP2PEndpoint(new SteamId(76561198977850505))), options => options.RespectingRuntimeTypes()); } @@ -58,8 +54,7 @@ public class EndpointParseTests { Address.Parse("STEAM_1:1:508792388") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option
.Some(new SteamP2PAddress(new SteamId(76561198977850505))), options => options.RespectingRuntimeTypes()); new SteamId(76561198977850505).StringRepresentation.Should().BeEquivalentTo("STEAM_1:1:508792388"); diff --git a/Barotrauma/BarotraumaTest/MathUtilsTests.cs b/Barotrauma/BarotraumaTest/MathUtilsTests.cs new file mode 100644 index 000000000..e624b3f4e --- /dev/null +++ b/Barotrauma/BarotraumaTest/MathUtilsTests.cs @@ -0,0 +1,67 @@ +using Barotrauma; +using FluentAssertions; +using Microsoft.Xna.Framework; +using System; +using Xunit; + +namespace TestProject; + +public class MathUtilsTests +{ + [Fact] + public void TestNearlyEquals() + { + MathUtils.NearlyEqual(0.0f, 0.0f).Should().BeTrue(); + MathUtils.NearlyEqual(-float.Epsilon, float.Epsilon).Should().BeTrue(); + MathUtils.NearlyEqual(0.1f + 0.2f, 0.3f).Should().BeTrue(); + MathUtils.NearlyEqual(-1.0f, 1.0f).Should().BeFalse(); + } + + [Fact] + public void TestWrapAngle() + { + MathUtils.NearlyEqual(MathUtils.WrapAnglePi(0.0f), 0.0f).Should().BeTrue(); + + CheckWrapAnglePiNearlyEqual(0, 0).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-90, -90).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-90, 90).Should().BeFalse(); + CheckWrapAnglePiNearlyEqual(-180, 180).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-190.0f, 170.0f).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-360, 0).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(360, 0).Should().BeTrue(); + + bool CheckWrapAnglePiNearlyEqual(float wrappedDeg, float deg) + { + float wrappedRad = MathUtils.WrapAnglePi(MathHelper.ToRadians(wrappedDeg)); + float rad = MathHelper.ToRadians(deg); + return MathUtils.NearlyEqual(wrappedRad, rad) || MathUtils.NearlyEqual(Math.Abs(wrappedRad - rad), MathHelper.TwoPi); + } + + CheckWrapAngleTwoPiNearlyEqual(0, 0).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(90, 90).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(-90, 270).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(180, 180).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(360 * 5, 0).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(-360, 0).Should().BeTrue(); + + bool CheckWrapAngleTwoPiNearlyEqual(float wrappedDeg, float deg) + { + float wrappedRad = MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(wrappedDeg)); + float rad = MathHelper.ToRadians(deg); + return MathUtils.NearlyEqual(wrappedRad, rad) || MathUtils.NearlyEqual(Math.Abs(wrappedRad - rad), MathHelper.TwoPi); + } + + CheckShortestAngleNearlyEqual(0.0f, 0.0f, 0.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(0.0f, 90.0f, 90.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(0.0f, 360.0f, 0.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(0.0f, -365.0f, -5.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(180.0f, -180.0f, 0.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(-355.0f, 5.0f, 10.0f); + + bool CheckShortestAngleNearlyEqual(float deg1, float deg2, float angle) + { + return MathUtils.NearlyEqual(MathUtils.GetShortestAngle(MathHelper.ToRadians(deg1), MathHelper.ToRadians(deg2)), MathHelper.ToRadians(angle)); + } + + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/PropertyConditionalTests.cs b/Barotrauma/BarotraumaTest/PropertyConditionalTests.cs new file mode 100644 index 000000000..4c22c9dcd --- /dev/null +++ b/Barotrauma/BarotraumaTest/PropertyConditionalTests.cs @@ -0,0 +1,67 @@ +using Barotrauma; +using FluentAssertions; +using FsCheck; +using System; +using System.Collections.Immutable; +using System.Linq; +using Xunit; +namespace TestProject; + +public sealed class PropertyConditionalTests +{ + private readonly record struct OperatorStr(string Str); + private readonly record struct ConditionStr(string Str); + + private class CustomGenerators + { + public static Arbitrary OperatorStrGeneratorOverride() + { + return Gen.Choose(0, operators.Length-1) + .Select(i => operators[i]) + .ToArbitrary(); + } + + public static Arbitrary ConditionStrGeneratorOverride() + { + return Arb.Generate() + .Where(s => s != null && !s.Any(char.IsWhiteSpace) && !s.Contains(',')) + .Select(s => new ConditionStr(s)).ToArbitrary(); + } + } + + public PropertyConditionalTests() + { + Arb.Register(); + Arb.Register(); + } + + static ImmutableArray operators + = new[] + { + "eq", "neq", "gt", + "gte", "lt", "lte" + }.Select(s => new OperatorStr(s)).ToImmutableArray(); + + [Fact] + public void TestExtractComparisonOperatorFromConditionString() + { + Prop.ForAll( + Arb.Generate().ToArbitrary(), + Arb.Generate().ToArbitrary(), + ExtractComparisonOperatorFromConditionStringCase) + .QuickCheckThrowOnFailure(); + } + + private static void ExtractComparisonOperatorFromConditionStringCase(OperatorStr operatorStr, ConditionStr conditionStr) + { + var op = PropertyConditional.GetComparisonOperatorType(operatorStr.Str); + + var (op2, condStr) = PropertyConditional.ExtractComparisonOperatorFromConditionString(operatorStr.Str + " " + conditionStr.Str); + op2.Should().Be(op); + condStr.Should().Be(conditionStr.Str); + + var (op3, condStr2) = PropertyConditional.ExtractComparisonOperatorFromConditionString(conditionStr.Str); + op3.Should().Be(PropertyConditional.ComparisonOperatorType.Equals); + condStr2.Should().Be(conditionStr.Str); + } +} diff --git a/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs b/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs index 12cdb9243..03cbcb402 100644 --- a/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs +++ b/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs @@ -37,22 +37,26 @@ public sealed class SerializableDateTimeTests { Prop.ForAll(EqualityCheck).QuickCheckThrowOnFailure(); } - + [Fact] public void ParseTest() { - var parseTest = "9369Y 09M 06D 03HR 43MIN 09SEC UTC+8:49"; - SerializableDateTime.Parse(parseTest); Prop.ForAll(ParseCheck).QuickCheckThrowOnFailure(); } - + + [Fact] + public void ToLocalTest() + { + Prop.ForAll(ToLocalCheck).QuickCheckThrowOnFailure(); + } + private static void EqualityCheck(SerializableDateTime original) { var local = original.ToLocal(); var utc = original.ToUtc(); - original.Should().BeEquivalentTo(local); - original.Should().BeEquivalentTo(utc); - local.Should().BeEquivalentTo(utc); + original.Should().BeEquivalentTo(local, because: "original must equal local"); + original.Should().BeEquivalentTo(utc, because: "original must equal utc"); + local.Should().BeEquivalentTo(utc, because: "local must equal utc"); } private static void ParseCheck(SerializableDateTime original) @@ -61,4 +65,11 @@ public sealed class SerializableDateTimeTests SerializableDateTime.Parse(str).TryUnwrap(out var parsedTime).Should().BeTrue(); parsedTime.Should().BeEquivalentTo(original); } + + private static void ToLocalCheck(SerializableDateTime original) + { + var localNow = SerializableDateTime.LocalNow; + var convertedDateTime = original.ToLocal(); + localNow.TimeZone.Should().BeEquivalentTo(convertedDateTime.TimeZone); + } } diff --git a/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs b/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs index fd6af2fa0..c47fdb10d 100644 --- a/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs +++ b/Libraries/Facepunch.Steamworks/Callbacks/CallResult.cs @@ -14,7 +14,7 @@ namespace Steamworks internal struct CallResult : INotifyCompletion where T : struct, ICallbackData { SteamAPICall_t call; - ISteamUtils utils; + ISteamUtils? utils; bool server; public CallResult( SteamAPICall_t call, bool server ) @@ -43,6 +43,8 @@ namespace Steamworks ///
public T? GetResult() { + if (utils is null) { return null; } + bool failed = false; if ( !utils.IsAPICallCompleted( call, ref failed ) || failed ) return null; @@ -76,6 +78,8 @@ namespace Steamworks { get { + if (utils is null) { return true; } + bool failed = false; if ( utils.IsAPICallCompleted( call, ref failed ) || failed ) return true; diff --git a/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs b/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs index 28c80c246..aedff442f 100644 --- a/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs +++ b/Libraries/Facepunch.Steamworks/Classes/AuthTicket.cs @@ -4,7 +4,7 @@ namespace Steamworks { public class AuthTicket : IDisposable { - public byte[] Data; + public byte[]? Data; public uint Handle; public bool Canceled { get; private set; } @@ -17,7 +17,7 @@ namespace Steamworks { if (Handle != 0) { - SteamUser.Internal.CancelAuthTicket(Handle); + SteamUser.Internal?.CancelAuthTicket(Handle); } Handle = 0; diff --git a/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs b/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs index 680c54238..a0a722464 100644 --- a/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs +++ b/Libraries/Facepunch.Steamworks/Classes/Dispatch.cs @@ -26,7 +26,7 @@ namespace Steamworks /// Params are : [Callback Type] [Callback Contents] [server] /// ///
- public static Action OnDebugCallback; + public static Action? OnDebugCallback; /// /// Called if an exception happens during a callback/callresult. @@ -34,7 +34,7 @@ namespace Steamworks /// async.. and can fail silently. With this hooked you won't be stuck wondering /// what happened. /// - public static Action OnException; + public static Action? OnException; #region interop [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ManualDispatch_Init", CallingConvention = CallingConvention.Cdecl )] @@ -288,7 +288,7 @@ namespace Steamworks /// /// Install a global callback. The passed function will get called if it's all good. /// - internal static void Install( Action p, bool server = false ) where T : ICallbackData + internal static void Install( Action p, bool server = false ) where T : struct, ICallbackData { var t = default( T ); var type = t.CallbackType; diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj index 474798281..26bfe8f64 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Posix.csproj @@ -5,10 +5,15 @@ $(DefineConstants);PLATFORM_POSIX64;PLATFORM_POSIX;PLATFORM_64 netstandard2.1 true - 8.0 + latest true false Steamworks + enable + + + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj index 94741525f..29d8dfcd1 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj @@ -10,6 +10,7 @@ true Steamworks AnyCPU;x64 + enable @@ -48,6 +49,10 @@ 1701;1702;1591;1587 + + ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + + diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs index baa03e68f..3e5e1c326 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamApps.cs @@ -76,7 +76,7 @@ namespace Steamworks private static extern Utf8StringPointer _GetCurrentGameLanguage( IntPtr self ); #endregion - internal string GetCurrentGameLanguage() + internal string? GetCurrentGameLanguage() { var returnValue = _GetCurrentGameLanguage( Self ); return returnValue; diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs index 9adbeae02..b8c24ac7d 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamInventory.cs @@ -40,13 +40,13 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetResultItems", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetResultItems( IntPtr self, SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[] pOutItemsArray, ref uint punOutItemsArraySize ); + private static extern bool _GetResultItems( IntPtr self, SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[]? pOutItemsArray, ref uint punOutItemsArraySize ); #endregion /// /// Copies the contents of a result set into a flat array. The specific contents of the result set depend on which query which was used. /// - internal bool GetResultItems( SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[] pOutItemsArray, ref uint punOutItemsArraySize ) + internal bool GetResultItems( SteamInventoryResult_t resultHandle, [In,Out] SteamItemDetails_t[]? pOutItemsArray, ref uint punOutItemsArraySize ) { var returnValue = _GetResultItems( Self, resultHandle, pOutItemsArray, ref punOutItemsArraySize ); return returnValue; @@ -55,10 +55,10 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetResultItemProperty", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetResultItemProperty( IntPtr self, SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); + private static extern bool _GetResultItemProperty( IntPtr self, SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); #endregion - internal bool GetResultItemProperty( SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) + internal bool GetResultItemProperty( SteamInventoryResult_t resultHandle, uint unItemIndex, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { using var memory = Helpers.TakeMemory(); IntPtr mempchValueBuffer = memory; @@ -311,10 +311,10 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetItemDefinitionIDs", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetItemDefinitionIDs( IntPtr self, [In,Out] InventoryDefId[] pItemDefIDs, ref uint punItemDefIDsArraySize ); + private static extern bool _GetItemDefinitionIDs( IntPtr self, [In,Out] InventoryDefId[]? pItemDefIDs, ref uint punItemDefIDsArraySize ); #endregion - internal bool GetItemDefinitionIDs( [In,Out] InventoryDefId[] pItemDefIDs, ref uint punItemDefIDsArraySize ) + internal bool GetItemDefinitionIDs( [In,Out] InventoryDefId[]? pItemDefIDs, ref uint punItemDefIDsArraySize ) { var returnValue = _GetItemDefinitionIDs( Self, pItemDefIDs, ref punItemDefIDsArraySize ); return returnValue; @@ -323,10 +323,10 @@ namespace Steamworks #region FunctionMeta [DllImport( Platform.LibraryName, EntryPoint = "SteamAPI_ISteamInventory_GetItemDefinitionProperty", CallingConvention = Platform.CC)] [return: MarshalAs( UnmanagedType.I1 )] - private static extern bool _GetItemDefinitionProperty( IntPtr self, InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); + private static extern bool _GetItemDefinitionProperty( IntPtr self, InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, IntPtr pchValueBuffer, ref uint punValueBufferSizeOut ); #endregion - internal bool GetItemDefinitionProperty( InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) + internal bool GetItemDefinitionProperty( InventoryDefId iDefinition, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Utf8StringToNative ) )] string? pchPropertyName, out string pchValueBuffer, ref uint punValueBufferSizeOut ) { using var memory = Helpers.TakeMemory(); IntPtr mempchValueBuffer = memory; diff --git a/Libraries/Facepunch.Steamworks/Networking/Connection.cs b/Libraries/Facepunch.Steamworks/Networking/Connection.cs index f5f75e5d2..0eb4aafce 100644 --- a/Libraries/Facepunch.Steamworks/Networking/Connection.cs +++ b/Libraries/Facepunch.Steamworks/Networking/Connection.cs @@ -24,7 +24,7 @@ namespace Steamworks.Data ///
public Result Accept() { - return SteamNetworkingSockets.Internal.AcceptConnection( this ); + return SteamNetworkingSockets.Internal?.AcceptConnection( this ) ?? Result.Fail; } /// @@ -33,7 +33,7 @@ namespace Steamworks.Data /// public bool Close( bool linger = false, int reasonCode = 0, string debugString = "Closing Connection" ) { - return SteamNetworkingSockets.Internal.CloseConnection( this, reasonCode, debugString, linger ); + return SteamNetworkingSockets.Internal != null && SteamNetworkingSockets.Internal.CloseConnection( this, reasonCode, debugString, linger ); } /// @@ -41,8 +41,8 @@ namespace Steamworks.Data /// public long UserData { - get => SteamNetworkingSockets.Internal.GetConnectionUserData( this ); - set => SteamNetworkingSockets.Internal.SetConnectionUserData( this, value ); + get => SteamNetworkingSockets.Internal?.GetConnectionUserData( this ) ?? 0; + set => SteamNetworkingSockets.Internal?.SetConnectionUserData( this, value ); } /// @@ -52,13 +52,13 @@ namespace Steamworks.Data { get { - if ( !SteamNetworkingSockets.Internal.GetConnectionName( this, out var strVal ) ) + if ( SteamNetworkingSockets.Internal is null || !SteamNetworkingSockets.Internal.GetConnectionName( this, out var strVal ) ) return "ERROR"; return strVal; } - set => SteamNetworkingSockets.Internal.SetConnectionName( this, value ); + set => SteamNetworkingSockets.Internal?.SetConnectionName( this, value ); } /// @@ -67,7 +67,7 @@ namespace Steamworks.Data public Result SendMessage( IntPtr ptr, int size, SendType sendType = SendType.Reliable ) { long messageNumber = 0; - return SteamNetworkingSockets.Internal.SendMessageToConnection( this, ptr, (uint) size, (int)sendType, ref messageNumber ); + return SteamNetworkingSockets.Internal?.SendMessageToConnection( this, ptr, (uint) size, (int)sendType, ref messageNumber ) ?? Result.Fail; } /// @@ -107,16 +107,16 @@ namespace Steamworks.Data /// Flush any messages waiting on the Nagle timer and send them at the next transmission /// opportunity (often that means right now). /// - public Result Flush() => SteamNetworkingSockets.Internal.FlushMessagesOnConnection( this ); + public Result Flush() => SteamNetworkingSockets.Internal?.FlushMessagesOnConnection( this ) ?? Result.Fail; /// /// Returns detailed connection stats in text format. Useful /// for dumping to a log, etc. /// /// Plain text connection info - public string DetailedStatus() + public string? DetailedStatus() { - if ( SteamNetworkingSockets.Internal.GetDetailedConnectionStatus( this, out var strVal ) != 0 ) + if ( SteamNetworkingSockets.Internal is null || SteamNetworkingSockets.Internal.GetDetailedConnectionStatus( this, out var strVal ) != 0 ) return null; return strVal; diff --git a/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs b/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs index 2007cc777..1ac65e609 100644 --- a/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/ConnectionManager.cs @@ -9,7 +9,7 @@ namespace Steamworks /// /// An optional interface to use instead of deriving /// - public IConnectionManager Interface { get; set; } + public IConnectionManager? Interface { get; set; } /// /// The actual connection we're managing @@ -94,6 +94,8 @@ namespace Steamworks public void Receive( int bufferSize = 32 ) { + if (SteamNetworkingSockets.Internal is null) { return; } + int processed = 0; IntPtr messageBuffer = Marshal.AllocHGlobal( IntPtr.Size * bufferSize ); diff --git a/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs b/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs index 270bf5a9a..f118bdde0 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetIdentity.cs @@ -107,10 +107,11 @@ namespace Steamworks.Data /// /// We override tostring to provide a sensible representation /// - public override string ToString() + public override string? ToString() { var id = this; - SteamNetworkingUtils.Internal.SteamNetworkingIdentity_ToString( ref id, out var str ); + string? str = null; + SteamNetworkingUtils.Internal?.SteamNetworkingIdentity_ToString( ref id, out str ); return str; } diff --git a/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs b/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs index 9aceca533..2fd3c926b 100644 --- a/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs +++ b/Libraries/Facepunch.Steamworks/Networking/NetPingLocation.cs @@ -25,15 +25,16 @@ namespace Steamworks.Data public static NetPingLocation? TryParseFromString( string str ) { var result = default( NetPingLocation ); - if ( !SteamNetworkingUtils.Internal.ParsePingLocationString( str, ref result ) ) + if ( SteamNetworkingUtils.Internal is null || !SteamNetworkingUtils.Internal.ParsePingLocationString( str, ref result ) ) return null; return result; } - public override string ToString() + public override string? ToString() { - SteamNetworkingUtils.Internal.ConvertPingLocationToString( ref this, out var strVal ); + string? strVal = null; + SteamNetworkingUtils.Internal?.ConvertPingLocationToString( ref this, out strVal ); return strVal; } @@ -61,7 +62,7 @@ namespace Steamworks.Data /// You are looking for the "ticketgen" library. public int EstimatePingTo( NetPingLocation target ) { - return SteamNetworkingUtils.Internal.EstimatePingTimeBetweenTwoLocations( ref this, ref target ); + return SteamNetworkingUtils.Internal?.EstimatePingTimeBetweenTwoLocations( ref this, ref target ) ?? Defines.k_nSteamNetworkingPing_Failed; } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Networking/Socket.cs b/Libraries/Facepunch.Steamworks/Networking/Socket.cs index 334f5893e..730e63324 100644 --- a/Libraries/Facepunch.Steamworks/Networking/Socket.cs +++ b/Libraries/Facepunch.Steamworks/Networking/Socket.cs @@ -1,4 +1,5 @@  +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; namespace Steamworks.Data @@ -17,10 +18,11 @@ namespace Steamworks.Data /// public bool Close() { - return SteamNetworkingSockets.Internal.CloseListenSocket( Id ); + return SteamNetworkingSockets.Internal != null && SteamNetworkingSockets.Internal.CloseListenSocket( Id ); } - public SocketManager Manager + [DisallowNull] + public SocketManager? Manager { get => SteamNetworkingSockets.GetSocketManager( Id ); set => SteamNetworkingSockets.SetSocketManager( Id, value ); diff --git a/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs b/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs index 5c585faec..1d9721cf4 100644 --- a/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs +++ b/Libraries/Facepunch.Steamworks/Networking/SocketManager.cs @@ -14,7 +14,7 @@ namespace Steamworks /// public partial class SocketManager { - public ISocketManager Interface { get; set; } + public ISocketManager? Interface { get; set; } public List Connecting = new List(); public List Connected = new List(); @@ -26,12 +26,12 @@ namespace Steamworks internal void Initialize() { - pollGroup = SteamNetworkingSockets.Internal.CreatePollGroup(); + pollGroup = SteamNetworkingSockets.Internal?.CreatePollGroup() ?? default; } public bool Close() { - if ( SteamNetworkingSockets.Internal.IsValid ) + if ( SteamNetworkingSockets.Internal is { IsValid: true } ) { SteamNetworkingSockets.Internal.DestroyPollGroup( pollGroup ); Socket.Close(); @@ -94,7 +94,7 @@ namespace Steamworks /// public virtual void OnConnected( Connection connection, ConnectionInfo info ) { - SteamNetworkingSockets.Internal.SetConnectionPollGroup( connection, pollGroup ); + SteamNetworkingSockets.Internal?.SetConnectionPollGroup( connection, pollGroup ); Interface?.OnConnected( connection, info ); } @@ -104,7 +104,7 @@ namespace Steamworks ///
public virtual void OnDisconnected( Connection connection, ConnectionInfo info ) { - SteamNetworkingSockets.Internal.SetConnectionPollGroup( connection, 0 ); + SteamNetworkingSockets.Internal?.SetConnectionPollGroup( connection, 0 ); connection.Close(); @@ -121,7 +121,7 @@ namespace Steamworks try { - processed = SteamNetworkingSockets.Internal.ReceiveMessagesOnPollGroup( pollGroup, messageBuffer, bufferSize ); + processed = SteamNetworkingSockets.Internal?.ReceiveMessagesOnPollGroup( pollGroup, messageBuffer, bufferSize ) ?? 0; for ( int i = 0; i < processed; i++ ) { diff --git a/Libraries/Facepunch.Steamworks/ServerList/Base.cs b/Libraries/Facepunch.Steamworks/ServerList/Base.cs index 9a5cb1125..eaf66449a 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Base.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Base.cs @@ -11,7 +11,7 @@ namespace Steamworks.ServerList { #region ISteamMatchmakingServers - internal static ISteamMatchmakingServers Internal => SteamMatchmakingServers.Internal; + internal static ISteamMatchmakingServers? Internal => SteamMatchmakingServers.Internal; #endregion @@ -23,17 +23,17 @@ namespace Steamworks.ServerList /// /// When a new server is added, this function will get called /// - public Action OnChanges; + public Action? OnChanges; /// /// Called for every responsive server /// - public Action OnResponsiveServer; + public Action? OnResponsiveServer; /// /// Called for every unresponsive server /// - public Action OnUnresponsiveServer; + public Action? OnUnresponsiveServer; /// /// A list of servers that responded. If you're only interested in servers that responded since you @@ -98,7 +98,7 @@ namespace Steamworks.ServerList return true; } - public virtual void Cancel() => Internal.CancelQuery( request ); + public virtual void Cancel() => Internal?.CancelQuery( request ); // Overrides internal abstract void LaunchQuery(); @@ -117,8 +117,8 @@ namespace Steamworks.ServerList #endregion - internal int Count => Internal.GetServerCount( request ); - internal bool IsRefreshing => request.Value != IntPtr.Zero && Internal.IsRefreshing( request ); + internal int Count => Internal?.GetServerCount( request ) ?? 0; + internal bool IsRefreshing => request.Value != IntPtr.Zero && Internal != null && Internal.IsRefreshing( request ); internal List watchList = new List(); internal int LastCount = 0; @@ -134,7 +134,7 @@ namespace Steamworks.ServerList if ( request.Value != IntPtr.Zero ) { Cancel(); - Internal.ReleaseRequest( request ); + Internal?.ReleaseRequest( request ); request = IntPtr.Zero; } } @@ -166,6 +166,8 @@ namespace Steamworks.ServerList { watchList.RemoveAll( x => { + if (Internal is null) { return true; } + var info = Internal.GetServerDetails( request, x ); if ( info.HadSuccessfulResponse ) { @@ -181,6 +183,8 @@ namespace Steamworks.ServerList { watchList.RemoveAll( x => { + if (Internal is null) { return true; } + var info = Internal.GetServerDetails( request, x ); OnServer( ServerInfo.From( info ), info.HadSuccessfulResponse ); return true; diff --git a/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs b/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs index 6f1ffa3c0..e71394570 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Favourites.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); request = Internal.RequestFavoritesServerList( AppId.Value, ref filters, (uint)filters.Length, IntPtr.Zero ); } diff --git a/Libraries/Facepunch.Steamworks/ServerList/Friends.cs b/Libraries/Facepunch.Steamworks/ServerList/Friends.cs index eb66a692b..60d72e31c 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Friends.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Friends.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); request = Internal.RequestFriendsServerList( AppId.Value, ref filters, (uint)filters.Length, IntPtr.Zero ); } diff --git a/Libraries/Facepunch.Steamworks/ServerList/History.cs b/Libraries/Facepunch.Steamworks/ServerList/History.cs index 55ccc166c..3d059767e 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/History.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/History.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); request = Internal.RequestHistoryServerList( AppId.Value, ref filters, (uint)filters.Length, IntPtr.Zero ); } diff --git a/Libraries/Facepunch.Steamworks/ServerList/Internet.cs b/Libraries/Facepunch.Steamworks/ServerList/Internet.cs index 86e599e66..c493b26fa 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/Internet.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/Internet.cs @@ -10,8 +10,8 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } var filters = GetFilters(); - request = Internal.RequestInternetServerList( AppId.Value, filters, (uint)filters.Length, IntPtr.Zero ); } } diff --git a/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs b/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs index 746887335..ed5d476c8 100644 --- a/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs +++ b/Libraries/Facepunch.Steamworks/ServerList/LocalNetwork.cs @@ -10,6 +10,7 @@ namespace Steamworks.ServerList { internal override void LaunchQuery() { + if (Internal is null) { return; } request = Internal.RequestLANServerList( AppId.Value, IntPtr.Zero ); } } diff --git a/Libraries/Facepunch.Steamworks/SteamApps.cs b/Libraries/Facepunch.Steamworks/SteamApps.cs index c201882b9..80af70a5e 100644 --- a/Libraries/Facepunch.Steamworks/SteamApps.cs +++ b/Libraries/Facepunch.Steamworks/SteamApps.cs @@ -13,7 +13,7 @@ namespace Steamworks /// public class SteamApps : SteamSharedClass { - internal static ISteamApps Internal => Interface as ISteamApps; + internal static ISteamApps? Internal => Interface as ISteamApps; internal override void InitializeInterface( bool server ) { @@ -29,7 +29,7 @@ namespace Steamworks /// /// posted after the user gains ownership of DLC and that DLC is installed /// - public static event Action OnDlcInstalled; + public static event Action? OnDlcInstalled; /// /// posted after the user gains executes a Steam URL with command line or query parameters @@ -37,61 +37,63 @@ namespace Steamworks /// while the game is already running. The new params can be queried /// with GetLaunchQueryParam and GetLaunchCommandLine /// - public static event Action OnNewLaunchParameters; + public static event Action? OnNewLaunchParameters; /// /// Checks if the active user is subscribed to the current App ID /// - public static bool IsSubscribed => Internal.BIsSubscribed(); + public static bool IsSubscribed => Internal != null && Internal.BIsSubscribed(); /// /// Check if user borrowed this game via Family Sharing, If true, call GetAppOwner() to get the lender SteamID /// - public static bool IsSubscribedFromFamilySharing => Internal.BIsSubscribedFromFamilySharing(); + public static bool IsSubscribedFromFamilySharing => Internal != null && Internal.BIsSubscribedFromFamilySharing(); /// /// Checks if the license owned by the user provides low violence depots. /// Low violence depots are useful for copies sold in countries that have content restrictions /// - public static bool IsLowViolence => Internal.BIsLowViolence(); + public static bool IsLowViolence => Internal != null && Internal.BIsLowViolence(); /// /// Checks whether the current App ID license is for Cyber Cafes. /// - public static bool IsCybercafe => Internal.BIsCybercafe(); + public static bool IsCybercafe => Internal != null && Internal.BIsCybercafe(); /// /// CChecks if the user has a VAC ban on their account /// - public static bool IsVACBanned => Internal.BIsVACBanned(); + public static bool IsVACBanned => Internal != null && Internal.BIsVACBanned(); /// /// Gets the current language that the user has set. /// This falls back to the Steam UI language if the user hasn't explicitly picked a language for the title. /// - public static string GameLanguage => Internal.GetCurrentGameLanguage(); + public static string? GameLanguage => Internal?.GetCurrentGameLanguage(); /// /// Gets a list of the languages the current app supports. /// - public static string[] AvailableLanguages => Internal.GetAvailableGameLanguages().Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries ); + public static string[]? AvailableLanguages => Internal?.GetAvailableGameLanguages().Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries ); /// /// Checks if the active user is subscribed to a specified AppId. /// Only use this if you need to check ownership of another game related to yours, a demo for example. /// - public static bool IsSubscribedToApp( AppId appid ) => Internal.BIsSubscribedApp( appid.Value ); + public static bool IsSubscribedToApp( AppId appid ) => Internal != null && Internal.BIsSubscribedApp( appid.Value ); /// /// Checks if the user owns a specific DLC and if the DLC is installed /// - public static bool IsDlcInstalled( AppId appid ) => Internal.BIsDlcInstalled( appid.Value ); + public static bool IsDlcInstalled( AppId appid ) => Internal != null && Internal.BIsDlcInstalled( appid.Value ); /// /// Returns the time of the purchase of the app /// public static DateTime PurchaseTime( AppId appid = default ) { + if (Internal is null) { return default; } + if ( appid == 0 ) appid = SteamClient.AppId; @@ -103,7 +105,7 @@ namespace Steamworks /// This function will return false for users who have a retail or other type of license /// Before using, please ask your Valve technical contact how to package and secure your free weekened ///
- public static bool IsSubscribedFromFreeWeekend => Internal.BIsSubscribedFromFreeWeekend(); + public static bool IsSubscribedFromFreeWeekend => Internal != null && Internal.BIsSubscribedFromFreeWeekend(); /// /// Returns metadata for all available DLC @@ -113,8 +115,12 @@ namespace Steamworks var appid = default( AppId ); var available = false; - for ( int i = 0; i < Internal.GetDLCCount(); i++ ) + if (Internal is null) { yield break; } + + int dlcCount = Internal.GetDLCCount(); + for ( int i = 0; i < dlcCount; i++ ) { + if (Internal is null) { yield break; } if ( !Internal.BGetDLCDataByIndex( i, ref appid, ref available, out var strVal ) ) continue; @@ -130,21 +136,21 @@ namespace Steamworks /// /// Install/Uninstall control for optional DLC /// - public static void InstallDlc( AppId appid ) => Internal.InstallDLC( appid.Value ); + public static void InstallDlc( AppId appid ) => Internal?.InstallDLC( appid.Value ); /// /// Install/Uninstall control for optional DLC /// - public static void UninstallDlc( AppId appid ) => Internal.UninstallDLC( appid.Value ); + public static void UninstallDlc( AppId appid ) => Internal?.UninstallDLC( appid.Value ); /// /// Returns null if we're not on a beta branch, else the name of the branch /// - public static string CurrentBetaName + public static string? CurrentBetaName { get { - if ( !Internal.GetCurrentBetaName( out var strVal ) ) + if ( Internal is null || !Internal.GetCurrentBetaName( out var strVal ) ) return null; return strVal; @@ -157,13 +163,15 @@ namespace Steamworks /// If you detect the game is out-of-date(for example, by having the client detect a version mismatch with a server), /// you can call use MarkContentCorrupt to force a verify, show a message to the user, and then quit. /// - public static void MarkContentCorrupt( bool missingFilesOnly ) => Internal.MarkContentCorrupt( missingFilesOnly ); + public static void MarkContentCorrupt( bool missingFilesOnly ) => Internal?.MarkContentCorrupt( missingFilesOnly ); /// /// Gets a list of all installed depots for a given App ID in mount order /// public static IEnumerable InstalledDepots( AppId appid = default ) { + if (Internal is null) { yield break; } + if ( appid == 0 ) appid = SteamClient.AppId; @@ -180,12 +188,12 @@ namespace Steamworks /// Gets the install folder for a specific AppID. /// This works even if the application is not installed, based on where the game would be installed with the default Steam library location. ///
- public static string AppInstallDir( AppId appid = default ) + public static string? AppInstallDir( AppId appid = default ) { if ( appid == 0 ) appid = SteamClient.AppId; - if ( Internal.GetAppInstallDir( appid.Value, out var strVal ) == 0 ) + if ( Internal is null || Internal.GetAppInstallDir( appid.Value, out var strVal ) == 0 ) return null; return strVal; @@ -194,12 +202,12 @@ namespace Steamworks /// /// The app may not actually be owned by the current user, they may have it left over from a free weekend, etc. /// - public static bool IsAppInstalled( AppId appid ) => Internal.BIsAppInstalled( appid.Value ); + public static bool IsAppInstalled( AppId appid ) => Internal != null && Internal.BIsAppInstalled( appid.Value ); /// /// Gets the Steam ID of the original owner of the current app. If it's different from the current user then it is borrowed.. /// - public static SteamId AppOwner => Internal.GetAppOwner().Value; + public static SteamId AppOwner => Internal?.GetAppOwner().Value ?? default; /// /// Gets the associated launch parameter if the game is run via steam://run/appid/?param1=value1;param2=value2;param3=value3 etc. @@ -207,7 +215,7 @@ namespace Steamworks /// Parameter names starting with an underscore '_' are reserved for steam features -- they can be queried by the game, /// but it is advised that you not param names beginning with an underscore for your own features. /// - public static string GetLaunchParam( string param ) => Internal.GetLaunchQueryParam( param ); + public static string? GetLaunchParam( string param ) => Internal?.GetLaunchQueryParam( param ); /// /// Gets the download progress for optional DLC. @@ -217,7 +225,7 @@ namespace Steamworks ulong punBytesDownloaded = 0; ulong punBytesTotal = 0; - if ( !Internal.GetDlcDownloadProgress( appid.Value, ref punBytesDownloaded, ref punBytesTotal ) ) + if ( Internal is null || !Internal.GetDlcDownloadProgress( appid.Value, ref punBytesDownloaded, ref punBytesTotal ) ) return default; return new DownloadProgress { BytesDownloaded = punBytesDownloaded, BytesTotal = punBytesTotal, Active = true }; @@ -227,7 +235,7 @@ namespace Steamworks /// Gets the buildid of this app, may change at any time based on backend updates to the game. /// Defaults to 0 if you're not running a build downloaded from steam. /// - public static int BuildId => Internal.GetAppBuildId(); + public static int BuildId => Internal?.GetAppBuildId() ?? 0; /// @@ -236,6 +244,7 @@ namespace Steamworks /// public static async Task GetFileDetailsAsync( string filename ) { + if (Internal is null) { return null; } var r = await Internal.GetFileDetails( filename ); if ( !r.HasValue || r.Value.Result != Result.OK ) @@ -257,11 +266,12 @@ namespace Steamworks /// path and not be placed on the OS command line, you must set a value in your app's /// configuration on Steam. Ask Valve for help with this. /// - public static string CommandLine + public static string? CommandLine { get { - Internal.GetLaunchCommandLine( out var strVal ); + string? strVal = null; + Internal?.GetLaunchCommandLine( out strVal ); return strVal; } } diff --git a/Libraries/Facepunch.Steamworks/SteamClient.cs b/Libraries/Facepunch.Steamworks/SteamClient.cs index fb44cff44..31ef55045 100644 --- a/Libraries/Facepunch.Steamworks/SteamClient.cs +++ b/Libraries/Facepunch.Steamworks/SteamClient.cs @@ -124,7 +124,7 @@ namespace Steamworks /// very good experience for the player and you could be preventing them from accessing APIs that do not /// need a live connection to Steam. /// - public static bool IsLoggedOn => SteamUser.Internal.BLoggedOn(); + public static bool IsLoggedOn => SteamUser.Internal != null && SteamUser.Internal.BLoggedOn(); /// /// Gets the Steam ID of the account currently logged into the Steam client. This is @@ -132,18 +132,18 @@ namespace Steamworks /// A Steam ID is a unique identifier for a Steam accounts, Steam groups, Lobbies and Chat /// rooms, and used to differentiate users in all parts of the Steamworks API. /// - public static SteamId SteamId => SteamUser.Internal.GetSteamID(); + public static SteamId SteamId => SteamUser.Internal?.GetSteamID() ?? default; /// /// returns the local players name - guaranteed to not be NULL. /// this is the same name as on the users community profile page /// - public static string Name => SteamFriends.Internal.GetPersonaName(); + public static string? Name => SteamFriends.Internal?.GetPersonaName(); /// /// gets the status of the current user /// - public static FriendState State => SteamFriends.Internal.GetPersonaState(); + public static FriendState State => SteamFriends.Internal?.GetPersonaState() ?? FriendState.Offline; /// /// returns the appID of the current process diff --git a/Libraries/Facepunch.Steamworks/SteamFriends.cs b/Libraries/Facepunch.Steamworks/SteamFriends.cs index 36a57e6cc..bcc457525 100644 --- a/Libraries/Facepunch.Steamworks/SteamFriends.cs +++ b/Libraries/Facepunch.Steamworks/SteamFriends.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamFriends : SteamClientClass { - internal static ISteamFriends Internal => Interface as ISteamFriends; + internal static ISteamFriends? Internal => Interface as ISteamFriends; internal override void InitializeInterface( bool server ) { @@ -23,7 +23,7 @@ namespace Steamworks InstallEvents(); } - static Dictionary richPresence; + static Dictionary? richPresence; internal void InstallEvents() { @@ -40,42 +40,42 @@ namespace Steamworks /// Called when chat message has been received from a friend. You'll need to turn on /// ListenForFriendsMessages to recieve this. (friend, msgtype, message) /// - public static event Action OnChatMessage; + public static event Action? OnChatMessage; /// /// called when a friends' status changes /// - public static event Action OnPersonaStateChange; + public static event Action? OnPersonaStateChange; /// /// Called when the user tries to join a game from their friends list /// rich presence will have been set with the "connect" key which is set here /// - public static event Action OnGameRichPresenceJoinRequested; + public static event Action? OnGameRichPresenceJoinRequested; /// /// Posted when game overlay activates or deactivates /// the game can use this to be pause or resume single player games /// - public static event Action OnGameOverlayActivated; + public static event Action? OnGameOverlayActivated; /// /// Called when the user tries to join a different game server from their friends list /// game client should attempt to connect to specified server when this is received /// - public static event Action OnGameServerChangeRequested; + public static event Action? OnGameServerChangeRequested; /// /// Called when the user tries to join a lobby from their friends list /// game client should attempt to connect to specified lobby when this is received /// - public static event Action OnGameLobbyJoinRequested; + public static event Action? OnGameLobbyJoinRequested; /// /// Callback indicating updated data about friends rich presence information /// - public static event Action OnFriendRichPresenceUpdate; + public static event Action? OnFriendRichPresenceUpdate; static unsafe void OnFriendChatMessage( GameConnectedFriendChatMsg_t data ) { @@ -86,7 +86,7 @@ namespace Steamworks using var buffer = Helpers.TakeMemory(); var type = ChatEntryType.ChatMsg; - var len = Internal.GetFriendMessage( data.SteamIDUser, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type ); + var len = Internal?.GetFriendMessage( data.SteamIDUser, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type ) ?? 0; if ( len == 0 && type == ChatEntryType.Invalid ) return; @@ -99,15 +99,18 @@ namespace Steamworks private static IEnumerable GetFriendsWithFlag(FriendFlags flag) { - for ( int i=0; i GetFriends() @@ -142,24 +145,33 @@ namespace Steamworks public static IEnumerable GetPlayedWith() { - for ( int i = 0; i < Internal.GetCoplayFriendCount(); i++ ) + if (Internal is null) { yield break; } + int friendCount = Internal.GetCoplayFriendCount(); + for ( int i = 0; i < friendCount; i++ ) { + if (Internal is null) { yield break; } yield return new Friend( Internal.GetCoplayFriend( i ) ); } } public static IEnumerable GetFromSource( SteamId steamid ) { - for ( int i = 0; i < Internal.GetFriendCountFromSource( steamid ); i++ ) - { + if (Internal is null) { yield break; } + int friendCount = Internal.GetFriendCountFromSource( steamid ); + for ( int i = 0; i < friendCount; i++ ) + { + if (Internal is null) { yield break; } yield return new Friend( Internal.GetFriendFromSourceByIndex( steamid, i ) ); } } public static IEnumerable GetClans() { - for (int i = 0; i < Internal.GetClanCount(); i++) + if (Internal is null) { yield break; } + int friendCount = Internal.GetClanCount(); + for ( int i = 0; i < friendCount; i++ ) { + if (Internal is null) { yield break; } yield return new Clan( Internal.GetClanByIndex( i ) ); } } @@ -174,7 +186,7 @@ namespace Steamworks /// "stats", /// "achievements". /// - public static void OpenOverlay( string type ) => Internal.ActivateGameOverlay( type ); + public static void OpenOverlay( string type ) => Internal?.ActivateGameOverlay( type ); /// /// "steamid" - Opens the overlay web browser to the specified user or groups profile. @@ -187,35 +199,35 @@ namespace Steamworks /// "friendrequestaccept" - Opens the overlay in minimal mode prompting the user to accept an incoming friend invite. /// "friendrequestignore" - Opens the overlay in minimal mode prompting the user to ignore an incoming friend invite. /// - public static void OpenUserOverlay( SteamId id, string type ) => Internal.ActivateGameOverlayToUser( type, id ); + public static void OpenUserOverlay( SteamId id, string type ) => Internal?.ActivateGameOverlayToUser( type, id ); /// /// Activates the Steam Overlay to the Steam store page for the provided app. /// - public static void OpenStoreOverlay( AppId id ) => Internal.ActivateGameOverlayToStore( id.Value, OverlayToStoreFlag.None ); + public static void OpenStoreOverlay( AppId id ) => Internal?.ActivateGameOverlayToStore( id.Value, OverlayToStoreFlag.None ); /// /// Activates Steam Overlay web browser directly to the specified URL. /// - public static void OpenWebOverlay( string url, bool modal = false ) => Internal.ActivateGameOverlayToWebPage( url, modal ? ActivateGameOverlayToWebPageMode.Modal : ActivateGameOverlayToWebPageMode.Default ); + public static void OpenWebOverlay( string url, bool modal = false ) => Internal?.ActivateGameOverlayToWebPage( url, modal ? ActivateGameOverlayToWebPageMode.Modal : ActivateGameOverlayToWebPageMode.Default ); /// /// Activates the Steam Overlay to open the invite dialog. Invitations sent from this dialog will be for the provided lobby. /// - public static void OpenGameInviteOverlay( SteamId lobby ) => Internal.ActivateGameOverlayInviteDialog( lobby ); + public static void OpenGameInviteOverlay( SteamId lobby ) => Internal?.ActivateGameOverlayInviteDialog( lobby ); /// /// Mark a target user as 'played with'. /// NOTE: The current user must be in game with the other player for the association to work. /// - public static void SetPlayedWith( SteamId steamid ) => Internal.SetPlayedWith( steamid ); + public static void SetPlayedWith( SteamId steamid ) => Internal?.SetPlayedWith( steamid ); /// /// Requests the persona name and optionally the avatar of a specified user. /// NOTE: It's a lot slower to download avatars and churns the local cache, so if you don't need avatars, don't request them. /// returns true if we're fetching the data, false if we already have it /// - public static bool RequestUserInformation( SteamId steamid, bool nameonly = true ) => Internal.RequestUserInformation( steamid, nameonly ); + public static bool RequestUserInformation( SteamId steamid, bool nameonly = true ) => Internal != null && Internal.RequestUserInformation( steamid, nameonly ); internal static async Task CacheUserInformationAsync( SteamId steamid, bool nameonly ) @@ -239,18 +251,21 @@ namespace Steamworks public static async Task GetSmallAvatarAsync( SteamId steamid ) { + if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); return SteamUtils.GetImage( Internal.GetSmallFriendAvatar( steamid ) ); } public static async Task GetMediumAvatarAsync( SteamId steamid ) { + if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); return SteamUtils.GetImage( Internal.GetMediumFriendAvatar( steamid ) ); } public static async Task GetLargeAvatarAsync( SteamId steamid ) { + if (Internal is null) { return null; } await CacheUserInformationAsync( steamid, false ); var imageid = Internal.GetLargeFriendAvatar( steamid ); @@ -268,8 +283,10 @@ namespace Steamworks /// /// Find a rich presence value by key for current user. Will be null if not found. /// - public static string GetRichPresence( string key ) + public static string? GetRichPresence( string key ) { + if (richPresence is null) { return null; } + if ( richPresence.TryGetValue( key, out var val ) ) return val; @@ -281,6 +298,8 @@ namespace Steamworks /// public static bool SetRichPresence( string key, string value ) { + if (richPresence is null || Internal is null) { return false; } + bool success = Internal.SetRichPresence( key, value ); if ( success ) @@ -294,8 +313,8 @@ namespace Steamworks /// public static void ClearRichPresence() { - richPresence.Clear(); - Internal.ClearRichPresence(); + richPresence?.Clear(); + Internal?.ClearRichPresence(); } static bool _listenForFriendsMessages; @@ -312,20 +331,22 @@ namespace Steamworks set { _listenForFriendsMessages = value; - Internal.SetListenForFriendsMessages( value ); + Internal?.SetListenForFriendsMessages( value ); } } public static async Task IsFollowing(SteamId steamID) { + if (Internal is null) { return false; } var r = await Internal.IsFollowing(steamID); - return r.Value.IsFollowing; + return r?.IsFollowing ?? false; } public static async Task GetFollowerCount(SteamId steamID) { + if (Internal is null) { return 0; } var r = await Internal.GetFollowerCount(steamID); - return r.Value.Count; + return r?.Count ?? 0; } public static async Task GetFollowingList() @@ -337,6 +358,7 @@ namespace Steamworks do { + if (Internal is null) { break; } if ( (result = await Internal.EnumerateFollowingList((uint)resultCount)) != null) { resultCount += result.Value.ResultsReturned; diff --git a/Libraries/Facepunch.Steamworks/SteamInput.cs b/Libraries/Facepunch.Steamworks/SteamInput.cs index d55e523c4..3db2293b4 100644 --- a/Libraries/Facepunch.Steamworks/SteamInput.cs +++ b/Libraries/Facepunch.Steamworks/SteamInput.cs @@ -5,7 +5,7 @@ namespace Steamworks { public class SteamInput : SteamClientClass { - internal static ISteamInput Internal => Interface as ISteamInput; + internal static ISteamInput? Internal => Interface as ISteamInput; internal override void InitializeInterface( bool server ) { @@ -22,7 +22,7 @@ namespace Steamworks /// public static void RunFrame() { - Internal.RunFrame(); + Internal?.RunFrame(); } static readonly InputHandle_t[] queryArray = new InputHandle_t[STEAM_CONTROLLER_MAX_COUNT]; @@ -34,7 +34,7 @@ namespace Steamworks { get { - var num = Internal.GetConnectedControllers( queryArray ); + var num = Internal?.GetConnectedControllers( queryArray ) ?? 0; for ( int i = 0; i < num; i++ ) { @@ -52,8 +52,10 @@ namespace Steamworks /// /// /// - public static string GetDigitalActionGlyph( Controller controller, string action ) + public static string? GetDigitalActionGlyph( Controller controller, string action ) { + if (Internal is null) { return null; } + InputActionOrigin origin = InputActionOrigin.None; Internal.GetDigitalActionOrigins( @@ -69,6 +71,8 @@ namespace Steamworks internal static Dictionary DigitalHandles = new Dictionary(); internal static InputDigitalActionHandle_t GetDigitalActionHandle( string name ) { + if (Internal is null) { return default; } + if ( DigitalHandles.TryGetValue( name, out var val ) ) return val; @@ -80,6 +84,8 @@ namespace Steamworks internal static Dictionary AnalogHandles = new Dictionary(); internal static InputAnalogActionHandle_t GetAnalogActionHandle( string name ) { + if (Internal is null) { return default; } + if ( AnalogHandles.TryGetValue( name, out var val ) ) return val; @@ -91,6 +97,8 @@ namespace Steamworks internal static Dictionary ActionSets = new Dictionary(); internal static InputActionSetHandle_t GetActionSetHandle( string name ) { + if (Internal is null) { return default; } + if ( ActionSets.TryGetValue( name, out var val ) ) return val; diff --git a/Libraries/Facepunch.Steamworks/SteamInventory.cs b/Libraries/Facepunch.Steamworks/SteamInventory.cs index f0a63c040..f45131dd1 100644 --- a/Libraries/Facepunch.Steamworks/SteamInventory.cs +++ b/Libraries/Facepunch.Steamworks/SteamInventory.cs @@ -14,7 +14,7 @@ namespace Steamworks /// public class SteamInventory : SteamSharedClass { - internal static ISteamInventory Internal => Interface as ISteamInventory; + internal static ISteamInventory? Internal => Interface as ISteamInventory; internal override void InitializeInterface( bool server ) { @@ -41,8 +41,8 @@ namespace Steamworks OnInventoryUpdated?.Invoke( r ); } - public static event Action OnInventoryUpdated; - public static event Action OnDefinitionsUpdated; + public static event Action? OnInventoryUpdated; + public static event Action? OnDefinitionsUpdated; static void LoadDefinitions() { @@ -79,7 +79,7 @@ namespace Steamworks LoadDefinitions(); } - Internal.LoadItemDefinitions(); + Internal?.LoadItemDefinitions(); } /// @@ -113,7 +113,7 @@ namespace Steamworks /// Try to find the definition that matches this definition ID. /// Uses a dictionary so should be about as fast as possible. /// - public static InventoryDef FindDefinition( InventoryDefId defId ) + public static InventoryDef? FindDefinition( InventoryDefId defId ) { if ( _defMap == null ) return null; @@ -124,15 +124,17 @@ namespace Steamworks return null; } - public static string Currency { get; internal set; } + public static string Currency { get; internal set; } = ""; - public static async Task GetDefinitionsWithPricesAsync() + public static async Task GetDefinitionsWithPricesAsync() { + if (Internal is null) { return null; } + var priceRequest = await Internal.RequestPrices(); - if ( !priceRequest.HasValue || priceRequest.Value.Result != Result.OK ) + if ( priceRequest?.Result != Result.OK ) return null; - Currency = priceRequest?.CurrencyUTF8(); + Currency = priceRequest.Value.CurrencyUTF8(); var num = Internal.GetNumItemsWithPrices(); @@ -153,15 +155,15 @@ namespace Steamworks /// /// We will try to keep this list of your items automatically up to date. /// - public static InventoryItem[] Items { get; internal set; } + public static InventoryItem[]? Items { get; internal set; } - public static InventoryDef[] Definitions { get; internal set; } - static Dictionary _defMap; + public static InventoryDef[]? Definitions { get; internal set; } + static Dictionary? _defMap; - internal static InventoryDef[] GetDefinitions() + internal static InventoryDef[]? GetDefinitions() { uint num = 0; - if ( !Internal.GetItemDefinitionIDs( null, ref num ) ) + if ( Internal is null || !Internal.GetItemDefinitionIDs( null, ref num ) ) return null; var defs = new InventoryDefId[num]; @@ -178,7 +180,7 @@ namespace Steamworks public static bool GetAllItems() { var sresult = Defines.k_SteamInventoryResultInvalid; - return Internal.GetAllItems( ref sresult ); + return Internal != null && Internal.GetAllItems( ref sresult ); } /// @@ -188,7 +190,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.GetAllItems( ref sresult ) ) + if ( Internal is null || !Internal.GetAllItems( ref sresult ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -207,7 +209,7 @@ namespace Steamworks var defs = new InventoryDefId[] { target.Id }; var cnts = new uint[] { (uint)amount }; - if ( !Internal.GenerateItems( ref sresult, defs, cnts, 1 ) ) + if ( Internal is null || !Internal.GenerateItems( ref sresult, defs, cnts, 1 ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -228,7 +230,7 @@ namespace Steamworks var sell = list.Select( x => x.Id ).ToArray(); var sellc = list.Select( x => (uint)1 ).ToArray(); - if ( !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) + if ( Internal is null || !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -249,7 +251,7 @@ namespace Steamworks var sell = list.Select( x => x.Item.Id ).ToArray(); var sellc = list.Select( x => (uint) x.Quantity ).ToArray(); - if ( !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) + if ( Internal is null || !Internal.ExchangeItems( ref sresult, give, givec, 1, sell, sellc, (uint)sell.Length ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -285,7 +287,7 @@ namespace Steamworks var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.DeserializeResult( ref sresult, (IntPtr)ptr, (uint)dataLength, false ) ) + if ( Internal is null || !Internal.DeserializeResult( ref sresult, (IntPtr)ptr, (uint)dataLength, false ) ) return null; @@ -306,7 +308,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.GrantPromoItems( ref sresult ) ) + if ( Internal is null || !Internal.GrantPromoItems( ref sresult ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -320,7 +322,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.TriggerItemDrop( ref sresult, id ) ) + if ( Internal is null || !Internal.TriggerItemDrop( ref sresult, id ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -334,7 +336,7 @@ namespace Steamworks { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !Internal.AddPromoItem( ref sresult, id ) ) + if ( Internal is null || !Internal.AddPromoItem( ref sresult, id ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -347,6 +349,8 @@ namespace Steamworks /// public static async Task StartPurchaseAsync( InventoryDef[] items ) { + if (Internal is null) { return null; } + var item_i = items.Select( x => x._id ).ToArray(); var item_q = items.Select( x => (uint)1 ).ToArray(); diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs index ff3857a6f..df3ac42eb 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmaking.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamMatchmaking : SteamClientClass { - internal static ISteamMatchmaking Internal => Interface as ISteamMatchmaking; + internal static ISteamMatchmaking? Internal => Interface as ISteamMatchmaking; internal override void InitializeInterface( bool server ) { @@ -70,6 +70,8 @@ namespace Steamworks static private unsafe void OnLobbyChatMessageRecievedAPI( LobbyChatMsg_t callback ) { + if (Internal is null) { return; } + SteamId steamid = default; ChatEntryType chatEntryType = default; using var buffer = Helpers.TakeMemory(); @@ -101,62 +103,62 @@ namespace Steamworks /// /// Someone invited you to a lobby /// - public static event Action OnLobbyInvite; + public static event Action? OnLobbyInvite; /// /// You joined a lobby /// - public static event Action OnLobbyEntered; + public static event Action? OnLobbyEntered; /// /// You created a lobby /// - public static event Action OnLobbyCreated; + public static event Action? OnLobbyCreated; /// /// A game server has been associated with the lobby /// - public static event Action OnLobbyGameCreated; + public static event Action? OnLobbyGameCreated; /// /// The lobby metadata has changed /// - public static event Action OnLobbyDataChanged; + public static event Action? OnLobbyDataChanged; /// /// The lobby member metadata has changed /// - public static event Action OnLobbyMemberDataChanged; + public static event Action? OnLobbyMemberDataChanged; /// /// The lobby member joined /// - public static event Action OnLobbyMemberJoined; + public static event Action? OnLobbyMemberJoined; /// /// The lobby member left the room /// - public static event Action OnLobbyMemberLeave; + public static event Action? OnLobbyMemberLeave; /// /// The lobby member left the room /// - public static event Action OnLobbyMemberDisconnected; + public static event Action? OnLobbyMemberDisconnected; /// /// The lobby member was kicked. The 3rd param is the user that kicked them. /// - public static event Action OnLobbyMemberKicked; + public static event Action? OnLobbyMemberKicked; /// /// The lobby member was banned. The 3rd param is the user that banned them. /// - public static event Action OnLobbyMemberBanned; + public static event Action? OnLobbyMemberBanned; /// /// A chat message was recieved from a member of a lobby /// - public static event Action OnChatMessage; + public static event Action? OnChatMessage; public static LobbyQuery CreateLobbyQuery() { return new LobbyQuery(); } @@ -165,6 +167,8 @@ namespace Steamworks /// public static async Task CreateLobbyAsync( int maxMembers = 100 ) { + if (Internal is null) { return null; } + var lobby = await Internal.CreateLobby( LobbyType.Invisible, maxMembers ); if ( !lobby.HasValue ) { return null; } @@ -176,6 +180,8 @@ namespace Steamworks /// public static async Task JoinLobbyAsync( SteamId lobbyId ) { + if (Internal is null) { return null; } + var lobby = await Internal.JoinLobby( lobbyId ); if ( !lobby.HasValue ) return null; @@ -187,6 +193,8 @@ namespace Steamworks /// public static IEnumerable GetFavoriteServers() { + if (Internal is null) { yield break; } + var count = Internal.GetFavoriteGameCount(); for( int i=0; i public static IEnumerable GetHistoryServers() { + if (Internal is null) { yield break; } + var count = Internal.GetFavoriteGameCount(); for ( int i = 0; i < count; i++ ) diff --git a/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs b/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs index c1af07aee..a1d7d2c71 100644 --- a/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs +++ b/Libraries/Facepunch.Steamworks/SteamMatchmakingServers.cs @@ -12,7 +12,7 @@ namespace Steamworks /// internal class SteamMatchmakingServers : SteamClientClass { - internal static ISteamMatchmakingServers Internal => Interface as ISteamMatchmakingServers; + internal static ISteamMatchmakingServers? Internal => Interface as ISteamMatchmakingServers; internal override void InitializeInterface( bool server ) { diff --git a/Libraries/Facepunch.Steamworks/SteamMusic.cs b/Libraries/Facepunch.Steamworks/SteamMusic.cs index 1ef38f8bd..b0d6ef42d 100644 --- a/Libraries/Facepunch.Steamworks/SteamMusic.cs +++ b/Libraries/Facepunch.Steamworks/SteamMusic.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamMusic : SteamClientClass { - internal static ISteamMusic Internal => Interface as ISteamMusic; + internal static ISteamMusic? Internal => Interface as ISteamMusic; internal override void InitializeInterface( bool server ) { @@ -33,50 +33,50 @@ namespace Steamworks /// /// Playback status changed /// - public static event Action OnPlaybackChanged; + public static event Action? OnPlaybackChanged; /// /// Volume changed, parameter is new volume /// - public static event Action OnVolumeChanged; + public static event Action? OnVolumeChanged; /// /// Checks if Steam Music is enabled /// - public static bool IsEnabled => Internal.BIsEnabled(); + public static bool IsEnabled => Internal != null && Internal.BIsEnabled(); /// /// true if a song is currently playing, paused, or queued up to play; otherwise false. /// - public static bool IsPlaying => Internal.BIsPlaying(); + public static bool IsPlaying => Internal != null && Internal.BIsPlaying(); /// /// Gets the current status of the Steam Music player /// - public static MusicStatus Status => Internal.GetPlaybackStatus(); + public static MusicStatus Status => Internal?.GetPlaybackStatus() ?? MusicStatus.Undefined; - public static void Play() => Internal.Play(); + public static void Play() => Internal?.Play(); - public static void Pause() => Internal.Pause(); + public static void Pause() => Internal?.Pause(); /// /// Have the Steam Music player play the previous song. /// - public static void PlayPrevious() => Internal.PlayPrevious(); + public static void PlayPrevious() => Internal?.PlayPrevious(); /// /// Have the Steam Music player skip to the next song /// - public static void PlayNext() => Internal.PlayNext(); + public static void PlayNext() => Internal?.PlayNext(); /// /// Gets/Sets the current volume of the Steam Music player /// public static float Volume { - get => Internal.GetVolume(); - set => Internal.SetVolume( value ); + get => Internal?.GetVolume() ?? 0f; + set => Internal?.SetVolume( value ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamNetworking.cs b/Libraries/Facepunch.Steamworks/SteamNetworking.cs index f211214c4..cadac81dd 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworking.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworking.cs @@ -10,7 +10,7 @@ namespace Steamworks { public class SteamNetworking : SteamSharedClass { - internal static ISteamNetworking Internal => Interface as ISteamNetworking; + internal static ISteamNetworking? Internal => Interface as ISteamNetworking; internal override void InitializeInterface( bool server ) { @@ -35,26 +35,26 @@ namespace Steamworks /// This SteamId wants to send you a message. You should respond by calling AcceptP2PSessionWithUser /// if you want to recieve their messages /// - public static Action OnP2PSessionRequest; + public static Action? OnP2PSessionRequest; /// /// Called when packets can't get through to the specified user. /// All queued packets unsent at this point will be dropped, further attempts /// to send will retry making the connection (but will be dropped if we fail again). /// - public static Action OnP2PConnectionFailed; + public static Action? OnP2PConnectionFailed; /// /// This should be called in response to a OnP2PSessionRequest /// - public static bool AcceptP2PSessionWithUser( SteamId user ) => Internal.AcceptP2PSessionWithUser( user ); + public static bool AcceptP2PSessionWithUser( SteamId user ) => Internal != null && Internal.AcceptP2PSessionWithUser( user ); /// /// Allow or disallow P2P connects to fall back on Steam server relay if direct /// connection or NAT traversal can't be established. Applies to connections /// created after setting or old connections that need to reconnect. /// - public static bool AllowP2PPacketRelay( bool allow ) => Internal.AllowP2PPacketRelay( allow ); + public static bool AllowP2PPacketRelay( bool allow ) => Internal != null && Internal.AllowP2PPacketRelay( allow ); /// /// This should be called when you're done communicating with a user, as this will @@ -62,7 +62,7 @@ namespace Steamworks /// If the remote user tries to send data to you again, a new OnP2PSessionRequest /// callback will be posted /// - public static bool CloseP2PSessionWithUser( SteamId user ) => Internal.CloseP2PSessionWithUser( user ); + public static bool CloseP2PSessionWithUser( SteamId user ) => Internal != null && Internal.CloseP2PSessionWithUser( user ); /// /// Checks if a P2P packet is available to read, and gets the size of the message if there is one. @@ -70,7 +70,7 @@ namespace Steamworks public static bool IsP2PPacketAvailable( int channel = 0 ) { uint _ = 0; - return Internal.IsP2PPacketAvailable( ref _, channel ); + return Internal != null && Internal.IsP2PPacketAvailable( ref _, channel ); } /// @@ -80,7 +80,7 @@ namespace Steamworks { uint size = 0; - if ( !Internal.IsP2PPacketAvailable( ref size, channel ) ) + if ( Internal is null || !Internal.IsP2PPacketAvailable( ref size, channel ) ) return null; var buffer = Helpers.TakeBuffer( (int) size ); @@ -108,7 +108,7 @@ namespace Steamworks public unsafe static bool ReadP2PPacket( byte[] buffer, ref uint size, ref SteamId steamid, int channel = 0 ) { fixed (byte* p = buffer) { - return Internal.ReadP2PPacket( (IntPtr)p, (uint)buffer.Length, ref size, ref steamid, channel ); + return Internal != null && Internal.ReadP2PPacket( (IntPtr)p, (uint)buffer.Length, ref size, ref steamid, channel ); } } @@ -117,7 +117,7 @@ namespace Steamworks /// public unsafe static bool ReadP2PPacket( byte* buffer, uint cbuf, ref uint size, ref SteamId steamid, int channel = 0 ) { - return Internal.ReadP2PPacket( (IntPtr)buffer, cbuf, ref size, ref steamid, channel ); + return Internal != null && Internal.ReadP2PPacket( (IntPtr)buffer, cbuf, ref size, ref steamid, channel ); } /// @@ -132,7 +132,7 @@ namespace Steamworks fixed ( byte* p = data ) { - return Internal.SendP2PPacket( steamid, (IntPtr)p, (uint)length, (P2PSend)sendType, nChannel ); + return Internal != null && Internal.SendP2PPacket( steamid, (IntPtr)p, (uint)length, (P2PSend)sendType, nChannel ); } } @@ -143,13 +143,13 @@ namespace Steamworks /// public static unsafe bool SendP2PPacket( SteamId steamid, byte* data, uint length, int nChannel = 1, P2PSend sendType = P2PSend.Reliable ) { - return Internal.SendP2PPacket( steamid, (IntPtr)data, (uint)length, (P2PSend)sendType, nChannel ); + return Internal != null && Internal.SendP2PPacket( steamid, (IntPtr)data, (uint)length, (P2PSend)sendType, nChannel ); } public static P2PSessionState? GetP2PSessionState( SteamId steamid ) { P2PSessionState_t state = new P2PSessionState_t(); - if (Internal.GetP2PSessionState(steamid, ref state)) + if (Internal != null && Internal.GetP2PSessionState(steamid, ref state)) { return new P2PSessionState(state); } diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs index ea5e983ac..50e80dfd2 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingSockets.cs @@ -10,7 +10,7 @@ namespace Steamworks { public class SteamNetworkingSockets : SteamSharedClass { - internal static ISteamNetworkingSockets Internal => Interface as ISteamNetworkingSockets; + internal static ISteamNetworkingSockets? Internal => Interface as ISteamNetworkingSockets; internal override void InitializeInterface( bool server ) { @@ -22,7 +22,7 @@ namespace Steamworks static readonly Dictionary SocketInterfaces = new Dictionary(); - internal static SocketManager GetSocketManager( uint id ) + internal static SocketManager? GetSocketManager( uint id ) { if ( SocketInterfaces == null ) return null; if ( id == 0 ) throw new System.ArgumentException( "Invalid Socket" ); @@ -43,7 +43,7 @@ namespace Steamworks #region ConnectionInterface static readonly Dictionary ConnectionInterfaces = new Dictionary(); - internal static ConnectionManager GetConnectionManager( uint id ) + internal static ConnectionManager? GetConnectionManager( uint id ) { if ( ConnectionInterfaces == null ) return null; if ( id == 0 ) return null; @@ -88,7 +88,7 @@ namespace Steamworks OnConnectionStatusChanged?.Invoke( data.Conn, data.Nfo ); } - public static event Action OnConnectionStatusChanged; + public static event Action? OnConnectionStatusChanged; /// @@ -98,8 +98,10 @@ namespace Steamworks /// To use this derive a class from SocketManager and override as much as you want. /// /// - public static T CreateNormalSocket( NetAddress address ) where T : SocketManager, new() + public static T? CreateNormalSocket( NetAddress address ) where T : SocketManager, new() { + if (Internal is null) { return null; } + var t = new T(); var options = Array.Empty(); t.Socket = Internal.CreateListenSocketIP( ref address, options.Length, options ); @@ -118,8 +120,10 @@ namespace Steamworks /// will received all the appropriate callbacks. /// /// - public static SocketManager CreateNormalSocket( NetAddress address, ISocketManager intrface ) + public static SocketManager? CreateNormalSocket( NetAddress address, ISocketManager intrface ) { + if (Internal is null) { return null; } + var options = Array.Empty(); var socket = Internal.CreateListenSocketIP( ref address, options.Length, options ); @@ -138,8 +142,10 @@ namespace Steamworks /// /// Connect to a socket created via CreateListenSocketIP /// - public static T ConnectNormal( NetAddress address ) where T : ConnectionManager, new() + public static T? ConnectNormal( NetAddress address ) where T : ConnectionManager, new() { + if (Internal is null) { return null; } + var t = new T(); var options = Array.Empty(); t.Connection = Internal.ConnectByIPAddress( ref address, options.Length, options ); @@ -150,8 +156,10 @@ namespace Steamworks /// /// Connect to a socket created via CreateListenSocketIP /// - public static ConnectionManager ConnectNormal( NetAddress address, IConnectionManager iface ) + public static ConnectionManager? ConnectNormal( NetAddress address, IConnectionManager iface ) { + if (Internal is null) { return null; } + var options = Array.Empty(); var connection = Internal.ConnectByIPAddress( ref address, options.Length, options ); @@ -171,8 +179,10 @@ namespace Steamworks /// To use this derive a class from SocketManager and override as much as you want. /// /// - public static T CreateRelaySocket( int virtualport = 0 ) where T : SocketManager, new() + public static T? CreateRelaySocket( int virtualport = 0 ) where T : SocketManager, new() { + if (Internal is null) { return null; } + var t = new T(); var options = Array.Empty(); t.Socket = Internal.CreateListenSocketP2P( virtualport, options.Length, options ); @@ -189,8 +199,10 @@ namespace Steamworks /// will received all the appropriate callbacks. /// /// - public static SocketManager CreateRelaySocket( int virtualport, ISocketManager intrface ) + public static SocketManager? CreateRelaySocket( int virtualport, ISocketManager intrface ) { + if (Internal is null) { return null; } + var options = Array.Empty(); var socket = Internal.CreateListenSocketP2P( virtualport, options.Length, options ); @@ -209,8 +221,10 @@ namespace Steamworks /// /// Connect to a relay server /// - public static T ConnectRelay( SteamId serverId, int virtualport = 0 ) where T : ConnectionManager, new() + public static T? ConnectRelay( SteamId serverId, int virtualport = 0 ) where T : ConnectionManager, new() { + if (Internal is null) { return null; } + var t = new T(); NetIdentity identity = serverId; var options = Array.Empty(); diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs index e78c07bf0..319e5d641 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamNetworkingUtils : SteamSharedClass { - internal static ISteamNetworkingUtils Internal => Interface as ISteamNetworkingUtils; + internal static ISteamNetworkingUtils? Internal => Interface as ISteamNetworkingUtils; internal override void InitializeInterface( bool server ) { @@ -43,7 +43,7 @@ namespace Steamworks /// and your frame rate will tank and you won't know why. /// - public static event Action OnDebugOutput; + public static event Action? OnDebugOutput; public struct SteamRelayNetworkStatus { @@ -82,7 +82,7 @@ namespace Steamworks /// public static void InitRelayNetworkAccess() { - Internal.InitRelayNetworkAccess(); + Internal?.InitRelayNetworkAccess(); } /// @@ -98,6 +98,8 @@ namespace Steamworks { get { + if (Internal is null) { return null; } + NetPingLocation location = default; var age = Internal.GetLocalPingLocation( ref location ); if ( age < 0 ) @@ -114,7 +116,7 @@ namespace Steamworks /// public static int EstimatePingTo( NetPingLocation target ) { - return Internal.EstimatePingTimeFromLocalHost( ref target ); + return Internal?.EstimatePingTimeFromLocalHost( ref target ) ?? 0; } /// @@ -124,7 +126,7 @@ namespace Steamworks public static async Task WaitForPingDataAsync( float maxAgeInSeconds = 60 * 5 ) { await Task.Yield(); - if ( Internal.CheckPingDataUpToDate( maxAgeInSeconds ) ) + if ( Internal is null || Internal.CheckPingDataUpToDate( maxAgeInSeconds ) ) return; SteamRelayNetworkStatus_t status = default; @@ -135,7 +137,7 @@ namespace Steamworks } } - public static long LocalTimestamp => Internal.GetLocalTimestamp(); + public static long LocalTimestamp => Internal?.GetLocalTimestamp() ?? 0; /// @@ -223,7 +225,7 @@ namespace Steamworks _debugLevel = value; _debugFunc = new NetDebugFunc( OnDebugMessage ); - Internal.SetDebugOutputFunction( value, _debugFunc ); + Internal?.SetDebugOutputFunction( value, _debugFunc ); } } @@ -235,7 +237,7 @@ namespace Steamworks /// /// We need to keep the delegate around until it's not used anymore /// - static NetDebugFunc _debugFunc; + static NetDebugFunc? _debugFunc; struct DebugMessage { @@ -274,7 +276,7 @@ namespace Steamworks internal unsafe static bool SetConfigInt( NetConfig type, int value ) { int* ptr = &value; - return Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Int32, (IntPtr)ptr ); + return Internal != null && Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Int32, (IntPtr)ptr ); } internal unsafe static int GetConfigInt( NetConfig type ) @@ -283,7 +285,7 @@ namespace Steamworks NetConfigType dtype = NetConfigType.Int32; int* ptr = &value; UIntPtr size = new UIntPtr( sizeof( int ) ); - var result = Internal.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr) ptr, ref size ); + var result = Internal?.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr) ptr, ref size ); if ( result != NetConfigResult.OK ) return 0; @@ -293,7 +295,7 @@ namespace Steamworks internal unsafe static bool SetConfigFloat( NetConfig type, float value ) { float* ptr = &value; - return Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Float, (IntPtr)ptr ); + return Internal != null && Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.Float, (IntPtr)ptr ); } internal unsafe static float GetConfigFloat( NetConfig type ) @@ -302,7 +304,7 @@ namespace Steamworks NetConfigType dtype = NetConfigType.Float; float* ptr = &value; UIntPtr size = new UIntPtr( sizeof( float ) ); - var result = Internal.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr)ptr, ref size ); + var result = Internal?.GetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, ref dtype, (IntPtr)ptr, ref size ); if ( result != NetConfigResult.OK ) return 0; @@ -315,7 +317,7 @@ namespace Steamworks fixed ( byte* ptr = bytes ) { - return Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.String, (IntPtr)ptr ); + return Internal != null && Internal.SetConfigValue( type, NetConfigScope.Global, IntPtr.Zero, NetConfigType.String, (IntPtr)ptr ); } } diff --git a/Libraries/Facepunch.Steamworks/SteamParental.cs b/Libraries/Facepunch.Steamworks/SteamParental.cs index b746ca3b6..7f3a23bd6 100644 --- a/Libraries/Facepunch.Steamworks/SteamParental.cs +++ b/Libraries/Facepunch.Steamworks/SteamParental.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamParental : SteamSharedClass { - internal static ISteamParentalSettings Internal => Interface as ISteamParentalSettings; + internal static ISteamParentalSettings? Internal => Interface as ISteamParentalSettings; internal override void InitializeInterface( bool server ) { @@ -28,37 +28,37 @@ namespace Steamworks /// /// Parental Settings Changed /// - public static event Action OnSettingsChanged; + public static event Action? OnSettingsChanged; /// /// /// - public static bool IsParentalLockEnabled => Internal.BIsParentalLockEnabled(); + public static bool IsParentalLockEnabled => Internal != null && Internal.BIsParentalLockEnabled(); /// /// /// - public static bool IsParentalLockLocked => Internal.BIsParentalLockLocked(); + public static bool IsParentalLockLocked => Internal != null && Internal.BIsParentalLockLocked(); /// /// /// - public static bool IsAppBlocked( AppId app ) => Internal.BIsAppBlocked( app.Value ); + public static bool IsAppBlocked( AppId app ) => Internal != null && Internal.BIsAppBlocked( app.Value ); /// /// /// - public static bool BIsAppInBlockList( AppId app ) => Internal.BIsAppInBlockList( app.Value ); + public static bool BIsAppInBlockList( AppId app ) => Internal != null && Internal.BIsAppInBlockList( app.Value ); /// /// /// - public static bool IsFeatureBlocked( ParentalFeature feature ) => Internal.BIsFeatureBlocked( feature ); + public static bool IsFeatureBlocked( ParentalFeature feature ) => Internal != null && Internal.BIsFeatureBlocked( feature ); /// /// /// - public static bool BIsFeatureInBlockList( ParentalFeature feature ) => Internal.BIsFeatureInBlockList( feature ); + public static bool BIsFeatureInBlockList( ParentalFeature feature ) => Internal != null && Internal.BIsFeatureInBlockList( feature ); } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamParties.cs b/Libraries/Facepunch.Steamworks/SteamParties.cs index aef57bb69..1d7c1eba3 100644 --- a/Libraries/Facepunch.Steamworks/SteamParties.cs +++ b/Libraries/Facepunch.Steamworks/SteamParties.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamParties : SteamClientClass { - internal static ISteamParties Internal => Interface as ISteamParties; + internal static ISteamParties? Internal => Interface as ISteamParties; internal override void InitializeInterface( bool server ) { @@ -32,15 +32,15 @@ namespace Steamworks /// /// The list of possible Party beacon locations has changed /// - public static event Action OnBeaconLocationsUpdated; + public static event Action? OnBeaconLocationsUpdated; /// /// The list of active beacons may have changed /// - public static event Action OnActiveBeaconsUpdated; + public static event Action? OnActiveBeaconsUpdated; - public static int ActiveBeaconCount => (int) Internal.GetNumActiveBeacons(); + public static int ActiveBeaconCount => (int)(Internal?.GetNumActiveBeacons() ?? 0); public static IEnumerable ActiveBeacons { @@ -50,7 +50,7 @@ namespace Steamworks { yield return new PartyBeacon { - Id = Internal.GetBeaconByIndex( i ) + Id = Internal?.GetBeaconByIndex( i ) ?? 0 }; } } diff --git a/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs b/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs index 48a1945d2..502d37291 100644 --- a/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs +++ b/Libraries/Facepunch.Steamworks/SteamRemotePlay.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamRemotePlay : SteamClientClass { - internal static ISteamRemotePlay Internal => Interface as ISteamRemotePlay; + internal static ISteamRemotePlay? Internal => Interface as ISteamRemotePlay; internal override void InitializeInterface( bool server ) { @@ -30,29 +30,29 @@ namespace Steamworks /// /// Called when a session is connected /// - public static event Action OnSessionConnected; + public static event Action? OnSessionConnected; /// /// Called when a session becomes disconnected /// - public static event Action OnSessionDisconnected; + public static event Action? OnSessionDisconnected; /// /// Get the number of currently connected Steam Remote Play sessions /// - public static int SessionCount => (int) Internal.GetSessionCount(); + public static int SessionCount => (int)(Internal?.GetSessionCount() ?? 0); /// /// Get the currently connected Steam Remote Play session ID at the specified index. /// IsValid will return false if it's out of bounds /// - public static RemotePlaySession GetSession( int index ) => (RemotePlaySession) Internal.GetSessionID( index ).Value; + public static RemotePlaySession GetSession( int index ) => Internal?.GetSessionID( index ).Value ?? default; /// /// Invite a friend to Remote Play Together /// This returns false if the invite can't be sent /// - public static bool SendInvite( SteamId steamid ) => Internal.BSendRemotePlayTogetherInvite( steamid ); + public static bool SendInvite( SteamId steamid ) => Internal != null && Internal.BSendRemotePlayTogetherInvite( steamid ); } } diff --git a/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs b/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs index bfb9a0a6b..847147680 100644 --- a/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs +++ b/Libraries/Facepunch.Steamworks/SteamRemoteStorage.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamRemoteStorage : SteamClientClass { - internal static ISteamRemoteStorage Internal => Interface as ISteamRemoteStorage; + internal static ISteamRemoteStorage? Internal => Interface as ISteamRemoteStorage; internal override void InitializeInterface( bool server ) { @@ -28,15 +28,17 @@ namespace Steamworks { fixed ( byte* ptr = data ) { - return Internal.FileWrite( filename, (IntPtr) ptr, data.Length ); + return Internal != null && Internal.FileWrite( filename, (IntPtr) ptr, data.Length ); } } /// /// Opens a binary file, reads the contents of the file into a byte array, and then closes the file. /// - public unsafe static byte[] FileRead( string filename ) + public unsafe static byte[]? FileRead( string filename ) { + if (Internal is null) { return null; } + var size = FileSize( filename ); if ( size <= 0 ) return null; var buffer = new byte[size]; @@ -51,32 +53,32 @@ namespace Steamworks /// /// Checks whether the specified file exists. /// - public static bool FileExists( string filename ) => Internal.FileExists( filename ); + public static bool FileExists( string filename ) => Internal != null && Internal.FileExists( filename ); /// /// Checks if a specific file is persisted in the steam cloud. /// - public static bool FilePersisted( string filename ) => Internal.FilePersisted( filename ); + public static bool FilePersisted( string filename ) => Internal != null && Internal.FilePersisted( filename ); /// /// Gets the specified file's last modified date/time. /// - public static DateTime FileTime( string filename ) => Epoch.ToDateTime( Internal.GetFileTimestamp( filename ) ); + public static DateTime FileTime( string filename ) => Internal != null ? Epoch.ToDateTime( Internal.GetFileTimestamp( filename ) ) : default; /// /// Gets the specified files size in bytes. 0 if not exists. /// - public static int FileSize( string filename ) => Internal.GetFileSize( filename ); + public static int FileSize( string filename ) => Internal?.GetFileSize( filename ) ?? 0; /// /// Deletes the file from remote storage, but leaves it on the local disk and remains accessible from the API. /// - public static bool FileForget( string filename ) => Internal.FileForget( filename ); + public static bool FileForget( string filename ) => Internal != null && Internal.FileForget( filename ); /// /// Deletes a file from the local disk, and propagates that delete to the cloud. /// - public static bool FileDelete( string filename ) => Internal.FileDelete( filename ); + public static bool FileDelete( string filename ) => Internal != null && Internal.FileDelete( filename ); /// @@ -87,7 +89,7 @@ namespace Steamworks get { ulong t = 0, a = 0; - Internal.GetQuota( ref t, ref a ); + Internal?.GetQuota( ref t, ref a ); return t; } } @@ -100,7 +102,7 @@ namespace Steamworks get { ulong t = 0, a = 0; - Internal.GetQuota( ref t, ref a ); + Internal?.GetQuota( ref t, ref a ); return t - a; } } @@ -113,7 +115,7 @@ namespace Steamworks get { ulong t = 0, a = 0; - Internal.GetQuota( ref t, ref a ); + Internal?.GetQuota( ref t, ref a ); return a; } } @@ -127,7 +129,7 @@ namespace Steamworks /// Checks if the account wide Steam Cloud setting is enabled for this user /// or if they disabled it in the Settings->Cloud dialog. /// - public static bool IsCloudEnabledForAccount => Internal.IsCloudEnabledForAccount(); + public static bool IsCloudEnabledForAccount => Internal != null && Internal.IsCloudEnabledForAccount(); /// /// Checks if the per game Steam Cloud setting is enabled for this user @@ -139,14 +141,14 @@ namespace Steamworks /// public static bool IsCloudEnabledForApp { - get => Internal.IsCloudEnabledForApp(); - set => Internal.SetCloudEnabledForApp( value ); + get => Internal != null && Internal.IsCloudEnabledForApp(); + set => Internal?.SetCloudEnabledForApp( value ); } /// /// Gets the total number of local files synchronized by Steam Cloud. /// - public static int FileCount => Internal.GetFileCount(); + public static int FileCount => Internal?.GetFileCount() ?? 0; public struct RemoteFile { @@ -155,7 +157,7 @@ namespace Steamworks public bool Delete() { - return Internal.FileDelete(Filename); + return Internal != null && Internal.FileDelete(Filename); } } @@ -167,6 +169,8 @@ namespace Steamworks get { var ret = new List(); + if (Internal is null) { return ret; } + int count = FileCount; for( int i=0; i public class SteamScreenshots : SteamClientClass { - internal static ISteamScreenshots Internal => Interface as ISteamScreenshots; + internal static ISteamScreenshots? Internal => Interface as ISteamScreenshots; internal override void InitializeInterface( bool server ) { @@ -37,17 +37,17 @@ namespace Steamworks /// This will only be called if Hooked is true, in which case Steam /// will not take the screenshot itself. /// - public static event Action OnScreenshotRequested; + public static event Action? OnScreenshotRequested; /// /// A screenshot successfully written or otherwise added to the library and can now be tagged. /// - public static event Action OnScreenshotReady; + public static event Action? OnScreenshotReady; /// /// A screenshot attempt failed /// - public static event Action OnScreenshotFailed; + public static event Action? OnScreenshotFailed; /// /// Writes a screenshot to the user's screenshot library given the raw image data, which must be in RGB format. @@ -55,6 +55,8 @@ namespace Steamworks /// public unsafe static Screenshot? WriteScreenshot( byte[] data, int width, int height ) { + if (Internal is null) { return null; } + fixed ( byte* ptr = data ) { var handle = Internal.WriteScreenshot( (IntPtr)ptr, (uint)data.Length, width, height ); @@ -72,6 +74,8 @@ namespace Steamworks /// public unsafe static Screenshot? AddScreenshot( string filename, string thumbnail, int width, int height ) { + if (Internal is null) { return null; } + var handle = Internal.AddScreenshotToLibrary( filename, thumbnail, width, height ); if ( handle.Value == 0 ) return null; @@ -83,7 +87,7 @@ namespace Steamworks /// If screenshots are being hooked by the game then a /// ScreenshotRequested callback is sent back to the game instead. /// - public static void TriggerScreenshot() => Internal.TriggerScreenshot(); + public static void TriggerScreenshot() => Internal?.TriggerScreenshot(); /// /// Toggles whether the overlay handles screenshots when the user presses the screenshot hotkey, or if the game handles them. @@ -93,8 +97,8 @@ namespace Steamworks /// public static bool Hooked { - get => Internal.IsScreenshotsHooked(); - set => Internal.HookScreenshots( value ); + get => Internal != null && Internal.IsScreenshotsHooked(); + set => Internal?.HookScreenshots( value ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamServer.cs b/Libraries/Facepunch.Steamworks/SteamServer.cs index 1b1559d45..b3ea1bef5 100644 --- a/Libraries/Facepunch.Steamworks/SteamServer.cs +++ b/Libraries/Facepunch.Steamworks/SteamServer.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Steamworks.Data; +using System.Diagnostics.CodeAnalysis; namespace Steamworks { @@ -12,7 +13,7 @@ namespace Steamworks /// public partial class SteamServer : SteamServerClass { - internal static ISteamGameServer Internal => Interface as ISteamGameServer; + internal static ISteamGameServer? Internal => Interface as ISteamGameServer; internal override void InitializeInterface( bool server ) { @@ -34,28 +35,28 @@ namespace Steamworks /// /// User has been authed or rejected /// - public static event Action OnValidateAuthTicketResponse; + public static event Action? OnValidateAuthTicketResponse; /// /// Called when a connections to the Steam back-end has been established. /// This means the server now is logged on and has a working connection to the Steam master server. /// - public static event Action OnSteamServersConnected; + public static event Action? OnSteamServersConnected; /// /// This will occur periodically if the Steam client is not connected, and has failed when retrying to establish a connection (result, stilltrying) /// - public static event Action OnSteamServerConnectFailure; + public static event Action? OnSteamServerConnectFailure; /// /// Disconnected from Steam /// - public static event Action OnSteamServersDisconnected; + public static event Action? OnSteamServersDisconnected; /// /// Called when authentication status changes, useful for grabbing SteamId once aavailability is current /// - public static event Action OnSteamNetAuthenticationStatus; + public static event Action? OnSteamNetAuthenticationStatus; /// @@ -172,7 +173,7 @@ namespace Steamworks public static bool DedicatedServer { get => _dedicatedServer; - set { if ( _dedicatedServer == value ) return; Internal.SetDedicatedServer( value ); _dedicatedServer = value; } + set { if ( _dedicatedServer == value ) return; Internal?.SetDedicatedServer( value ); _dedicatedServer = value; } } private static bool _dedicatedServer; @@ -183,7 +184,7 @@ namespace Steamworks public static int MaxPlayers { get => _maxplayers; - set { if ( _maxplayers == value ) return; Internal.SetMaxPlayerCount( value ); _maxplayers = value; } + set { if ( _maxplayers == value ) return; Internal?.SetMaxPlayerCount( value ); _maxplayers = value; } } private static int _maxplayers = 0; @@ -194,7 +195,7 @@ namespace Steamworks public static int BotCount { get => _botcount; - set { if ( _botcount == value ) return; Internal.SetBotPlayerCount( value ); _botcount = value; } + set { if ( _botcount == value ) return; Internal?.SetBotPlayerCount( value ); _botcount = value; } } private static int _botcount = 0; @@ -204,9 +205,9 @@ namespace Steamworks public static string MapName { get => _mapname; - set { if ( _mapname == value ) return; Internal.SetMapName( value ); _mapname = value; } + set { if ( _mapname == value ) return; Internal?.SetMapName( value ); _mapname = value; } } - private static string _mapname; + private static string _mapname = ""; /// /// Gets or sets the current ModDir @@ -214,7 +215,7 @@ namespace Steamworks public static string ModDir { get => _modDir; - internal set { if ( _modDir == value ) return; Internal.SetModDir( value ); _modDir = value; } + internal set { if ( _modDir == value ) return; Internal?.SetModDir( value ); _modDir = value; } } private static string _modDir = ""; @@ -224,7 +225,7 @@ namespace Steamworks public static string Product { get => _product; - internal set { if ( _product == value ) return; Internal.SetProduct( value ); _product = value; } + internal set { if ( _product == value ) return; Internal?.SetProduct( value ); _product = value; } } private static string _product = ""; @@ -234,7 +235,7 @@ namespace Steamworks public static string GameDescription { get => _gameDescription; - internal set { if ( _gameDescription == value ) return; Internal.SetGameDescription( value ); _gameDescription = value; } + internal set { if ( _gameDescription == value ) return; Internal?.SetGameDescription( value ); _gameDescription = value; } } private static string _gameDescription = ""; @@ -244,7 +245,7 @@ namespace Steamworks public static string ServerName { get => _serverName; - set { if ( _serverName == value ) return; Internal.SetServerName( value ); _serverName = value; } + set { if ( _serverName == value ) return; Internal?.SetServerName( value ); _serverName = value; } } private static string _serverName = ""; @@ -254,7 +255,7 @@ namespace Steamworks public static bool Passworded { get => _passworded; - set { if ( _passworded == value ) return; Internal.SetPasswordProtected( value ); _passworded = value; } + set { if ( _passworded == value ) return; Internal?.SetPasswordProtected( value ); _passworded = value; } } private static bool _passworded; @@ -268,20 +269,20 @@ namespace Steamworks set { if ( _gametags == value ) return; - Internal.SetGameTags( value ); + Internal?.SetGameTags( value ); _gametags = value; } } private static string _gametags = ""; - public static SteamId SteamId => Internal.GetSteamID(); + public static SteamId SteamId => Internal?.GetSteamID() ?? default; /// /// Log onto Steam anonymously. /// public static void LogOnAnonymous() { - Internal.LogOnAnonymous(); + Internal?.LogOnAnonymous(); ForceHeartbeat(); } @@ -290,21 +291,21 @@ namespace Steamworks /// public static void LogOff() { - Internal.LogOff(); + Internal?.LogOff(); } /// /// Returns true if the server is connected and registered with the Steam master server /// You should have called LogOnAnonymous etc on startup. /// - public static bool LoggedOn => Internal.BLoggedOn(); + public static bool LoggedOn => Internal != null && Internal.BLoggedOn(); /// /// To the best of its ability this tries to get the server's /// current public ip address. Be aware that this is likely to return /// null for the first few seconds after initialization. /// - public static System.Net.IPAddress PublicIp => Internal.GetPublicIP(); + public static System.Net.IPAddress? PublicIp => Internal?.GetPublicIP(); /// /// Enable or disable heartbeats, which are sent regularly to the master server. @@ -312,7 +313,7 @@ namespace Steamworks /// public static bool AutomaticHeartbeats { - set { Internal.EnableHeartbeats( value ); } + set { Internal?.EnableHeartbeats( value ); } } /// @@ -321,7 +322,7 @@ namespace Steamworks /// public static int AutomaticHeartbeatRate { - set { Internal.SetHeartbeatInterval( value ); } + set { Internal?.SetHeartbeatInterval( value ); } } /// @@ -330,7 +331,7 @@ namespace Steamworks /// public static void ForceHeartbeat() { - Internal.ForceHeartbeat(); + Internal?.ForceHeartbeat(); } /// @@ -340,7 +341,7 @@ namespace Steamworks /// public static void UpdatePlayer( SteamId steamid, string name, int score ) { - Internal.BUpdateUserData( steamid, name, (uint)score ); + Internal?.BUpdateUserData( steamid, name, (uint)score ); } static Dictionary KeyValue = new Dictionary(); @@ -353,6 +354,8 @@ namespace Steamworks /// public static void SetKey( string Key, string Value ) { + if (Internal is null) { return; } + if ( KeyValue.ContainsKey( Key ) ) { if ( KeyValue[Key] == Value ) @@ -374,7 +377,7 @@ namespace Steamworks public static void ClearKeys() { KeyValue.Clear(); - Internal.ClearAllKeyValues(); + Internal?.ClearAllKeyValues(); } /// @@ -382,6 +385,7 @@ namespace Steamworks /// public static unsafe BeginAuthResult BeginAuthSession( byte[] data, SteamId steamid ) { + if (Internal is null) { return BeginAuthResult.ServerNotConnectedToSteam; } fixed ( byte* p = data ) { var result = Internal.BeginAuthSession( (IntPtr)p, data.Length, steamid ); @@ -395,7 +399,7 @@ namespace Steamworks /// public static void EndSession( SteamId steamid ) { - Internal.EndAuthSession( steamid ); + Internal?.EndAuthSession( steamid ); } /// @@ -406,6 +410,12 @@ namespace Steamworks /// True if we want to send a packet public static unsafe bool GetOutgoingPacket( out OutgoingPacket packet ) { + if (Internal is null) + { + packet = default; + return false; + } + var buffer = Helpers.TakeBuffer( 1024 * 32 ); packet = new OutgoingPacket(); @@ -442,7 +452,7 @@ namespace Steamworks /// public static unsafe void HandleIncomingPacket( IntPtr ptr, int size, uint address, ushort port ) { - Internal.HandleIncomingPacket( ptr, size, address, port ); + Internal?.HandleIncomingPacket( ptr, size, address, port ); } /// @@ -450,7 +460,7 @@ namespace Steamworks /// public static UserHasLicenseForAppResult UserHasLicenseForApp( SteamId steamid, AppId appid ) { - return Internal.UserHasLicenseForApp( steamid, appid ); + return Internal?.UserHasLicenseForApp( steamid, appid ) ?? UserHasLicenseForAppResult.NoAuth; } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamServerStats.cs b/Libraries/Facepunch.Steamworks/SteamServerStats.cs index 2de7b0f14..823a92771 100644 --- a/Libraries/Facepunch.Steamworks/SteamServerStats.cs +++ b/Libraries/Facepunch.Steamworks/SteamServerStats.cs @@ -9,7 +9,7 @@ namespace Steamworks { public class SteamServerStats : SteamServerClass { - internal static ISteamGameServerStats Internal => Interface as ISteamGameServerStats; + internal static ISteamGameServerStats? Internal => Interface as ISteamGameServerStats; internal override void InitializeInterface( bool server ) { @@ -24,6 +24,7 @@ namespace Steamworks /// public static async Task RequestUserStatsAsync( SteamId steamid ) { + if (Internal is null) { return Result.Fail; } var r = await Internal.RequestUserStats( steamid ); if ( !r.HasValue ) return Result.Fail; return r.Value.Result; @@ -35,7 +36,7 @@ namespace Steamworks /// public static bool SetInt( SteamId steamid, string name, int stat ) { - return Internal.SetUserStat( steamid, name, stat ); + return Internal != null && Internal.SetUserStat( steamid, name, stat ); } /// @@ -44,7 +45,7 @@ namespace Steamworks /// public static bool SetFloat( SteamId steamid, string name, float stat ) { - return Internal.SetUserStat( steamid, name, stat ); + return Internal != null && Internal.SetUserStat( steamid, name, stat ); } /// @@ -56,7 +57,7 @@ namespace Steamworks { int data = defaultValue; - if ( !Internal.GetUserStat( steamid, name, ref data ) ) + if ( Internal is null || !Internal.GetUserStat( steamid, name, ref data ) ) return defaultValue; return data; @@ -71,7 +72,7 @@ namespace Steamworks { float data = defaultValue; - if ( !Internal.GetUserStat( steamid, name, ref data ) ) + if ( Internal is null || !Internal.GetUserStat( steamid, name, ref data ) ) return defaultValue; return data; @@ -83,7 +84,7 @@ namespace Steamworks /// public static bool SetAchievement( SteamId steamid, string name ) { - return Internal.SetUserAchievement( steamid, name ); + return Internal != null && Internal.SetUserAchievement( steamid, name ); } /// @@ -92,7 +93,7 @@ namespace Steamworks /// public static bool ClearAchievement( SteamId steamid, string name ) { - return Internal.ClearUserAchievement( steamid, name ); + return Internal != null && Internal.ClearUserAchievement( steamid, name ); } /// @@ -102,7 +103,7 @@ namespace Steamworks { bool achieved = false; - if ( !Internal.GetUserAchievement( steamid, name, ref achieved ) ) + if ( Internal is null || !Internal.GetUserAchievement( steamid, name, ref achieved ) ) return false; return achieved; @@ -115,6 +116,7 @@ namespace Steamworks /// public static async Task StoreUserStats( SteamId steamid ) { + if (Internal is null) { return Result.Fail; } var r = await Internal.StoreUserStats( steamid ); if ( !r.HasValue ) return Result.Fail; return r.Value.Result; diff --git a/Libraries/Facepunch.Steamworks/SteamUgc.cs b/Libraries/Facepunch.Steamworks/SteamUgc.cs index 21c51a991..ecf20286e 100644 --- a/Libraries/Facepunch.Steamworks/SteamUgc.cs +++ b/Libraries/Facepunch.Steamworks/SteamUgc.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamUGC : SteamSharedClass { - internal static ISteamUGC Internal => Interface as ISteamUGC; + internal static ISteamUGC? Internal => Interface as ISteamUGC; internal override void InitializeInterface( bool server ) { @@ -44,10 +44,11 @@ namespace Steamworks /// /// Posted after Download call /// - public static event Action OnDownloadItemResult; + public static event Action? OnDownloadItemResult; public static async Task DeleteFileAsync( PublishedFileId fileId ) { + if (Internal is null) { return false; } var r = await Internal.DeleteItem( fileId ); return r?.Result == Result.OK; } @@ -60,7 +61,7 @@ namespace Steamworks /// true if nothing went wrong and the download is started public static bool Download( PublishedFileId fileId, bool highPriority = false ) { - return Internal.DownloadItem( fileId, highPriority ); + return Internal != null && Internal.DownloadItem( fileId, highPriority ); } /// @@ -73,7 +74,7 @@ namespace Steamworks /// true if downloaded and installed correctly public static async Task DownloadAsync( PublishedFileId fileId, - Action progress = null, + Action? progress = null, int millisecondsUpdateDelay = 60, CancellationToken? ct = null) { @@ -163,28 +164,32 @@ namespace Steamworks public static async Task StartPlaytimeTracking(PublishedFileId fileId) { + if (Internal is null) { return false; } var result = await Internal.StartPlaytimeTracking(new[] {fileId}, 1); - return result.Value.Result == Result.OK; + return result?.Result == Result.OK; } public static async Task StopPlaytimeTracking(PublishedFileId fileId) { + if (Internal is null) { return false; } var result = await Internal.StopPlaytimeTracking(new[] {fileId}, 1); - return result.Value.Result == Result.OK; + return result?.Result == Result.OK; } public static async Task StopPlaytimeTrackingForAllItems() { + if (Internal is null) { return false; } var result = await Internal.StopPlaytimeTrackingForAllItems(); - return result.Value.Result == Result.OK; + return result?.Result == Result.OK; } - public static Action GlobalOnItemInstalled; + public static Action? GlobalOnItemInstalled; - public static uint NumSubscribedItems { get { return Internal.GetNumSubscribedItems(); } } + public static uint NumSubscribedItems { get { return Internal?.GetNumSubscribedItems() ?? 0; } } public static PublishedFileId[] GetSubscribedItems() { + if (Internal is null) { return Array.Empty(); } uint numSubscribed = NumSubscribedItems; PublishedFileId[] ids = new PublishedFileId[numSubscribed]; Internal.GetSubscribedItems(ids, numSubscribed); diff --git a/Libraries/Facepunch.Steamworks/SteamUser.cs b/Libraries/Facepunch.Steamworks/SteamUser.cs index e481d8493..8f4c6620e 100644 --- a/Libraries/Facepunch.Steamworks/SteamUser.cs +++ b/Libraries/Facepunch.Steamworks/SteamUser.cs @@ -15,7 +15,7 @@ namespace Steamworks /// public class SteamUser : SteamClientClass { - internal static ISteamUser Internal => Interface as ISteamUser; + internal static ISteamUser? Internal => Interface as ISteamUser; internal override void InitializeInterface( bool server ) { @@ -26,7 +26,7 @@ namespace Steamworks SampleRate = OptimalSampleRate; } - static Dictionary richPresence; + static Dictionary? richPresence; internal static void InstallEvents() { @@ -48,20 +48,20 @@ namespace Steamworks /// Usually this will have occurred before the game has launched, and should only be seen if the /// user has dropped connection due to a networking issue or a Steam server update. /// - public static event Action OnSteamServersConnected; + public static event Action? OnSteamServersConnected; /// /// Called when a connection attempt has failed. /// This will occur periodically if the Steam client is not connected, /// and has failed when retrying to establish a connection. /// - public static event Action OnSteamServerConnectFailure; + public static event Action? OnSteamServerConnectFailure; /// /// Called if the client has lost connection to the Steam servers. /// Real-time services will be disabled until a matching OnSteamServersConnected has been posted. /// - public static event Action OnSteamServersDisconnected; + public static event Action? OnSteamServersDisconnected; /// /// Sent by the Steam server to the client telling it to disconnect from the specified game server, @@ -69,12 +69,12 @@ namespace Steamworks /// The game client should immediately disconnect upon receiving this message. /// This can usually occur if the user doesn't have rights to play on the game server. /// - public static event Action OnClientGameServerDeny; + public static event Action? OnClientGameServerDeny; /// /// Called whenever the users licenses (owned packages) changes. /// - public static event Action OnLicensesUpdated; + public static event Action? OnLicensesUpdated; /// /// Called when an auth ticket has been validated. @@ -82,18 +82,18 @@ namespace Steamworks /// The second is the Steam ID that owns the game, this will be different from the first /// if the game is being borrowed via Steam Family Sharing /// - public static event Action OnValidateAuthTicketResponse; + public static event Action? OnValidateAuthTicketResponse; /// /// Used internally for GetAuthSessionTicketAsync /// - internal static event Action OnGetAuthSessionTicketResponse; + internal static event Action? OnGetAuthSessionTicketResponse; /// /// Called when a user has responded to a microtransaction authorization request. /// ( appid, orderid, user authorized ) /// - public static event Action OnMicroTxnAuthorizationResponse; + public static event Action? OnMicroTxnAuthorizationResponse; /// /// Sent to your game in response to a steam://gamewebcallback/(appid)/command/stuff command from a user clicking a @@ -101,14 +101,14 @@ namespace Steamworks /// You can use this to add support for external site signups where you want to pop back into the browser after some web page /// signup sequence, and optionally get back some detail about that. /// - public static event Action OnGameWebCallback; + public static event Action? OnGameWebCallback; /// /// Sent for games with enabled anti indulgence / duration control, for enabled users. /// Lets the game know whether persistent rewards or XP should be granted at normal rate, /// half rate, or zero rate. /// - public static event Action OnDurationControl; + public static event Action? OnDurationControl; @@ -127,8 +127,8 @@ namespace Steamworks set { _recordingVoice = value; - if ( value ) Internal.StartVoiceRecording(); - else Internal.StopVoiceRecording(); + if ( value ) Internal?.StartVoiceRecording(); + else Internal?.StopVoiceRecording(); } } @@ -142,7 +142,7 @@ namespace Steamworks { uint szCompressed = 0, deprecated = 0; - if ( Internal.GetAvailableVoice( ref szCompressed, ref deprecated, 0 ) != VoiceResult.OK ) + if ( Internal is null || Internal.GetAvailableVoice( ref szCompressed, ref deprecated, 0 ) != VoiceResult.OK ) return false; return szCompressed > 0; @@ -168,7 +168,7 @@ namespace Steamworks fixed ( byte* b = readBuffer ) { - if ( Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) + if ( Internal is null || Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) return 0; } @@ -185,7 +185,7 @@ namespace Steamworks /// ReadVoiceData because it won't be creating a new byte array every call. But this /// makes it easier to get it working, so let the babies have their bottle. /// - public static unsafe byte[] ReadVoiceDataBytes() + public static unsafe byte[]? ReadVoiceDataBytes() { if ( !HasVoiceData ) return null; @@ -195,7 +195,7 @@ namespace Steamworks fixed ( byte* b = readBuffer ) { - if ( Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) + if ( Internal is null || Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK ) return null; } @@ -222,7 +222,7 @@ namespace Steamworks } } - public static uint OptimalSampleRate => Internal.GetVoiceOptimalSampleRate(); + public static uint OptimalSampleRate => Internal?.GetVoiceOptimalSampleRate() ?? 0; /// @@ -247,7 +247,7 @@ namespace Steamworks fixed ( byte* frm = from ) fixed ( byte* dst = to ) { - if ( Internal.DecompressVoice( (IntPtr) frm, (uint) length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) + if ( Internal is null || Internal.DecompressVoice( (IntPtr) frm, (uint) length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) return 0; } @@ -273,7 +273,7 @@ namespace Steamworks fixed ( byte* frm = from ) fixed ( byte* dst = to ) { - if ( Internal.DecompressVoice( (IntPtr)frm, (uint)from.Length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) + if ( Internal is null || Internal.DecompressVoice( (IntPtr)frm, (uint)from.Length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK ) return 0; } @@ -297,7 +297,7 @@ namespace Steamworks uint szWritten = 0; - if ( Internal.DecompressVoice( from, (uint) length, to, (uint)bufferSize, ref szWritten, SampleRate ) != VoiceResult.OK ) + if ( Internal is null || Internal.DecompressVoice( from, (uint) length, to, (uint)bufferSize, ref szWritten, SampleRate ) != VoiceResult.OK ) return 0; return (int)szWritten; @@ -306,14 +306,14 @@ namespace Steamworks /// /// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you. /// - public static unsafe AuthTicket GetAuthSessionTicket() + public static unsafe AuthTicket? GetAuthSessionTicket() { var data = Helpers.TakeBuffer( 1024 ); fixed ( byte* b = data ) { uint ticketLength = 0; - uint ticket = Internal.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength ); + uint ticket = Internal?.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength ) ?? 0; if ( ticket == 0 ) return null; @@ -332,15 +332,15 @@ namespace Steamworks /// the ticket is definitely ready to go as soon as it returns. Will return null if the callback /// times out or returns negatively. /// - public static async Task GetAuthSessionTicketAsync( double timeoutSeconds = 10.0f ) + public static async Task GetAuthSessionTicketAsync( double timeoutSeconds = 10.0f ) { var result = Result.Pending; - AuthTicket ticket = null; + AuthTicket? ticket = null; var stopwatch = Stopwatch.StartNew(); void f( GetAuthSessionTicketResponse_t t ) { - if ( t.AuthTicket != ticket.Handle ) return; + if ( t.AuthTicket != ticket?.Handle ) return; result = t.Result; } @@ -379,11 +379,11 @@ namespace Steamworks { fixed ( byte* ptr = ticketData ) { - return Internal.BeginAuthSession( (IntPtr) ptr, ticketData.Length, steamid ); + return Internal?.BeginAuthSession( (IntPtr) ptr, ticketData.Length, steamid ) ?? BeginAuthResult.ServerNotConnectedToSteam; } } - public static void EndAuthSession( SteamId steamid ) => Internal.EndAuthSession( steamid ); + public static void EndAuthSession( SteamId steamid ) => Internal?.EndAuthSession( steamid ); // UserHasLicenseForApp - SERVER VERSION ( DLC CHECKING ) @@ -392,12 +392,12 @@ namespace Steamworks /// Checks if the current users looks like they are behind a NAT device. /// This is only valid if the user is connected to the Steam servers and may not catch all forms of NAT. /// - public static bool IsBehindNAT => Internal.BIsBehindNAT(); + public static bool IsBehindNAT => Internal != null && Internal.BIsBehindNAT(); /// /// Gets the Steam level of the user, as shown on their Steam community profile. /// - public static int SteamLevel => Internal.GetPlayerSteamLevel(); + public static int SteamLevel => Internal?.GetPlayerSteamLevel() ?? 0; /// /// Requests a URL which authenticates an in-game browser for store check-out, and then redirects to the specified URL. @@ -405,8 +405,10 @@ namespace Steamworks /// NOTE: The URL has a very short lifetime to prevent history-snooping attacks, so you should only call this API when you are about to launch the browser, or else immediately navigate to the result URL using a hidden browser window. /// NOTE: The resulting authorization cookie has an expiration time of one day, so it would be a good idea to request and visit a new auth URL every 12 hours. /// - public static async Task GetStoreAuthUrlAsync( string url ) + public static async Task GetStoreAuthUrlAsync( string url ) { + if (Internal is null) { return null; } + var response = await Internal.RequestStoreAuthURL( url ); if ( !response.HasValue ) return null; @@ -417,22 +419,22 @@ namespace Steamworks /// /// Checks whether the current user has verified their phone number. /// - public static bool IsPhoneVerified => Internal.BIsPhoneVerified(); + public static bool IsPhoneVerified => Internal != null && Internal.BIsPhoneVerified(); /// /// Checks whether the current user has Steam Guard two factor authentication enabled on their account. /// - public static bool IsTwoFactorEnabled => Internal.BIsTwoFactorEnabled(); + public static bool IsTwoFactorEnabled => Internal != null && Internal.BIsTwoFactorEnabled(); /// /// Checks whether the user's phone number is used to uniquely identify them. /// - public static bool IsPhoneIdentifying => Internal.BIsPhoneIdentifying(); + public static bool IsPhoneIdentifying => Internal != null && Internal.BIsPhoneIdentifying(); /// /// Checks whether the current user's phone number is awaiting (re)verification. /// - public static bool IsPhoneRequiringVerification => Internal.BIsPhoneRequiringVerification(); + public static bool IsPhoneRequiringVerification => Internal != null && Internal.BIsPhoneRequiringVerification(); /// /// Requests an application ticket encrypted with the secret "encrypted app ticket key". @@ -441,8 +443,10 @@ namespace Steamworks /// If you get a null result from this it's probably because you're calling it too often. /// This can fail if you don't have an encrypted ticket set for your app here https://partner.steamgames.com/apps/sdkauth/ /// - public static async Task RequestEncryptedAppTicketAsync( byte[] dataToInclude ) + public static async Task RequestEncryptedAppTicketAsync( byte[] dataToInclude ) { + if (Internal is null) { return null; } + var dataPtr = Marshal.AllocHGlobal( dataToInclude.Length ); Marshal.Copy( dataToInclude, 0, dataPtr, dataToInclude.Length ); @@ -453,7 +457,7 @@ namespace Steamworks var ticketData = Marshal.AllocHGlobal( 1024 ); uint outSize = 0; - byte[] data = null; + byte[]? data = null; if ( Internal.GetEncryptedAppTicket( ticketData, 1024, ref outSize ) ) { @@ -477,14 +481,16 @@ namespace Steamworks /// There can only be one call pending, and this call is subject to a 60 second rate limit. /// This can fail if you don't have an encrypted ticket set for your app here https://partner.steamgames.com/apps/sdkauth/ /// - public static async Task RequestEncryptedAppTicketAsync() + public static async Task RequestEncryptedAppTicketAsync() { + if (Internal is null) { return null; } + var result = await Internal.RequestEncryptedAppTicket( IntPtr.Zero, 0 ); if ( !result.HasValue || result.Value.Result != Result.OK ) return null; var ticketData = Marshal.AllocHGlobal( 1024 ); uint outSize = 0; - byte[] data = null; + byte[]? data = null; if ( Internal.GetEncryptedAppTicket( ticketData, 1024, ref outSize ) ) { @@ -504,6 +510,8 @@ namespace Steamworks /// public static async Task GetDurationControl() { + if (Internal is null) { return default; } + var response = await Internal.GetDurationControl(); if ( !response.HasValue ) return default; diff --git a/Libraries/Facepunch.Steamworks/SteamUserStats.cs b/Libraries/Facepunch.Steamworks/SteamUserStats.cs index 97bbb31f0..f9fc6fa5d 100644 --- a/Libraries/Facepunch.Steamworks/SteamUserStats.cs +++ b/Libraries/Facepunch.Steamworks/SteamUserStats.cs @@ -9,7 +9,7 @@ namespace Steamworks { public class SteamUserStats : SteamClientClass { - internal static ISteamUserStats Internal => Interface as ISteamUserStats; + internal static ISteamUserStats? Internal => Interface as ISteamUserStats; internal override void InitializeInterface( bool server ) { @@ -40,31 +40,31 @@ namespace Steamworks /// /// called when the achivement icon is loaded /// - internal static event Action OnAchievementIconFetched; + internal static event Action? OnAchievementIconFetched; /// /// called when the latests stats and achievements have been received /// from the server /// - public static event Action OnUserStatsReceived; + public static event Action? OnUserStatsReceived; /// /// result of a request to store the user stats for a game /// - public static event Action OnUserStatsStored; + public static event Action? OnUserStatsStored; /// /// result of a request to store the achievements for a game, or an /// "indicate progress" call. If both m_nCurProgress and m_nMaxProgress /// are zero, that means the achievement has been fully unlocked /// - public static event Action OnAchievementProgress; + public static event Action? OnAchievementProgress; /// /// Callback indicating that a user's stats have been unloaded /// - public static event Action OnUserStatsUnloaded; + public static event Action? OnUserStatsUnloaded; /// /// Get the available achievements @@ -73,8 +73,11 @@ namespace Steamworks { get { - for( int i=0; i< Internal.GetNumAchievements(); i++ ) + if (Internal is null) { yield break; } + uint numAchievements = Internal.GetNumAchievements(); + for( int i=0; i < numAchievements; i++ ) { + if (Internal is null) { yield break; } yield return new Achievement( Internal.GetAchievementName( (uint) i ) ); } } @@ -88,6 +91,8 @@ namespace Steamworks /// public static bool IndicateAchievementProgress( string achName, int curProg, int maxProg ) { + if (Internal is null) { return false; } + if ( string.IsNullOrEmpty( achName ) ) throw new ArgumentNullException( "Achievement string is null or empty" ); @@ -103,6 +108,8 @@ namespace Steamworks /// public static async Task PlayerCountAsync() { + if (Internal is null) { return -1; } + var result = await Internal.GetNumberOfCurrentPlayers(); if ( !result.HasValue || result.Value.Success == 0 ) return -1; @@ -122,7 +129,7 @@ namespace Steamworks /// public static bool StoreStats() { - return Internal.StoreStats(); + return Internal != null && Internal.StoreStats(); } /// @@ -133,7 +140,7 @@ namespace Steamworks /// public static bool RequestCurrentStats() { - return Internal.RequestCurrentStats(); + return Internal != null && Internal.RequestCurrentStats(); } /// @@ -146,6 +153,7 @@ namespace Steamworks /// OK indicates success, InvalidState means you need to call RequestCurrentStats first, Fail means the remote call failed public static async Task RequestGlobalStatsAsync( int days ) { + if (Internal is null) { return Result.Fail; } var result = await SteamUserStats.Internal.RequestGlobalStats( days ); if ( !result.HasValue ) return Result.Fail; return result.Value.Result; @@ -162,6 +170,7 @@ namespace Steamworks /// public static async Task FindOrCreateLeaderboardAsync( string name, LeaderboardSort sort, LeaderboardDisplay display ) { + if (Internal is null) { return null; } var result = await Internal.FindOrCreateLeaderboard( name, sort, display ); if ( !result.HasValue || result.Value.LeaderboardFound == 0 ) return null; @@ -172,6 +181,7 @@ namespace Steamworks public static async Task FindLeaderboardAsync( string name ) { + if (Internal is null) { return null; } var result = await Internal.FindLeaderboard( name ); if ( !result.HasValue || result.Value.LeaderboardFound == 0 ) return null; @@ -211,7 +221,7 @@ namespace Steamworks /// public static bool SetStat( string name, int value ) { - return Internal.SetStat( name, value ); + return Internal != null && Internal.SetStat( name, value ); } /// @@ -220,7 +230,7 @@ namespace Steamworks /// public static bool SetStat( string name, float value ) { - return Internal.SetStat( name, value ); + return Internal != null && Internal.SetStat( name, value ); } /// @@ -229,7 +239,7 @@ namespace Steamworks public static int GetStatInt( string name ) { int data = 0; - Internal.GetStat( name, ref data ); + Internal?.GetStat( name, ref data ); return data; } @@ -239,7 +249,7 @@ namespace Steamworks public static float GetStatFloat( string name ) { float data = 0; - Internal.GetStat( name, ref data ); + Internal?.GetStat( name, ref data ); return data; } @@ -250,7 +260,7 @@ namespace Steamworks /// public static bool ResetAll( bool includeAchievements ) { - return Internal.ResetAllStats( includeAchievements ); + return Internal != null && Internal.ResetAllStats( includeAchievements ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamUtils.cs b/Libraries/Facepunch.Steamworks/SteamUtils.cs index 9d3eea0d5..3f1553c23 100644 --- a/Libraries/Facepunch.Steamworks/SteamUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamUtils.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamUtils : SteamSharedClass { - internal static ISteamUtils Internal => Interface as ISteamUtils; + internal static ISteamUtils? Internal => Interface as ISteamUtils; internal override void InitializeInterface( bool server ) { @@ -38,47 +38,47 @@ namespace Steamworks /// /// The country of the user changed /// - public static event Action OnIpCountryChanged; + public static event Action? OnIpCountryChanged; /// /// Fired when running on a laptop and less than 10 minutes of battery is left, fires then every minute /// The parameter is the number of minutes left /// - public static event Action OnLowBatteryPower; + public static event Action? OnLowBatteryPower; /// /// Called when Steam wants to shutdown /// - public static event Action OnSteamShutdown; + public static event Action? OnSteamShutdown; /// /// Big Picture gamepad text input has been closed. Parameter is true if text was submitted, false if cancelled etc. /// - public static event Action OnGamepadTextInputDismissed; + public static event Action? OnGamepadTextInputDismissed; /// /// Returns the number of seconds since the application was active /// - public static uint SecondsSinceAppActive => Internal.GetSecondsSinceAppActive(); + public static uint SecondsSinceAppActive => Internal?.GetSecondsSinceAppActive() ?? 0; /// /// Returns the number of seconds since the user last moved the mouse etc /// - public static uint SecondsSinceComputerActive => Internal.GetSecondsSinceComputerActive(); + public static uint SecondsSinceComputerActive => Internal?.GetSecondsSinceComputerActive() ?? 0; // the universe this client is connecting to - public static Universe ConnectedUniverse => Internal.GetConnectedUniverse(); + public static Universe ConnectedUniverse => Internal?.GetConnectedUniverse() ?? Universe.Invalid; /// /// Steam server time. Number of seconds since January 1, 1970, GMT (i.e unix time) /// - public static DateTime SteamServerTime => Epoch.ToDateTime( Internal.GetServerRealTime() ); + public static DateTime SteamServerTime => Internal != null ? Epoch.ToDateTime( Internal.GetServerRealTime() ) : default; /// /// returns the 2 digit ISO 3166-1-alpha-2 format country code this client is running in (as looked up via an IP-to-location database) /// e.g "US" or "UK". /// - public static string IpCountry => Internal.GetIPCountry(); + public static string? IpCountry => Internal?.GetIPCountry(); /// /// returns true if the image exists, and the buffer was successfully filled out @@ -89,7 +89,7 @@ namespace Steamworks { width = 0; height = 0; - return Internal.GetImageSize( image, ref width, ref height ); + return Internal != null && Internal.GetImageSize( image, ref width, ref height ); } /// @@ -109,7 +109,7 @@ namespace Steamworks var buf = Helpers.TakeBuffer( (int) size ); - if ( !Internal.GetImageRGBA( image, buf, (int)size ) ) + if ( Internal is null || !Internal.GetImageRGBA( image, buf, (int)size ) ) return null; i.Data = new byte[size]; @@ -120,12 +120,12 @@ namespace Steamworks /// /// Returns true if we're using a battery (ie, a laptop not plugged in) /// - public static bool UsingBatteryPower => Internal.GetCurrentBatteryPower() != 255; + public static bool UsingBatteryPower => Internal != null && Internal.GetCurrentBatteryPower() != 255; /// /// Returns battery power [0-1] /// - public static float CurrentBatteryPower => Math.Min( Internal.GetCurrentBatteryPower() / 100, 1.0f ); + public static float CurrentBatteryPower => Math.Min( (Internal?.GetCurrentBatteryPower() ?? 0f) / 100, 1.0f ); static NotificationPosition overlayNotificationPosition = NotificationPosition.BottomRight; @@ -140,7 +140,7 @@ namespace Steamworks set { overlayNotificationPosition = value; - Internal.SetOverlayNotificationPosition( value ); + Internal?.SetOverlayNotificationPosition( value ); } } @@ -148,7 +148,7 @@ namespace Steamworks /// Returns true if the overlay is running and the user can access it. The overlay process could take a few seconds to /// start and hook the game process, so this function will initially return false while the overlay is loading. /// - public static bool IsOverlayEnabled => Internal.IsOverlayEnabled(); + public static bool IsOverlayEnabled => Internal != null && Internal.IsOverlayEnabled(); /// /// Normally this call is unneeded if your game has a constantly running frame loop that calls the @@ -161,7 +161,7 @@ namespace Steamworks /// in that case, and then you can check for this periodically (roughly 33hz is desirable) and make sure you /// refresh the screen with Present or SwapBuffers to allow the overlay to do it's work. /// - public static bool DoesOverlayNeedPresent => Internal.BOverlayNeedsPresent(); + public static bool DoesOverlayNeedPresent => Internal != null && Internal.BOverlayNeedsPresent(); /// /// Asynchronous call to check if an executable file has been signed using the public key set on the signing tab @@ -169,6 +169,8 @@ namespace Steamworks /// public static async Task CheckFileSignatureAsync( string filename ) { + if (Internal is null) { throw new System.Exception( "SteamUtils not initialized" ); } + var r = await Internal.CheckFileSignature( filename ); if ( !r.HasValue ) @@ -184,7 +186,7 @@ namespace Steamworks /// public static bool ShowGamepadTextInput( GamepadTextInputMode inputMode, GamepadTextInputLineMode lineInputMode, string description, int maxChars, string existingText = "" ) { - return Internal.ShowGamepadTextInput( inputMode, lineInputMode, description, (uint)maxChars, existingText ); + return Internal != null && Internal.ShowGamepadTextInput( inputMode, lineInputMode, description, (uint)maxChars, existingText ); } /// @@ -192,6 +194,8 @@ namespace Steamworks /// public static string GetEnteredGamepadText() { + if (Internal is null) { return string.Empty; } + var len = Internal.GetEnteredGamepadTextLength(); if ( len == 0 ) return string.Empty; @@ -205,19 +209,19 @@ namespace Steamworks /// returns the language the steam client is running in, you probably want /// Apps.CurrentGameLanguage instead, this is for very special usage cases /// - public static string SteamUILanguage => Internal.GetSteamUILanguage(); + public static string? SteamUILanguage => Internal?.GetSteamUILanguage(); /// /// returns true if Steam itself is running in VR mode /// - public static bool IsSteamRunningInVR => Internal.IsSteamRunningInVR(); + public static bool IsSteamRunningInVR => Internal != null && Internal.IsSteamRunningInVR(); /// /// Sets the inset of the overlay notification from the corner specified by SetOverlayNotificationPosition /// public static void SetOverlayNotificationInset( int x, int y ) { - Internal.SetOverlayNotificationInset( x, y ); + Internal?.SetOverlayNotificationInset( x, y ); } /// @@ -225,13 +229,13 @@ namespace Steamworks /// Games much be launched through the Steam client to enable the Big Picture overlay. During development, /// a game can be added as a non-steam game to the developers library to test this feature /// - public static bool IsSteamInBigPictureMode => Internal.IsSteamInBigPictureMode(); + public static bool IsSteamInBigPictureMode => Internal != null && Internal.IsSteamInBigPictureMode(); /// /// ask SteamUI to create and render its OpenVR dashboard /// - public static void StartVRDashboard() => Internal.StartVRDashboard(); + public static void StartVRDashboard() => Internal?.StartVRDashboard(); /// /// Set whether the HMD content will be streamed via Steam In-Home Streaming @@ -242,24 +246,24 @@ namespace Steamworks /// public static bool VrHeadsetStreaming { - get => Internal.IsVRHeadsetStreamingEnabled(); + get => Internal != null && Internal.IsVRHeadsetStreamingEnabled(); set { - Internal.SetVRHeadsetStreamingEnabled( value ); + Internal?.SetVRHeadsetStreamingEnabled( value ); } } internal static bool IsCallComplete( SteamAPICall_t call, out bool failed ) { failed = false; - return Internal.IsAPICallCompleted( call, ref failed ); + return Internal != null && Internal.IsAPICallCompleted( call, ref failed ); } /// /// Returns whether this steam client is a Steam China specific client, vs the global client /// - public static bool IsSteamChinaLauncher => Internal.IsSteamChinaLauncher(); + public static bool IsSteamChinaLauncher => Internal != null && Internal.IsSteamChinaLauncher(); } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/SteamVideo.cs b/Libraries/Facepunch.Steamworks/SteamVideo.cs index c6eaf438c..f3ec5fabe 100644 --- a/Libraries/Facepunch.Steamworks/SteamVideo.cs +++ b/Libraries/Facepunch.Steamworks/SteamVideo.cs @@ -12,7 +12,7 @@ namespace Steamworks /// public class SteamVideo : SteamClientClass { - internal static ISteamVideo Internal => Interface as ISteamVideo; + internal static ISteamVideo? Internal => Interface as ISteamVideo; internal override void InitializeInterface( bool server ) { @@ -26,8 +26,8 @@ namespace Steamworks Dispatch.Install( x => OnBroadcastStopped?.Invoke( x.Result ) ); } - public static event Action OnBroadcastStarted; - public static event Action OnBroadcastStopped; + public static event Action? OnBroadcastStarted; + public static event Action? OnBroadcastStopped; /// /// Return true if currently using Steam's live broadcasting @@ -37,7 +37,7 @@ namespace Steamworks get { int viewers = 0; - return Internal.IsBroadcasting( ref viewers ); + return Internal != null && Internal.IsBroadcasting( ref viewers ); } } @@ -50,7 +50,7 @@ namespace Steamworks { int viewers = 0; - if ( !Internal.IsBroadcasting( ref viewers ) ) + if ( Internal is null || !Internal.IsBroadcasting( ref viewers ) ) return 0; return viewers; diff --git a/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs b/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs index 9f0feeaf2..eb731ad94 100644 --- a/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs +++ b/Libraries/Facepunch.Steamworks/Steamworks.NET/SteamMatchmakingResponses.cs @@ -74,12 +74,10 @@ namespace Steamworks m_RulesFailedToRespond = onRulesFailedToRespond; m_RulesRefreshComplete = onRulesRefreshComplete; - m_VTable = new VTable() - { - m_VTRulesResponded = InternalOnRulesResponded, - m_VTRulesFailedToRespond = InternalOnRulesFailedToRespond, - m_VTRulesRefreshComplete = InternalOnRulesRefreshComplete - }; + m_VTable = new VTable( + mVtRulesResponded: InternalOnRulesResponded, + mVtRulesFailedToRespond: InternalOnRulesFailedToRespond, + mVtRulesRefreshComplete: InternalOnRulesRefreshComplete); m_pVTable = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(VTable))); Marshal.StructureToPtr(m_VTable, m_pVTable, false); @@ -153,13 +151,20 @@ namespace Steamworks private class VTable { [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalRulesResponded m_VTRulesResponded; + public readonly InternalRulesResponded m_VTRulesResponded; [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalRulesFailedToRespond m_VTRulesFailedToRespond; + public readonly InternalRulesFailedToRespond m_VTRulesFailedToRespond; [NonSerialized] [MarshalAs(UnmanagedType.FunctionPtr)] - public InternalRulesRefreshComplete m_VTRulesRefreshComplete; + public readonly InternalRulesRefreshComplete m_VTRulesRefreshComplete; + + public VTable(InternalRulesResponded mVtRulesResponded, InternalRulesFailedToRespond mVtRulesFailedToRespond, InternalRulesRefreshComplete mVtRulesRefreshComplete) + { + m_VTRulesResponded = mVtRulesResponded; + m_VTRulesFailedToRespond = mVtRulesFailedToRespond; + m_VTRulesRefreshComplete = mVtRulesRefreshComplete; + } } public static explicit operator System.IntPtr(SteamMatchmakingRulesResponse that) diff --git a/Libraries/Facepunch.Steamworks/Structs/Achievement.cs b/Libraries/Facepunch.Steamworks/Structs/Achievement.cs index 04ac43871..963596c7e 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Achievement.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Achievement.cs @@ -25,16 +25,16 @@ namespace Steamworks.Data get { var state = false; - SteamUserStats.Internal.GetAchievement( Value, ref state ); + SteamUserStats.Internal?.GetAchievement( Value, ref state ); return state; } } public string Identifier => Value; - public string Name => SteamUserStats.Internal.GetAchievementDisplayAttribute( Value, "name" ); + public string? Name => SteamUserStats.Internal?.GetAchievementDisplayAttribute( Value, "name" ); - public string Description => SteamUserStats.Internal.GetAchievementDisplayAttribute( Value, "desc" ); + public string? Description => SteamUserStats.Internal?.GetAchievementDisplayAttribute( Value, "desc" ); /// @@ -47,7 +47,7 @@ namespace Steamworks.Data var state = false; uint time = 0; - if ( !SteamUserStats.Internal.GetAchievementAndUnlockTime( Value, ref state, ref time ) || !state ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetAchievementAndUnlockTime( Value, ref state, ref time ) || !state ) return null; return Epoch.ToDateTime( time ); @@ -60,6 +60,7 @@ namespace Steamworks.Data /// public Image? GetIcon() { + if (SteamUserStats.Internal is null) { return null; } return SteamUtils.GetImage( SteamUserStats.Internal.GetAchievementIcon( Value ) ); } @@ -69,6 +70,7 @@ namespace Steamworks.Data /// public async Task GetIconAsync( int timeout = 5000 ) { + if (SteamUserStats.Internal is null) { return null; } var i = SteamUserStats.Internal.GetAchievementIcon( Value ); if ( i != 0 ) return SteamUtils.GetImage( i ); @@ -115,7 +117,7 @@ namespace Steamworks.Data { float pct = 0; - if ( !SteamUserStats.Internal.GetAchievementAchievedPercent( Value, ref pct ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetAchievementAchievedPercent( Value, ref pct ) ) return -1.0f; return pct / 100.0f; @@ -127,6 +129,8 @@ namespace Steamworks.Data /// public bool Trigger( bool apply = true ) { + if (SteamUserStats.Internal is null) { return false; } + var r = SteamUserStats.Internal.SetAchievement( Value ); if ( apply && r ) @@ -142,6 +146,7 @@ namespace Steamworks.Data /// public bool Clear() { + if (SteamUserStats.Internal is null) { return false; } return SteamUserStats.Internal.ClearAchievement( Value ); } } diff --git a/Libraries/Facepunch.Steamworks/Structs/Clan.cs b/Libraries/Facepunch.Steamworks/Structs/Clan.cs index 6bcdda826..e6cca31d5 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Clan.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Clan.cs @@ -14,20 +14,20 @@ namespace Steamworks Id = id; } - public string Name => SteamFriends.Internal.GetClanName(Id); + public string? Name => SteamFriends.Internal?.GetClanName(Id); - public string Tag => SteamFriends.Internal.GetClanTag(Id); + public string? Tag => SteamFriends.Internal?.GetClanTag(Id); - public int ChatMemberCount => SteamFriends.Internal.GetClanChatMemberCount(Id); + public int ChatMemberCount => SteamFriends.Internal?.GetClanChatMemberCount(Id) ?? 0; - public Friend Owner => new Friend(SteamFriends.Internal.GetClanOwner(Id)); + public Friend Owner => new Friend(SteamFriends.Internal?.GetClanOwner(Id) ?? 0); - public bool Public => SteamFriends.Internal.IsClanPublic(Id); + public bool Public => SteamFriends.Internal != null && SteamFriends.Internal.IsClanPublic(Id); /// /// Is the clan an official game group? /// - public bool Official => SteamFriends.Internal.IsClanOfficialGameGroup(Id); + public bool Official => SteamFriends.Internal != null && SteamFriends.Internal.IsClanOfficialGameGroup(Id); /// /// Asynchronously fetches the officer list for a given clan @@ -35,14 +35,18 @@ namespace Steamworks /// Whether the request was successful or not public async Task RequestOfficerList() { + if (SteamFriends.Internal is null) { return false; } var req = await SteamFriends.Internal.RequestClanOfficerList(Id); return req.HasValue && req.Value.Success != 0x0; } public IEnumerable GetOfficers() { - for (int i = 0; i < SteamFriends.Internal.GetClanOfficerCount(Id); i++) + if (SteamFriends.Internal is null) { yield break; } + var officerCount = SteamFriends.Internal.GetClanOfficerCount(Id); + for (int i = 0; i < officerCount; i++) { + if (SteamFriends.Internal is null) { yield break; } yield return new Friend(SteamFriends.Internal.GetClanOfficerByIndex(Id, i)); } } diff --git a/Libraries/Facepunch.Steamworks/Structs/Controller.cs b/Libraries/Facepunch.Steamworks/Structs/Controller.cs index f694ecd2c..6fe355bd8 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Controller.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Controller.cs @@ -14,7 +14,7 @@ namespace Steamworks } public ulong Id => Handle.Value; - public InputType InputType => SteamInput.Internal.GetInputTypeForHandle( Handle ); + public InputType InputType => SteamInput.Internal?.GetInputTypeForHandle( Handle ) ?? InputType.Unknown; /// /// Reconfigure the controller to use the specified action set (ie 'Menu', 'Walk' or 'Drive') @@ -23,12 +23,12 @@ namespace Steamworks /// public string ActionSet { - set => SteamInput.Internal.ActivateActionSet( Handle, SteamInput.Internal.GetActionSetHandle( value ) ); + set => SteamInput.Internal?.ActivateActionSet( Handle, SteamInput.Internal.GetActionSetHandle( value ) ); } - public void DeactivateLayer( string layer ) => SteamInput.Internal.DeactivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); - public void ActivateLayer( string layer ) => SteamInput.Internal.ActivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); - public void ClearLayers() => SteamInput.Internal.DeactivateAllActionSetLayers( Handle ); + public void DeactivateLayer( string layer ) => SteamInput.Internal?.DeactivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); + public void ActivateLayer( string layer ) => SteamInput.Internal?.ActivateActionSetLayer( Handle, SteamInput.Internal.GetActionSetHandle( layer ) ); + public void ClearLayers() => SteamInput.Internal?.DeactivateAllActionSetLayers( Handle ); /// @@ -36,7 +36,7 @@ namespace Steamworks /// public DigitalState GetDigitalState( string actionName ) { - return SteamInput.Internal.GetDigitalActionData( Handle, SteamInput.GetDigitalActionHandle( actionName ) ); + return SteamInput.Internal?.GetDigitalActionData( Handle, SteamInput.GetDigitalActionHandle( actionName ) ) ?? default; } /// @@ -44,7 +44,7 @@ namespace Steamworks /// public AnalogState GetAnalogState( string actionName ) { - return SteamInput.Internal.GetAnalogActionData( Handle, SteamInput.GetAnalogActionHandle( actionName ) ); + return SteamInput.Internal?.GetAnalogActionData( Handle, SteamInput.GetAnalogActionHandle( actionName ) ) ?? default; } diff --git a/Libraries/Facepunch.Steamworks/Structs/Friend.cs b/Libraries/Facepunch.Steamworks/Structs/Friend.cs index 510227bbf..90a1df26f 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Friend.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Friend.cs @@ -73,16 +73,16 @@ namespace Steamworks - public Relationship Relationship => SteamFriends.Internal.GetFriendRelationship( Id ); - public FriendState State => SteamFriends.Internal.GetFriendPersonaState( Id ); - public string Name => SteamFriends.Internal.GetFriendPersonaName( Id ); + public Relationship Relationship => SteamFriends.Internal?.GetFriendRelationship( Id ) ?? Relationship.None; + public FriendState State => SteamFriends.Internal?.GetFriendPersonaState( Id ) ?? FriendState.Offline; + public string? Name => SteamFriends.Internal?.GetFriendPersonaName( Id ); public IEnumerable NameHistory { get { for( int i=0; i<32; i++ ) { - var n = SteamFriends.Internal.GetFriendPersonaNameHistory( Id, i ); + var n = SteamFriends.Internal?.GetFriendPersonaNameHistory( Id, i ); if ( string.IsNullOrEmpty( n ) ) break; @@ -91,7 +91,7 @@ namespace Steamworks } } - public int SteamLevel => SteamFriends.Internal.GetFriendSteamLevel( Id ); + public int SteamLevel => SteamFriends.Internal?.GetFriendSteamLevel( Id ) ?? 0; @@ -100,7 +100,7 @@ namespace Steamworks get { FriendGameInfo_t gameInfo = default; - if ( !SteamFriends.Internal.GetFriendGamePlayed( Id, ref gameInfo ) ) + if ( SteamFriends.Internal is null || !SteamFriends.Internal.GetFriendGamePlayed( Id, ref gameInfo ) ) return null; return FriendGameInfo.From( gameInfo ); @@ -109,7 +109,7 @@ namespace Steamworks public bool IsIn( SteamId group_or_room ) { - return SteamFriends.Internal.IsUserInSource( Id, group_or_room ); + return SteamFriends.Internal != null && SteamFriends.Internal.IsUserInSource( Id, group_or_room ); } public struct FriendGameInfo @@ -161,9 +161,9 @@ namespace Steamworks return await SteamFriends.GetLargeAvatarAsync( Id ); } - public string GetRichPresence( string key ) + public string? GetRichPresence( string key ) { - var val = SteamFriends.Internal.GetFriendRichPresence( Id, key ); + var val = SteamFriends.Internal?.GetFriendRichPresence( Id, key ); if ( string.IsNullOrEmpty( val ) ) return null; return val; } @@ -173,7 +173,7 @@ namespace Steamworks /// public bool InviteToGame( string Text ) { - return SteamFriends.Internal.InviteUserToGame( Id, Text ); + return SteamFriends.Internal != null && SteamFriends.Internal.InviteUserToGame( Id, Text ); } /// @@ -181,7 +181,7 @@ namespace Steamworks /// public bool SendMessage( string message ) { - return SteamFriends.Internal.ReplyToFriendMessage( Id, message ); + return SteamFriends.Internal != null && SteamFriends.Internal.ReplyToFriendMessage( Id, message ); } @@ -191,8 +191,9 @@ namespace Steamworks /// True if successful, False if failure public async Task RequestUserStatsAsync() { + if (SteamUserStats.Internal is null) { return false; } var result = await SteamUserStats.Internal.RequestUserStats( Id ); - return result.HasValue && result.Value.Result == Result.OK; + return result?.Result == Result.OK; } /// @@ -205,7 +206,7 @@ namespace Steamworks { var val = defult; - if ( !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) return defult; return val; @@ -221,7 +222,7 @@ namespace Steamworks { var val = defult; - if ( !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserStat( Id, statName, ref val ) ) return defult; return val; @@ -237,7 +238,7 @@ namespace Steamworks { var val = defult; - if ( !SteamUserStats.Internal.GetUserAchievement( Id, statName, ref val ) ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserAchievement( Id, statName, ref val ) ) return defult; return val; @@ -253,7 +254,7 @@ namespace Steamworks bool val = false; uint time = 0; - if ( !SteamUserStats.Internal.GetUserAchievementAndUnlockTime( Id, statName, ref val, ref time ) || !val ) + if ( SteamUserStats.Internal is null || !SteamUserStats.Internal.GetUserAchievementAndUnlockTime( Id, statName, ref val, ref time ) || !val ) return DateTime.MinValue; return Epoch.ToDateTime( time ); diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs index 815094000..25afbd570 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryDef.cs @@ -8,7 +8,7 @@ namespace Steamworks public class InventoryDef : IEquatable { internal InventoryDefId _id; - internal Dictionary _properties; + internal Dictionary? _properties; public InventoryDef( InventoryDefId defId ) { @@ -20,32 +20,32 @@ namespace Steamworks /// /// Shortcut to call GetProperty( "name" ) /// - public string Name => GetProperty( "name" ); + public string? Name => GetProperty( "name" ); /// /// Shortcut to call GetProperty( "description" ) /// - public string Description => GetProperty( "description" ); + public string? Description => GetProperty( "description" ); /// /// Shortcut to call GetProperty( "icon_url" ) /// - public string IconUrl => GetProperty( "icon_url" ); + public string? IconUrl => GetProperty( "icon_url" ); /// /// Shortcut to call GetProperty( "icon_url_large" ) /// - public string IconUrlLarge => GetProperty( "icon_url_large" ); + public string? IconUrlLarge => GetProperty( "icon_url_large" ); /// /// Shortcut to call GetProperty( "price_category" ) /// - public string PriceCategory => GetProperty( "price_category" ); + public string? PriceCategory => GetProperty( "price_category" ); /// /// Shortcut to call GetProperty( "type" ) /// - public string Type => GetProperty( "type" ); + public string? Type => GetProperty( "type" ); /// /// Returns true if this is an item that generates an item, rather @@ -56,12 +56,12 @@ namespace Steamworks /// /// Shortcut to call GetProperty( "exchange" ) /// - public string ExchangeSchema => GetProperty( "exchange" ); + public string? ExchangeSchema => GetProperty( "exchange" ); /// /// Get a list of exchanges that are available to make this item /// - public InventoryRecipe[] GetRecipes() + public InventoryRecipe[]? GetRecipes() { if ( string.IsNullOrEmpty( ExchangeSchema ) ) return null; @@ -93,19 +93,19 @@ namespace Steamworks /// /// Get a specific property by name /// - public string GetProperty( string name ) + public string? GetProperty( string? name ) { - if ( _properties!= null && _properties.TryGetValue( name, out string val ) ) + if ( _properties != null && name != null && _properties.TryGetValue( name, out string val ) ) return val; uint _ = (uint)Helpers.MemoryBufferSize; - if ( !SteamInventory.Internal.GetItemDefinitionProperty( Id, name, out var vl, ref _ ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetItemDefinitionProperty( Id, name, out var vl, ref _ ) ) return null; - + if (name == null) //return keys string return vl; - + if ( _properties == null ) _properties = new Dictionary(); @@ -119,9 +119,9 @@ namespace Steamworks /// public bool GetBoolProperty( string name ) { - string val = GetProperty( name ); + string? val = GetProperty( name ); - if ( val.Length == 0 ) return false; + if ( string.IsNullOrEmpty(val) ) return false; if ( val[0] == '0' || val[0] == 'F' || val[0] == 'f' ) return false; return true; @@ -130,9 +130,9 @@ namespace Steamworks /// /// Read a raw property from the definition schema /// - public T GetProperty( string name ) + public T? GetProperty( string name ) { - string val = GetProperty( name ); + string? val = GetProperty( name ); if ( string.IsNullOrEmpty( val ) ) return default; @@ -150,16 +150,16 @@ namespace Steamworks /// /// Gets a list of all properties on this item /// - public IEnumerable> Properties + public IEnumerable> Properties { get { - var list = GetProperty( null ); + var list = GetProperty( null ) ?? ""; var keys = list.Split( ',' ); foreach ( var key in keys ) { - yield return new KeyValuePair( key, GetProperty( key ) ); + yield return new KeyValuePair( key, GetProperty( key ) ); } } } @@ -174,7 +174,7 @@ namespace Steamworks ulong curprice = 0; ulong baseprice = 0; - if ( !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) return 0; return (int) curprice; @@ -194,7 +194,7 @@ namespace Steamworks ulong curprice = 0; ulong baseprice = 0; - if ( !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetItemPrice( Id, ref curprice, ref baseprice ) ) return 0; return (int)baseprice; @@ -203,12 +203,12 @@ namespace Steamworks public string LocalBasePriceFormatted => Utility.FormatPrice( SteamInventory.Currency, LocalPrice / 100.0 ); - InventoryRecipe[] _recContaining; + InventoryRecipe[]? _recContaining; /// /// Return a list of recepies that contain this item /// - public InventoryRecipe[] GetRecipesContainingThis() + public InventoryRecipe[]? GetRecipesContainingThis() { if ( _recContaining != null ) return _recContaining; @@ -221,17 +221,17 @@ namespace Steamworks return _recContaining; } - public static bool operator ==( InventoryDef a, InventoryDef b ) + public static bool operator ==( InventoryDef? a, InventoryDef? b ) { if ( Object.ReferenceEquals( a, null ) ) return Object.ReferenceEquals( b, null ); return a.Equals( b ); } - public static bool operator !=( InventoryDef a, InventoryDef b ) => !(a == b); + public static bool operator !=( InventoryDef? a, InventoryDef? b ) => !(a == b); public override bool Equals( object p ) => this.Equals( (InventoryDef)p ); public override int GetHashCode() => Id.GetHashCode(); - public bool Equals( InventoryDef p ) + public bool Equals( InventoryDef? p ) { if ( p == null ) return false; return p.Id == Id; diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs index 886ed9be9..aeee9a5e3 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryItem.cs @@ -11,7 +11,7 @@ namespace Steamworks internal InventoryDefId _def; internal SteamItemFlags _flags; internal ushort _quantity; - internal Dictionary _properties; + internal Dictionary? _properties; public InventoryItemId Id => _id; @@ -19,13 +19,13 @@ namespace Steamworks public int Quantity => _quantity; - public InventoryDef Def => SteamInventory.FindDefinition( DefId ); + public InventoryDef? Def => SteamInventory.FindDefinition( DefId ); /// /// Only available if the result set was created with the getproperties /// - public Dictionary Properties => _properties; + public Dictionary? Properties => _properties; /// /// This item is account-locked and cannot be traded or given away. @@ -54,7 +54,7 @@ namespace Steamworks public async Task ConsumeAsync( int amount = 1 ) { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !SteamInventory.Internal.ConsumeItem( ref sresult, Id, (uint)amount ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.ConsumeItem( ref sresult, Id, (uint)amount ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -66,7 +66,7 @@ namespace Steamworks public async Task SplitStackAsync( int quantity = 1 ) { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !SteamInventory.Internal.TransferItemQuantity( ref sresult, Id, (uint)quantity, ulong.MaxValue ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.TransferItemQuantity( ref sresult, Id, (uint)quantity, ulong.MaxValue ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -78,7 +78,7 @@ namespace Steamworks public async Task AddAsync( InventoryItem add, int quantity = 1 ) { var sresult = Defines.k_SteamInventoryResultInvalid; - if ( !SteamInventory.Internal.TransferItemQuantity( ref sresult, add.Id, (uint)quantity, Id ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.TransferItemQuantity( ref sresult, add.Id, (uint)quantity, Id ) ) return null; return await InventoryResult.GetAsync( sresult ); @@ -98,11 +98,11 @@ namespace Steamworks return i; } - internal static Dictionary GetProperties( SteamInventoryResult_t result, int index ) + internal static Dictionary? GetProperties( SteamInventoryResult_t result, int index ) { var strlen = (uint) Helpers.MemoryBufferSize; - if ( !SteamInventory.Internal.GetResultItemProperty( result, (uint)index, null, out var propNames, ref strlen ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetResultItemProperty( result, (uint)index, null, out var propNames, ref strlen ) ) return null; var props = new Dictionary(); @@ -151,7 +151,7 @@ namespace Steamworks /// Tries to get the origin property. Need properties for this to work. /// Will return a string like "market" /// - public string Origin + public string? Origin { get { diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs index 1305eb8d0..21440664d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryRecipe.cs @@ -22,7 +22,7 @@ namespace Steamworks /// If we don't know about this item definition this might be null. /// In which case, DefinitionId should still hold the correct id. /// - public InventoryDef Definition; + public InventoryDef? Definition; /// /// The amount of this item needed. Generally this will be 1. diff --git a/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs b/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs index 5518ba4ed..8c58353b1 100644 --- a/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs +++ b/Libraries/Facepunch.Steamworks/Structs/InventoryResult.cs @@ -23,7 +23,7 @@ namespace Steamworks { uint cnt = 0; - if ( !SteamInventory.Internal.GetResultItems( _id, null, ref cnt ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetResultItems( _id, null, ref cnt ) ) return 0; return (int) cnt; @@ -36,17 +36,17 @@ namespace Steamworks /// public bool BelongsTo( SteamId steamId ) { - return SteamInventory.Internal.CheckResultSteamID( _id, steamId ); + return SteamInventory.Internal != null && SteamInventory.Internal.CheckResultSteamID( _id, steamId ); } - public InventoryItem[] GetItems( bool includeProperties = false ) + public InventoryItem[]? GetItems( bool includeProperties = false ) { uint cnt = (uint) ItemCount; if ( cnt <= 0 ) return null; var pOutItemsArray = new SteamItemDetails_t[cnt]; - if ( !SteamInventory.Internal.GetResultItems( _id, pOutItemsArray, ref cnt ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.GetResultItems( _id, pOutItemsArray, ref cnt ) ) return null; var items = new InventoryItem[cnt]; @@ -69,7 +69,7 @@ namespace Steamworks { if ( _id.Value == -1 ) return; - SteamInventory.Internal.DestroyResult( _id ); + SteamInventory.Internal?.DestroyResult( _id ); } internal static async Task GetAsync( SteamInventoryResult_t sresult ) @@ -77,7 +77,7 @@ namespace Steamworks var _result = Result.Pending; while ( _result == Result.Pending ) { - _result = SteamInventory.Internal.GetResultStatus( sresult ); + _result = SteamInventory.Internal?.GetResultStatus( sresult ) ?? Result.Fail; await Task.Delay( 10 ); } @@ -97,11 +97,11 @@ namespace Steamworks /// Results have a built-in timestamp which will be considered "expired" after an hour has elapsed.See DeserializeResult /// for expiration handling. /// - public unsafe byte[] Serialize() + public unsafe byte[]? Serialize() { uint size = 0; - if ( !SteamInventory.Internal.SerializeResult( _id, IntPtr.Zero, ref size ) ) + if ( SteamInventory.Internal is null || !SteamInventory.Internal.SerializeResult( _id, IntPtr.Zero, ref size ) ) return null; var data = new byte[size]; diff --git a/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs b/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs index ed7deb8c6..38c0dc1e0 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Leaderboard.cs @@ -14,10 +14,10 @@ namespace Steamworks.Data /// /// the name of a leaderboard /// - public string Name => SteamUserStats.Internal.GetLeaderboardName( Id ); - public LeaderboardSort Sort => SteamUserStats.Internal.GetLeaderboardSortMethod( Id ); - public LeaderboardDisplay Display => SteamUserStats.Internal.GetLeaderboardDisplayType( Id ); - public int EntryCount => SteamUserStats.Internal.GetLeaderboardEntryCount(Id); + public string? Name => SteamUserStats.Internal?.GetLeaderboardName( Id ); + public LeaderboardSort Sort => SteamUserStats.Internal?.GetLeaderboardSortMethod( Id ) ?? default; + public LeaderboardDisplay Display => SteamUserStats.Internal?.GetLeaderboardDisplayType( Id ) ?? default; + public int EntryCount => SteamUserStats.Internal?.GetLeaderboardEntryCount(Id) ?? 0; static int[] detailsBuffer = new int[64]; static int[] noDetails = Array.Empty(); @@ -25,8 +25,9 @@ namespace Steamworks.Data /// /// Submit your score and replace your old score even if it was better /// - public async Task ReplaceScore( int score, int[] details = null ) + public async Task ReplaceScore( int score, int[]? details = null ) { + if (SteamUserStats.Internal is null) { return null; } if ( details == null ) details = noDetails; var r = await SteamUserStats.Internal.UploadLeaderboardScore( Id, LeaderboardUploadScoreMethod.ForceUpdate, score, details, details.Length ); @@ -38,8 +39,9 @@ namespace Steamworks.Data /// /// Submit your new score, but won't replace your high score if it's lower /// - public async Task SubmitScoreAsync( int score, int[] details = null ) + public async Task SubmitScoreAsync( int score, int[]? details = null ) { + if (SteamUserStats.Internal is null) { return null; } if ( details == null ) details = noDetails; var r = await SteamUserStats.Internal.UploadLeaderboardScore( Id, LeaderboardUploadScoreMethod.KeepBest, score, details, details.Length ); @@ -53,6 +55,7 @@ namespace Steamworks.Data /// public async Task AttachUgc( Ugc file ) { + if (SteamUserStats.Internal is null) { return Result.Fail; } var r = await SteamUserStats.Internal.AttachLeaderboardUGC( Id, file.Handle ); if ( !r.HasValue ) return Result.Fail; @@ -62,8 +65,9 @@ namespace Steamworks.Data /// /// Fetches leaderboard entries for an arbitrary set of users on a specified leaderboard. /// - public async Task GetScoresForUsersAsync( SteamId[] users ) + public async Task GetScoresForUsersAsync( SteamId[]? users ) { + if (SteamUserStats.Internal is null) { return null; } if ( users == null || users.Length == 0 ) return null; @@ -77,8 +81,9 @@ namespace Steamworks.Data /// /// Used to query for a sequential range of leaderboard entries by leaderboard Sort. /// - public async Task GetScoresAsync( int count, int offset = 1 ) + public async Task GetScoresAsync( int count, int offset = 1 ) { + if (SteamUserStats.Internal is null) { return null; } if ( offset <= 0 ) throw new System.ArgumentException( "Should be 1+", nameof( offset ) ); var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.Global, offset, offset + count - 1 ); @@ -94,8 +99,9 @@ namespace Steamworks.Data /// For example, if the user is #1 on the leaderboard and start is set to -2, end is set to 2, Steam will return the first /// 5 entries in the leaderboard. If The current user has no entry, this will return null. /// - public async Task GetScoresAroundUserAsync( int start = -10, int end = 10 ) + public async Task GetScoresAroundUserAsync( int start = -10, int end = 10 ) { + if (SteamUserStats.Internal is null) { return null; } var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.GlobalAroundUser, start, end ); if ( !r.HasValue ) return null; @@ -106,8 +112,9 @@ namespace Steamworks.Data /// /// Used to retrieve all leaderboard entries for friends of the current user /// - public async Task GetScoresFromFriendsAsync() + public async Task GetScoresFromFriendsAsync() { + if (SteamUserStats.Internal is null) { return null; } var r = await SteamUserStats.Internal.DownloadLeaderboardEntries( Id, LeaderboardDataRequest.Friends, 0, 0 ); if ( !r.HasValue ) return null; @@ -116,8 +123,9 @@ namespace Steamworks.Data } #region util - internal async Task LeaderboardResultToEntries( LeaderboardScoresDownloaded_t r ) + internal async Task LeaderboardResultToEntries( LeaderboardScoresDownloaded_t r ) { + if (SteamUserStats.Internal is null) { return null; } if ( r.CEntryCount <= 0 ) return null; @@ -142,6 +150,8 @@ namespace Steamworks.Data bool gotAll = false; while ( !gotAll ) { + if (SteamFriends.Internal is null) { return; } + gotAll = true; foreach ( var entry in entries ) diff --git a/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs b/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs index 82eb26fb7..6758da17d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs +++ b/Libraries/Facepunch.Steamworks/Structs/LeaderboardEntry.cs @@ -7,7 +7,7 @@ namespace Steamworks.Data public Friend User; public int GlobalRank; public int Score; - public int[] Details; + public int[]? Details; // UGCHandle_t m_hUGC internal static LeaderboardEntry From( LeaderboardEntry_t e, int[] detailsBuffer ) diff --git a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs index 2b0fdb57a..397e82350 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Lobby.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Lobby.cs @@ -23,6 +23,8 @@ namespace Steamworks.Data /// public async Task Join() { + if (SteamMatchmaking.Internal is null) { return RoomEnter.Error; } + var result = await SteamMatchmaking.Internal.JoinLobby( Id ); if ( !result.HasValue ) return RoomEnter.Error; @@ -35,7 +37,7 @@ namespace Steamworks.Data /// public void Leave() { - SteamMatchmaking.Internal.LeaveLobby( Id ); + SteamMatchmaking.Internal?.LeaveLobby( Id ); } /// @@ -45,13 +47,13 @@ namespace Steamworks.Data /// public bool InviteFriend( SteamId steamid ) { - return SteamMatchmaking.Internal.InviteUserToLobby( Id, steamid ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.InviteUserToLobby( Id, steamid ); } /// /// returns the number of users in the specified lobby /// - public int MemberCount => SteamMatchmaking.Internal.GetNumLobbyMembers( Id ); + public int MemberCount => SteamMatchmaking.Internal?.GetNumLobbyMembers( Id ) ?? 0; /// /// Returns current members. Need to be in the lobby to see the users. @@ -62,6 +64,7 @@ namespace Steamworks.Data { for( int i = 0; i < MemberCount; i++ ) { + if (SteamMatchmaking.Internal is null) { break; } yield return new Friend( SteamMatchmaking.Internal.GetLobbyMemberByIndex( Id, i ) ); } } @@ -71,9 +74,9 @@ namespace Steamworks.Data /// /// Get data associated with this lobby /// - public string GetData( string key ) + public string? GetData( string key ) { - return SteamMatchmaking.Internal.GetLobbyData( Id, key ); + return SteamMatchmaking.Internal?.GetLobbyData( Id, key ); } /// @@ -84,7 +87,7 @@ namespace Steamworks.Data if ( key.Length > 255 ) throw new System.ArgumentException( "Key should be < 255 chars", nameof( key ) ); if ( value.Length > 8192 ) throw new System.ArgumentException( "Value should be < 8192 chars", nameof( key ) ); - return SteamMatchmaking.Internal.SetLobbyData( Id, key, value ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyData( Id, key, value ); } /// @@ -92,7 +95,7 @@ namespace Steamworks.Data /// public bool DeleteData( string key ) { - return SteamMatchmaking.Internal.DeleteLobbyData( Id, key ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.DeleteLobbyData( Id, key ); } /// @@ -102,10 +105,11 @@ namespace Steamworks.Data { get { - var cnt = SteamMatchmaking.Internal.GetLobbyDataCount( Id ); + var cnt = SteamMatchmaking.Internal?.GetLobbyDataCount( Id ) ?? 0; for ( int i =0; i( a, b ); @@ -117,9 +121,9 @@ namespace Steamworks.Data /// /// Gets per-user metadata for someone in this lobby /// - public string GetMemberData( Friend member, string key ) + public string? GetMemberData( Friend member, string key ) { - return SteamMatchmaking.Internal.GetLobbyMemberData( Id, member.Id, key ); + return SteamMatchmaking.Internal?.GetLobbyMemberData( Id, member.Id, key ); } /// @@ -127,7 +131,7 @@ namespace Steamworks.Data /// public void SetMemberData( string key, string value ) { - SteamMatchmaking.Internal.SetLobbyMemberData( Id, key, value ); + SteamMatchmaking.Internal?.SetLobbyMemberData( Id, key, value ); } /// @@ -148,7 +152,7 @@ namespace Steamworks.Data { fixed ( byte* ptr = data ) { - return SteamMatchmaking.Internal.SendLobbyChatMsg( Id, (IntPtr)ptr, data.Length ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SendLobbyChatMsg( Id, (IntPtr)ptr, data.Length ); } } @@ -163,7 +167,7 @@ namespace Steamworks.Data /// public bool Refresh() { - return SteamMatchmaking.Internal.RequestLobbyData( Id ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.RequestLobbyData( Id ); } /// @@ -172,33 +176,33 @@ namespace Steamworks.Data /// public int MaxMembers { - get => SteamMatchmaking.Internal.GetLobbyMemberLimit( Id ); - set => SteamMatchmaking.Internal.SetLobbyMemberLimit( Id, value ); + get => SteamMatchmaking.Internal?.GetLobbyMemberLimit( Id ) ?? 0; + set => SteamMatchmaking.Internal?.SetLobbyMemberLimit( Id, value ); } public bool SetPublic() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Public ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Public ); } public bool SetPrivate() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Private ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Private ); } public bool SetInvisible() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Invisible ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.Invisible ); } public bool SetFriendsOnly() { - return SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.FriendsOnly ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyType( Id, LobbyType.FriendsOnly ); } public bool SetJoinable( bool b ) { - return SteamMatchmaking.Internal.SetLobbyJoinable( Id, b ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.SetLobbyJoinable( Id, b ); } /// @@ -211,7 +215,7 @@ namespace Steamworks.Data if ( !steamServer.IsValid ) throw new ArgumentException( $"SteamId for server is invalid" ); - SteamMatchmaking.Internal.SetLobbyGameServer( Id, 0, 0, steamServer ); + SteamMatchmaking.Internal?.SetLobbyGameServer( Id, 0, 0, steamServer ); } /// @@ -224,7 +228,7 @@ namespace Steamworks.Data if ( !IPAddress.TryParse( ip, out IPAddress add ) ) throw new ArgumentException( $"IP address for server is invalid" ); - SteamMatchmaking.Internal.SetLobbyGameServer( Id, add.IpToInt32(), port, new SteamId() ); + SteamMatchmaking.Internal?.SetLobbyGameServer( Id, add.IpToInt32(), port, new SteamId() ); } /// @@ -233,7 +237,7 @@ namespace Steamworks.Data /// public bool GetGameServer( ref uint ip, ref ushort port, ref SteamId serverId ) { - return SteamMatchmaking.Internal.GetLobbyGameServer( Id, ref ip, ref port, ref serverId ); + return SteamMatchmaking.Internal != null && SteamMatchmaking.Internal.GetLobbyGameServer( Id, ref ip, ref port, ref serverId ); } /// @@ -241,8 +245,8 @@ namespace Steamworks.Data /// public Friend Owner { - get => new Friend( SteamMatchmaking.Internal.GetLobbyOwner( Id ) ); - set => SteamMatchmaking.Internal.SetLobbyOwner( Id, value.Id ); + get => new Friend( SteamMatchmaking.Internal?.GetLobbyOwner( Id ) ?? 0 ); + set => SteamMatchmaking.Internal?.SetLobbyOwner( Id, value.Id ); } /// diff --git a/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs b/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs index 1401e5820..4a55ef640 100644 --- a/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs @@ -197,6 +197,8 @@ namespace Steamworks.Data void ApplyFilters() { + if (SteamMatchmaking.Internal is null) { return; } + if ( distance.HasValue ) { SteamMatchmaking.Internal.AddRequestLobbyListDistanceFilter( distance.Value ); @@ -251,8 +253,10 @@ namespace Steamworks.Data /// /// Run the query, get the matching lobbies /// - public async Task RequestAsync() + public async Task RequestAsync() { + if (SteamMatchmaking.Internal is null) { return null; } + await Task.Yield(); ApplyFilters(); diff --git a/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs b/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs index b88904221..5254c9f50 100644 --- a/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs +++ b/Libraries/Facepunch.Steamworks/Structs/PartyBeacon.cs @@ -5,7 +5,7 @@ namespace Steamworks { public struct PartyBeacon { - static ISteamParties Internal => SteamParties.Internal; + static ISteamParties? Internal => SteamParties.Internal; internal PartyBeaconID_t Id; @@ -18,7 +18,7 @@ namespace Steamworks { var owner = default( SteamId ); var location = default( SteamPartyBeaconLocation_t ); - Internal.GetBeaconDetails( Id, ref owner, ref location, out _ ); + Internal?.GetBeaconDetails( Id, ref owner, ref location, out _ ); return owner; } } @@ -26,13 +26,14 @@ namespace Steamworks /// /// Creator of the beacon /// - public string MetaData + public string? MetaData { get { var owner = default( SteamId ); var location = default( SteamPartyBeaconLocation_t ); - _ = Internal.GetBeaconDetails( Id, ref owner, ref location, out var strVal ); + string? strVal = null; + _ = Internal?.GetBeaconDetails( Id, ref owner, ref location, out strVal ); return strVal; } } @@ -41,8 +42,10 @@ namespace Steamworks /// Will attempt to join the party. If successful will return a connection string. /// If failed, will return null /// - public async Task JoinAsync() + public async Task JoinAsync() { + if (Internal is null) { return null; } + var result = await Internal.JoinParty( Id ); if ( !result.HasValue || result.Value.Result != Result.OK ) return null; @@ -56,7 +59,7 @@ namespace Steamworks /// public void OnReservationCompleted( SteamId steamid ) { - Internal.OnReservationCompleted( Id, steamid ); + Internal?.OnReservationCompleted( Id, steamid ); } /// @@ -66,7 +69,7 @@ namespace Steamworks /// public void CancelReservation( SteamId steamid ) { - Internal.CancelReservation( Id, steamid ); + Internal?.CancelReservation( Id, steamid ); } /// @@ -74,7 +77,7 @@ namespace Steamworks /// public bool Destroy() { - return Internal.DestroyBeacon( Id ); + return Internal != null && Internal.DestroyBeacon( Id ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs b/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs index f39693fce..655ab288d 100644 --- a/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs +++ b/Libraries/Facepunch.Steamworks/Structs/RemotePlaySession.cs @@ -23,16 +23,16 @@ namespace Steamworks.Data /// /// Get the SteamID of the connected user /// - public SteamId SteamId => SteamRemotePlay.Internal.GetSessionSteamID( Id ); + public SteamId SteamId => SteamRemotePlay.Internal?.GetSessionSteamID( Id ) ?? default; /// /// Get the name of the session client device /// - public string ClientName => SteamRemotePlay.Internal.GetSessionClientName( Id ); + public string? ClientName => SteamRemotePlay.Internal?.GetSessionClientName( Id ); /// /// Get the name of the session client device /// - public SteamDeviceFormFactor FormFactor => SteamRemotePlay.Internal.GetSessionClientFormFactor( Id ); + public SteamDeviceFormFactor FormFactor => SteamRemotePlay.Internal?.GetSessionClientFormFactor( Id ) ?? SteamDeviceFormFactor.Unknown; } } diff --git a/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs b/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs index 2d6e92010..a30b506e4 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Screenshot.cs @@ -15,7 +15,7 @@ namespace Steamworks.Data /// public bool TagUser( SteamId user ) { - return SteamScreenshots.Internal.TagUser( Value, user ); + return SteamScreenshots.Internal != null && SteamScreenshots.Internal.TagUser( Value, user ); } /// @@ -23,7 +23,7 @@ namespace Steamworks.Data /// public bool SetLocation( string location ) { - return SteamScreenshots.Internal.SetLocation( Value, location ); + return SteamScreenshots.Internal != null && SteamScreenshots.Internal.SetLocation( Value, location ); } /// @@ -31,7 +31,7 @@ namespace Steamworks.Data /// public bool TagPublishedFile( PublishedFileId file ) { - return SteamScreenshots.Internal.TagPublishedFile( Value, file ); + return SteamScreenshots.Internal != null && SteamScreenshots.Internal.TagPublishedFile( Value, file ); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/Server.cs b/Libraries/Facepunch.Steamworks/Structs/Server.cs index 89ea2243a..5e74b0a35 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Server.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Server.cs @@ -9,11 +9,11 @@ namespace Steamworks.Data { public struct ServerInfo : IEquatable { - public string Name { get; set; } + public string? Name { get; set; } public int Ping { get; set; } - public string GameDir { get; set; } - public string Map { get; set; } - public string Description { get; set; } + public string? GameDir { get; set; } + public string? Map { get; set; } + public string? Description { get; set; } public uint AppId { get; set; } public int Players { get; set; } public int MaxPlayers { get; set; } @@ -22,19 +22,19 @@ namespace Steamworks.Data public bool Secure { get; set; } public uint LastTimePlayed { get; set; } public int Version { get; set; } - public string TagString { get; set; } + public string? TagString { get; set; } public ulong SteamId { get; set; } public uint AddressRaw { get; set; } - public IPAddress Address { get; set; } + public IPAddress? Address { get; set; } public int ConnectionPort { get; set; } public int QueryPort { get; set; } - string[] _tags; + string[]? _tags; /// /// Gets the individual tags for this server /// - public string[] Tags + public string[]? Tags { get { @@ -97,13 +97,13 @@ namespace Steamworks.Data /// public void AddToHistory() { - SteamMatchmaking.Internal.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory, (uint)Epoch.Current ); + SteamMatchmaking.Internal?.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory, (uint)Epoch.Current ); } /// /// If this server responds to source engine style queries, we'll be able to get a list of rules here /// - public async Task> QueryRulesAsync() + public async Task?> QueryRulesAsync() { return await SourceServerQuery.GetRules( this ); } @@ -113,7 +113,7 @@ namespace Steamworks.Data /// public void RemoveFromHistory() { - SteamMatchmaking.Internal.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory ); + SteamMatchmaking.Internal?.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory ); } /// @@ -121,7 +121,7 @@ namespace Steamworks.Data /// public void AddToFavourites() { - SteamMatchmaking.Internal.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite, (uint)Epoch.Current ); + SteamMatchmaking.Internal?.AddFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite, (uint)Epoch.Current ); } /// @@ -129,7 +129,7 @@ namespace Steamworks.Data /// public void RemoveFromFavourites() { - SteamMatchmaking.Internal.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite ); + SteamMatchmaking.Internal?.RemoveFavoriteGame( SteamClient.AppId, AddressRaw, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite ); } public bool Equals( ServerInfo other ) @@ -139,7 +139,7 @@ namespace Steamworks.Data public override int GetHashCode() { - return Address.GetHashCode() + SteamId.GetHashCode() + ConnectionPort.GetHashCode() + QueryPort.GetHashCode(); + return (Address?.GetHashCode() ?? 0) + SteamId.GetHashCode() + ConnectionPort.GetHashCode() + QueryPort.GetHashCode(); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs index 7664a9bb1..637c85c06 100644 --- a/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs +++ b/Libraries/Facepunch.Steamworks/Structs/ServerInit.cs @@ -21,7 +21,7 @@ namespace Steamworks /// public struct SteamServerInit { - public IPAddress IpAddress; + public IPAddress? IpAddress; public ushort SteamPort; public ushort GamePort; public ushort QueryPort; diff --git a/Libraries/Facepunch.Steamworks/Structs/Stat.cs b/Libraries/Facepunch.Steamworks/Structs/Stat.cs index 559fb6954..505ffeb4b 100644 --- a/Libraries/Facepunch.Steamworks/Structs/Stat.cs +++ b/Libraries/Facepunch.Steamworks/Structs/Stat.cs @@ -26,7 +26,7 @@ namespace Steamworks.Data UserId = user; } - internal void LocalUserOnly( [CallerMemberName] string caller = null ) + internal void LocalUserOnly( [CallerMemberName] string? caller = null ) { if ( UserId == 0 ) return; throw new System.Exception( $"Stat.{caller} can only be called for the local user" ); @@ -36,7 +36,7 @@ namespace Steamworks.Data { double val = 0.0; - if ( SteamUserStats.Internal.GetGlobalStat( Name, ref val ) ) + if ( SteamUserStats.Internal != null && SteamUserStats.Internal.GetGlobalStat( Name, ref val ) ) return val; return 0; @@ -45,12 +45,14 @@ namespace Steamworks.Data public long GetGlobalInt() { long val = 0; - SteamUserStats.Internal.GetGlobalStat( Name, ref val ); + SteamUserStats.Internal?.GetGlobalStat( Name, ref val ); return val; } - public async Task GetGlobalIntDaysAsync( int days ) + public async Task GetGlobalIntDaysAsync( int days ) { + if (SteamUserStats.Internal is null) { return null; } + var result = await SteamUserStats.Internal.RequestGlobalStats( days ); if ( result?.Result != Result.OK ) return null; @@ -64,8 +66,10 @@ namespace Steamworks.Data return r; } - public async Task GetGlobalFloatDays( int days ) + public async Task GetGlobalFloatDays( int days ) { + if (SteamUserStats.Internal is null) { return null; } + var result = await SteamUserStats.Internal.RequestGlobalStats( days ); if ( result?.Result != Result.OK ) return null; @@ -85,14 +89,14 @@ namespace Steamworks.Data if ( UserId > 0 ) { - SteamUserStats.Internal.GetUserStat( UserId, Name, ref val ); + SteamUserStats.Internal?.GetUserStat( UserId, Name, ref val ); } else { - SteamUserStats.Internal.GetStat( Name, ref val ); + SteamUserStats.Internal?.GetStat( Name, ref val ); } - return 0; + return val; } public int GetInt() @@ -101,11 +105,11 @@ namespace Steamworks.Data if ( UserId > 0 ) { - SteamUserStats.Internal.GetUserStat( UserId, Name, ref val ); + SteamUserStats.Internal?.GetUserStat( UserId, Name, ref val ); } else { - SteamUserStats.Internal.GetStat( Name, ref val ); + SteamUserStats.Internal?.GetStat( Name, ref val ); } return val; @@ -114,13 +118,13 @@ namespace Steamworks.Data public bool Set( int val ) { LocalUserOnly(); - return SteamUserStats.Internal.SetStat( Name, val ); + return SteamUserStats.Internal != null && SteamUserStats.Internal.SetStat( Name, val ); } public bool Set( float val ) { LocalUserOnly(); - return SteamUserStats.Internal.SetStat( Name, val ); + return SteamUserStats.Internal != null && SteamUserStats.Internal.SetStat( Name, val ); } public bool Add( int val ) @@ -138,13 +142,13 @@ namespace Steamworks.Data public bool UpdateAverageRate( float count, float sessionlength ) { LocalUserOnly(); - return SteamUserStats.Internal.UpdateAvgRateStat( Name, count, sessionlength ); + return SteamUserStats.Internal != null && SteamUserStats.Internal.UpdateAvgRateStat( Name, count, sessionlength ); } public bool Store() { LocalUserOnly(); - return SteamUserStats.Internal.StoreStats(); + return SteamUserStats.Internal != null && SteamUserStats.Internal.StoreStats(); } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs index 9acaf229c..1067141b5 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcEditor.cs @@ -47,25 +47,25 @@ namespace Steamworks.Ugc public Editor ForAppId( AppId id ) { this.consumerAppId = id; return this; } - public string Title { get; private set; } + public string? Title { get; private set; } public Editor WithTitle( string t ) { this.Title = t; return this; } - public string Description { get; private set; } + public string? Description { get; private set; } public Editor WithDescription( string t ) { this.Description = t; return this; } - string MetaData; + string? MetaData; public Editor WithMetaData( string t ) { this.MetaData = t; return this; } - string ChangeLog; + string? ChangeLog; public Editor WithChangeLog( string t ) { this.ChangeLog = t; return this; } - string Language; + string? Language; public Editor InLanguage( string t ) { this.Language = t; return this; } - public string PreviewFile { get; private set; } - public Editor WithPreviewFile( string t ) { this.PreviewFile = t; return this; } + public string? PreviewFile { get; private set; } + public Editor WithPreviewFile( string? t ) { this.PreviewFile = t; return this; } - public System.IO.DirectoryInfo ContentFolder { get; private set; } + public System.IO.DirectoryInfo? ContentFolder { get; private set; } public Editor WithContent( System.IO.DirectoryInfo t ) { this.ContentFolder = t; return this; } public Editor WithContent( string folderName ) { return WithContent( new System.IO.DirectoryInfo( folderName ) ); } @@ -73,9 +73,9 @@ namespace Steamworks.Ugc public Editor WithVisibility(Visibility visibility) { Visibility = visibility; return this; } - public List Tags { get; private set; } - Dictionary> keyValueTags; - HashSet keyValueTagsToRemove; + public List? Tags { get; private set; } + Dictionary>? keyValueTags; + HashSet? keyValueTagsToRemove; public Editor WithTag( string tag ) { @@ -143,9 +143,10 @@ namespace Steamworks.Ugc return false; } - public async Task SubmitAsync( IProgress progress = null ) + public async Task SubmitAsync( IProgress? progress = null ) { var result = default( PublishResult ); + if (SteamUGC.Internal is null) { return result; } progress?.Report( 0 ); diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs index 7a69e3819..49ff42292 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs @@ -35,22 +35,22 @@ namespace Steamworks.Ugc /// /// The given title of this item /// - public string Title { get; internal set; } + public string? Title { get; internal set; } /// /// The description of this item, in your local language if available /// - public string Description { get; internal set; } + public string? Description { get; internal set; } /// /// A list of tags for this item, all lowercase /// - public string[] Tags { get; internal set; } + public string[]? Tags { get; internal set; } /// /// A dictionary of key value tags for this item, only available from queries WithKeyValueTags(true) /// - public Dictionary KeyValueTags { get; internal set; } + public Dictionary? KeyValueTags { get; internal set; } /// /// App Id of the app that created this item @@ -123,14 +123,14 @@ namespace Steamworks.Ugc public bool IsSubscribed => (State & ItemState.Subscribed) == ItemState.Subscribed; public bool NeedsUpdate => (State & ItemState.NeedsUpdate) == ItemState.NeedsUpdate; - public string Directory + public string? Directory { get { ulong size = 0; uint ts = 0; - if (SteamUGC.Internal.GetItemInstallInfo(Id, ref size, out var strVal, ref ts)) { return strVal; } + if (SteamUGC.Internal != null && SteamUGC.Internal.GetItemInstallInfo(Id, ref size, out var strVal, ref ts)) { return strVal; } return null; } } @@ -147,7 +147,7 @@ namespace Steamworks.Ugc ulong downloaded = 0; ulong total = 0; - if ( SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) + if ( SteamUGC.Internal != null && SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) return (long) total; return -1; @@ -166,7 +166,7 @@ namespace Steamworks.Ugc ulong downloaded = 0; ulong total = 0; - if ( SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) + if ( SteamUGC.Internal != null && SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) ) return (long)downloaded; return -1; @@ -185,7 +185,7 @@ namespace Steamworks.Ugc ulong size = 0; uint ts = 0; - if ( !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) + if ( SteamUGC.Internal is null || !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) return 0; return (long) size; @@ -201,7 +201,7 @@ namespace Steamworks.Ugc { ulong size = 0; uint ts = 0; - if ( !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) + if ( SteamUGC.Internal is null || !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out _, ref ts ) ) return null; return Epoch.ToDateTime(ts); @@ -226,7 +226,7 @@ namespace Steamworks.Ugc //possibly similar properties should also be changed ulong downloaded = 0; ulong total = 0; - if (SteamUGC.Internal.GetItemDownloadInfo(Id, ref downloaded, ref total) && total > 0) + if (SteamUGC.Internal != null && SteamUGC.Internal.GetItemDownloadInfo(Id, ref downloaded, ref total) && total > 0) { return (float)((double)downloaded / (double)total); } @@ -277,7 +277,7 @@ namespace Steamworks.Ugc /// public bool HasTag( string find ) { - if ( Tags.Length == 0 ) return false; + if ( Tags is null || Tags.Length == 0 ) return false; return Tags.Contains( find, StringComparer.OrdinalIgnoreCase ); } @@ -287,6 +287,7 @@ namespace Steamworks.Ugc /// public async Task Subscribe () { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.SubscribeItem( _id ); return result?.Result == Result.OK; } @@ -296,7 +297,7 @@ namespace Steamworks.Ugc /// If CancellationToken is default then there is 60 seconds timeout /// Progress will be set to 0-1 /// - public async Task DownloadAsync( Action progress = null, int milisecondsUpdateDelay = 60, CancellationToken ct = default ) + public async Task DownloadAsync( Action? progress = null, int milisecondsUpdateDelay = 60, CancellationToken ct = default ) { return await SteamUGC.DownloadAsync( Id, progress, milisecondsUpdateDelay, ct ); } @@ -305,7 +306,8 @@ namespace Steamworks.Ugc /// Allows the user to unsubscribe from this item /// public async Task Unsubscribe () - { + { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.UnsubscribeItem( _id ); return result?.Result == Result.OK; } @@ -315,6 +317,7 @@ namespace Steamworks.Ugc /// public async Task AddFavorite() { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.AddItemToFavorites(details.ConsumerAppID, _id); return result?.Result == Result.OK; } @@ -324,6 +327,7 @@ namespace Steamworks.Ugc /// public async Task RemoveFavorite() { + if (SteamUGC.Internal is null) { return false; } var result = await SteamUGC.Internal.RemoveItemFromFavorites(details.ConsumerAppID, _id); return result?.Result == Result.OK; } @@ -333,6 +337,7 @@ namespace Steamworks.Ugc /// public async Task Vote( bool up ) { + if (SteamUGC.Internal is null) { return null; } var r = await SteamUGC.Internal.SetUserItemVote( Id, up ); return r?.Result; } @@ -342,6 +347,7 @@ namespace Steamworks.Ugc /// public async Task GetUserVote() { + if (SteamUGC.Internal is null) { return null; } var result = await SteamUGC.Internal.GetUserItemVote(_id); if (!result.HasValue) return null; @@ -351,27 +357,27 @@ namespace Steamworks.Ugc /// /// Return a URL to view this item online /// - public string Url => $"http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={Id}"; + public string Url => $"https://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={Id}"; /// /// The URl to view this item's changelog /// - public string ChangelogUrl => $"http://steamcommunity.com/sharedfiles/filedetails/changelog/{Id}"; + public string ChangelogUrl => $"https://steamcommunity.com/sharedfiles/filedetails/changelog/{Id}"; /// /// The URL to view the comments on this item /// - public string CommentsUrl => $"http://steamcommunity.com/sharedfiles/filedetails/comments/{Id}"; + public string CommentsUrl => $"https://steamcommunity.com/sharedfiles/filedetails/comments/{Id}"; /// /// The URL to discuss this item /// - public string DiscussUrl => $"http://steamcommunity.com/sharedfiles/filedetails/discussions/{Id}"; + public string DiscussUrl => $"https://steamcommunity.com/sharedfiles/filedetails/discussions/{Id}"; /// /// The URL to view this items stats online /// - public string StatsUrl => $"http://steamcommunity.com/sharedfiles/filedetails/stats/{Id}"; + public string StatsUrl => $"https://steamcommunity.com/sharedfiles/filedetails/stats/{Id}"; public ulong NumSubscriptions { get; internal set; } public ulong NumFavorites { get; internal set; } @@ -390,12 +396,12 @@ namespace Steamworks.Ugc /// /// The URL to the preview image for this item /// - public string PreviewImageUrl { get; internal set; } + public string? PreviewImageUrl { get; internal set; } /// /// The metadata string for this item, only available from queries WithMetadata(true) /// - public string Metadata { get; internal set; } + public string? Metadata { get; internal set; } /// /// Edit this item diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs index b8bc42740..cfb65ca75 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs @@ -14,7 +14,7 @@ namespace Steamworks.Ugc UGCQuery queryType; AppId consumerApp; AppId creatorApp; - string searchText; + string? searchText; public Query( UgcType type ) : this() { @@ -96,7 +96,7 @@ namespace Steamworks.Ugc #endregion #region Files - PublishedFileId[] Files; + PublishedFileId[]? Files; public Query WithFileId( params PublishedFileId[] files ) { @@ -109,6 +109,8 @@ namespace Steamworks.Ugc { if ( page <= 0 ) throw new System.Exception( "page should be > 0" ); + if (SteamUGC.Internal is null) { return null; } + if ( consumerApp == 0 ) consumerApp = SteamClient.AppId; if ( creatorApp == 0 ) creatorApp = consumerApp; @@ -159,16 +161,16 @@ namespace Steamworks.Ugc public QueryType WithType( UgcType type ) { matchingType = type; return this; } int? maxCacheAge; public QueryType AllowCachedResponse( int maxSecondsAge ) { maxCacheAge = maxSecondsAge; return this; } - string language; + string? language; public QueryType InLanguage( string lang ) { language = lang; return this; } int? trendDays; public QueryType WithTrendDays( int days ) { trendDays = days; return this; } - List requiredTags; + List? requiredTags; bool? matchAnyTag; - List excludedTags; - Dictionary requiredKv; + List? excludedTags; + Dictionary? requiredKv; /// /// Found items must have at least one of the defined tags @@ -213,34 +215,34 @@ namespace Steamworks.Ugc if ( requiredTags != null ) { foreach ( var tag in requiredTags ) - SteamUGC.Internal.AddRequiredTag( handle, tag ); + SteamUGC.Internal?.AddRequiredTag( handle, tag ); } if ( excludedTags != null ) { foreach ( var tag in excludedTags ) - SteamUGC.Internal.AddExcludedTag( handle, tag ); + SteamUGC.Internal?.AddExcludedTag( handle, tag ); } if ( requiredKv != null ) { foreach ( var tag in requiredKv ) - SteamUGC.Internal.AddRequiredKeyValueTag( handle, tag.Key, tag.Value ); + SteamUGC.Internal?.AddRequiredKeyValueTag( handle, tag.Key, tag.Value ); } if ( matchAnyTag.HasValue ) { - SteamUGC.Internal.SetMatchAnyTag( handle, matchAnyTag.Value ); + SteamUGC.Internal?.SetMatchAnyTag( handle, matchAnyTag.Value ); } if ( trendDays.HasValue ) { - SteamUGC.Internal.SetRankedByTrendDays( handle, (uint)trendDays.Value ); + SteamUGC.Internal?.SetRankedByTrendDays( handle, (uint)trendDays.Value ); } if ( !string.IsNullOrEmpty( searchText ) ) { - SteamUGC.Internal.SetSearchText( handle, searchText ); + SteamUGC.Internal?.SetSearchText( handle, searchText ); } } @@ -271,42 +273,42 @@ namespace Steamworks.Ugc { if (WantsReturnOnlyIDs.HasValue) { - SteamUGC.Internal.SetReturnOnlyIDs(handle, WantsReturnOnlyIDs.Value); + SteamUGC.Internal?.SetReturnOnlyIDs(handle, WantsReturnOnlyIDs.Value); } if (WantsReturnKeyValueTags.HasValue) { - SteamUGC.Internal.SetReturnKeyValueTags(handle, WantsReturnKeyValueTags.Value); + SteamUGC.Internal?.SetReturnKeyValueTags(handle, WantsReturnKeyValueTags.Value); } if (WantsReturnLongDescription.HasValue) { - SteamUGC.Internal.SetReturnLongDescription(handle, WantsReturnLongDescription.Value); + SteamUGC.Internal?.SetReturnLongDescription(handle, WantsReturnLongDescription.Value); } if (WantsReturnMetadata.HasValue) { - SteamUGC.Internal.SetReturnMetadata(handle, WantsReturnMetadata.Value); + SteamUGC.Internal?.SetReturnMetadata(handle, WantsReturnMetadata.Value); } if (WantsReturnChildren.HasValue) { - SteamUGC.Internal.SetReturnChildren(handle, WantsReturnChildren.Value); + SteamUGC.Internal?.SetReturnChildren(handle, WantsReturnChildren.Value); } if (WantsReturnAdditionalPreviews.HasValue) { - SteamUGC.Internal.SetReturnAdditionalPreviews(handle, WantsReturnAdditionalPreviews.Value); + SteamUGC.Internal?.SetReturnAdditionalPreviews(handle, WantsReturnAdditionalPreviews.Value); } if (WantsReturnTotalOnly.HasValue) { - SteamUGC.Internal.SetReturnTotalOnly(handle, WantsReturnTotalOnly.Value); + SteamUGC.Internal?.SetReturnTotalOnly(handle, WantsReturnTotalOnly.Value); } if (WantsReturnPlaytimeStats.HasValue) { - SteamUGC.Internal.SetReturnPlaytimeStats(handle, WantsReturnPlaytimeStats.Value); + SteamUGC.Internal?.SetReturnPlaytimeStats(handle, WantsReturnPlaytimeStats.Value); } } diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs b/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs index 86a6e2b51..54ffe6973 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcResultPage.cs @@ -24,6 +24,7 @@ namespace Steamworks.Ugc var details = default( SteamUGCDetails_t ); for ( uint i=0; i< ResultCount; i++ ) { + if (SteamUGC.Internal is null) { yield break; } if ( SteamUGC.Internal.GetQueryUGCResult( Handle, i, ref details ) ) { var item = Item.From( details ); @@ -86,7 +87,7 @@ namespace Steamworks.Ugc { ulong val = 0; - if ( !SteamUGC.Internal.GetQueryUGCStatistic( Handle, index, stat, ref val ) ) + if ( SteamUGC.Internal is null || !SteamUGC.Internal.GetQueryUGCStatistic( Handle, index, stat, ref val ) ) return 0; return val; @@ -96,7 +97,7 @@ namespace Steamworks.Ugc { if ( Handle > 0 ) { - SteamUGC.Internal.ReleaseQueryUGCRequest( Handle ); + SteamUGC.Internal?.ReleaseQueryUGCRequest( Handle ); Handle = 0; } } diff --git a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs index bc1c4d848..0f35438bd 100644 --- a/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs +++ b/Libraries/Facepunch.Steamworks/Utility/SourceServerQuery.cs @@ -16,13 +16,13 @@ namespace Steamworks private static readonly HashSet ruleResponseHandlers = new HashSet(); - internal static async Task> GetRules(Steamworks.Data.ServerInfo server) + internal static async Task?> GetRules(Steamworks.Data.ServerInfo server) { Status status = Status.Pending; var rules = new Dictionary(); - SteamMatchmakingRulesResponse responseHandler = null; + SteamMatchmakingRulesResponse? responseHandler = null; void onRulesResponded(string key, string value) => rules.Add(key, value); @@ -51,6 +51,8 @@ namespace Steamworks responseHandler = null; } + if (SteamMatchmakingServers.Internal is null) { return null; } + responseHandler = new SteamMatchmakingRulesResponse( onRulesResponded, onRulesFailToRespond, diff --git a/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs b/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs index a8b13dab4..ca0b5a20c 100644 --- a/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs +++ b/Libraries/Facepunch.Steamworks/Utility/SteamInterface.cs @@ -61,9 +61,9 @@ namespace Steamworks public class SteamSharedClass : SteamClass { - internal static SteamInterface Interface => InterfaceClient ?? InterfaceServer; - internal static SteamInterface InterfaceClient; - internal static SteamInterface InterfaceServer; + internal static SteamInterface? Interface => InterfaceClient ?? InterfaceServer; + internal static SteamInterface? InterfaceClient; + internal static SteamInterface? InterfaceServer; internal override void InitializeInterface( bool server ) { @@ -99,7 +99,7 @@ namespace Steamworks public class SteamClientClass : SteamClass { - internal static SteamInterface Interface; + internal static SteamInterface? Interface; internal override void InitializeInterface( bool server ) { @@ -122,7 +122,7 @@ namespace Steamworks public class SteamServerClass : SteamClass { - internal static SteamInterface Interface; + internal static SteamInterface? Interface; internal override void InitializeInterface( bool server ) { diff --git a/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs b/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs index 1d02c3bf1..59d1517f9 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Utf8String.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -48,12 +49,13 @@ namespace Steamworks internal IntPtr ptr; #pragma warning restore 649 - public unsafe static implicit operator string( Utf8StringPointer p ) + [return: NotNullIfNotNull("p")] + public unsafe static implicit operator string?( Utf8StringPointer p ) { - return ConvertPtrToString(p.ptr); + return ConvertPtrToString(p.ptr)!; } - public unsafe static string ConvertPtrToString(IntPtr ptr) + public unsafe static string? ConvertPtrToString(IntPtr ptr) { if (ptr == IntPtr.Zero) return null; diff --git a/Libraries/Facepunch.Steamworks/Utility/Utility.cs b/Libraries/Facepunch.Steamworks/Utility/Utility.cs index 3365d2ff4..ad77d63d2 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Utility.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Utility.cs @@ -10,7 +10,7 @@ namespace Steamworks { public static partial class Utility { - static internal T ToType( this IntPtr ptr ) + static internal T? ToType( this IntPtr ptr ) { if ( ptr == IntPtr.Zero ) return default; @@ -18,7 +18,7 @@ namespace Steamworks return (T)Marshal.PtrToStructure( ptr, typeof( T ) ); } - static internal object ToType( this IntPtr ptr, System.Type t ) + static internal object? ToType( this IntPtr ptr, System.Type t ) { if ( ptr == IntPtr.Zero ) return default;