From b91e85559da72263c1b64a13d31278842586e5a0 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Thu, 14 Dec 2023 16:11:27 +0200 Subject: [PATCH] v1.2.6.0 (Winter Update) --- .../BarotraumaClient/ClientSource/Camera.cs | 19 +- .../ClientSource/Characters/Character.cs | 30 +- .../ClientSource/Characters/CharacterHUD.cs | 2 +- .../ClientSource/Characters/CharacterInfo.cs | 14 +- .../Characters/Health/CharacterHealth.cs | 2 + .../ClientSource/Characters/Limb.cs | 2 +- .../ClientSource/DebugConsole.cs | 45 ++- .../Events/EventActions/ConversationAction.cs | 3 +- .../EventActions/EventObjectiveAction.cs | 10 +- ...lHighlightAction.cs => HighlightAction.cs} | 17 +- .../ClientSource/Events/EventManager.cs | 11 +- .../Events/Missions/CargoMission.cs | 3 +- .../ClientSource/Events/Missions/Mission.cs | 8 +- .../ClientSource/GUI/CrewManagement.cs | 56 +-- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 18 +- .../ClientSource/GUI/GUIComponent.cs | 21 +- .../ClientSource/GUI/GUIMessageBox.cs | 12 + .../ClientSource/GUI/GUINumberInput.cs | 67 ++-- .../ClientSource/GUI/GUIPrefab.cs | 9 +- .../ClientSource/GUI/GUIStyle.cs | 72 ++-- .../ClientSource/GUI/GUITextBlock.cs | 9 +- .../ClientSource/GUI/GUITextBox.cs | 4 + .../ClientSource/GUI/RectTransform.cs | 96 ++++- .../ClientSource/GUI/Store.cs | 85 +++-- .../ClientSource/GUI/SubmarineSelection.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 9 +- .../ClientSource/GUI/UISprite.cs | 2 +- .../ClientSource/GUI/UpgradeStore.cs | 6 +- .../BarotraumaClient/ClientSource/GameMain.cs | 6 +- .../ClientSource/GameSession/CargoManager.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 21 +- .../GameModes/MultiPlayerCampaign.cs | 19 +- .../GameModes/SinglePlayerCampaign.cs | 9 +- .../ClientSource/GameSession/GameSession.cs | 12 +- .../GameSession/ObjectiveManager.cs | 20 +- .../ClientSource/GameSession/RoundSummary.cs | 16 +- .../ClientSource/Items/CharacterInventory.cs | 12 +- .../Items/Components/GeneticMaterial.cs | 6 +- .../Items/Components/ItemComponent.cs | 28 +- .../Items/Components/ItemContainer.cs | 16 +- .../Items/Components/ItemLabel.cs | 2 +- .../Items/Components/LightComponent.cs | 2 +- .../Items/Components/Machines/Engine.cs | 6 +- .../Items/Components/Machines/Fabricator.cs | 104 +++++- .../Items/Components/Machines/Sonar.cs | 65 ++-- .../Items/Components/Machines/Steering.cs | 19 +- .../Items/Components/RepairTool.cs | 2 +- .../Components/Signal/CustomInterface.cs | 7 +- .../Items/Components/StatusHUD.cs | 6 +- .../ClientSource/Items/Components/Turret.cs | 16 + .../ClientSource/Items/Inventory.cs | 86 +++-- .../ClientSource/Items/Item.cs | 177 +++++++--- .../ClientSource/Items/ItemPrefab.cs | 28 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 19 +- .../ClientSource/Map/ItemAssemblyPrefab.cs | 2 +- .../BackgroundCreatureManager.cs | 2 +- .../ClientSource/Map/Levels/Level.cs | 9 +- .../Map/Levels/LevelObjects/LevelObject.cs | 2 + .../Levels/LevelObjects/LevelObjectManager.cs | 9 +- .../ClientSource/Map/Lights/ConvexHull.cs | 21 ++ .../ClientSource/Map/Lights/LightManager.cs | 14 + .../ClientSource/Map/Lights/LightSource.cs | 130 +++++-- .../ClientSource/Map/Map/Map.cs | 10 +- .../ClientSource/Map/Map/Radiation.cs | 2 +- .../ClientSource/Map/MapEntity.cs | 166 ++++++--- .../ClientSource/Map/MapEntityPrefab.cs | 2 +- .../ClientSource/Map/RoundSound.cs | 20 +- .../ClientSource/Map/Structure.cs | 54 ++- .../ClientSource/Map/StructurePrefab.cs | 21 +- .../ClientSource/Map/Submarine.cs | 14 +- .../ClientSource/Map/SubmarinePreview.cs | 28 +- .../ClientSource/Networking/Client.cs | 6 +- .../ClientSource/Networking/GameClient.cs | 27 +- .../Networking/ServerList/ServerInfo.cs | 108 ++++-- .../ClientSource/Networking/ServerSettings.cs | 4 + .../ClientSource/Particles/ParticleEmitter.cs | 22 +- .../ClientSource/Particles/ParticlePrefab.cs | 3 +- .../BarotraumaClient/ClientSource/Program.cs | 13 +- .../ClientSource/Screens/CampaignUI.cs | 8 +- .../ClientSource/Screens/GameScreen.cs | 20 +- .../ClientSource/Screens/LevelEditorScreen.cs | 12 +- .../ClientSource/Screens/MainMenuScreen.cs | 116 ++++-- .../ClientSource/Screens/NetLobbyScreen.cs | 48 ++- .../ServerListScreen/ServerListScreen.cs | 188 +++++++++- .../ClientSource/Screens/SubEditorScreen.cs | 326 +++++++++-------- .../Serialization/SerializableEntityEditor.cs | 31 +- .../ClientSource/Settings/SettingsMenu.cs | 16 +- .../ClientSource/Sounds/OggSound.cs | 13 +- .../ClientSource/Sounds/Sound.cs | 6 +- .../ClientSource/Sounds/SoundChannel.cs | 15 +- .../ClientSource/Sounds/SoundManager.cs | 57 +-- .../ClientSource/Sounds/SoundPlayer.cs | 5 + .../ClientSource/Sounds/SoundPrefab.cs | 2 + .../ClientSource/SpamServerFilter.cs | 330 ++++++++++++++++++ .../ClientSource/Sprite/Sprite.cs | 54 ++- .../StatusEffects/StatusEffect.cs | 2 +- .../ClientSource/Steam/BulkDownloader.cs | 36 +- .../ClientSource/Steam/Lobby.cs | 4 + .../ClientSource/Steam/Workshop.cs | 23 +- .../ClientSource/SubEditorCommands.cs | 83 +++-- .../Utils/{Quad.cs => GraphicsQuad.cs} | 2 +- .../ClientSource/Utils/SpriteRecorder.cs | 11 +- .../ClientSource/Utils/TextureLoader.cs | 9 +- .../ClientSource/Utils/WikiImage.cs | 2 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Characters/CharacterInfo.cs | 4 +- .../ServerSource/DebugConsole.cs | 145 +++++--- .../Events/EventActions/EventLogAction.cs | 5 +- .../Events/EventActions/HighlightAction.cs | 24 ++ .../ServerSource/Events/Missions/Mission.cs | 8 +- .../BarotraumaServer/ServerSource/GameMain.cs | 25 +- .../ServerSource/GameSession/CargoManager.cs | 32 +- .../GameSession/GameModes/CampaignMode.cs | 12 +- .../GameModes/CharacterCampaignData.cs | 16 +- .../GameModes/MultiPlayerCampaign.cs | 107 ++++-- .../Items/Components/Signal/CircuitBox.cs | 3 +- .../ServerSource/Items/Inventory.cs | 8 +- .../ServerSource/Items/Item.cs | 14 + .../ServerSource/Items/ItemEventData.cs | 21 +- .../BarotraumaServer/ServerSource/Map/Hull.cs | 4 +- .../ServerSource/Networking/GameServer.cs | 89 +++-- .../ServerEntityEventManager.cs | 5 +- .../ServerSource/Networking/RespawnManager.cs | 2 +- .../ServerSource/Networking/ServerSettings.cs | 2 +- .../BarotraumaServer/ServerSource/Program.cs | 16 +- .../ServerSource/Steam/SteamManager.cs | 15 +- .../ServerSource/Traitors/TraitorManager.cs | 23 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Data/campaignsettings.xml | 12 +- .../Characters/AI/AIController.cs | 7 +- .../Characters/AI/EnemyAIController.cs | 28 +- .../Characters/AI/HumanAIController.cs | 129 +++---- .../Characters/AI/Objectives/AIObjective.cs | 11 - .../Objectives/AIObjectiveCheckStolenItems.cs | 160 +++++++++ .../AI/Objectives/AIObjectiveCombat.cs | 6 +- .../Objectives/AIObjectiveEscapeHandcuffs.cs | 10 +- .../AI/Objectives/AIObjectiveFindThieves.cs | 152 ++++++++ .../AI/Objectives/AIObjectiveIdle.cs | 2 +- .../AI/Objectives/AIObjectiveLoop.cs | 2 +- .../AI/Objectives/AIObjectiveManager.cs | 4 + .../SharedSource/Characters/AI/Order.cs | 17 +- .../Characters/Animation/AnimController.cs | 3 +- .../Animation/HumanoidAnimController.cs | 255 ++++++++------ .../Characters/Animation/Ragdoll.cs | 144 +++++--- .../SharedSource/Characters/Attack.cs | 68 +++- .../SharedSource/Characters/Character.cs | 228 +++++++----- .../SharedSource/Characters/CharacterInfo.cs | 55 ++- .../Characters/CharacterPrefab.cs | 3 +- .../Health/Afflictions/AfflictionHusk.cs | 27 +- .../Health/Afflictions/AfflictionPrefab.cs | 31 +- .../Characters/Health/CharacterHealth.cs | 14 +- .../Characters/Health/DamageModifier.cs | 13 +- .../SharedSource/Characters/HumanPrefab.cs | 13 +- .../SharedSource/Characters/Jobs/Job.cs | 5 +- .../SharedSource/Characters/Jobs/JobPrefab.cs | 3 +- .../Params/Animation/AnimationParams.cs | 3 +- .../Params/Animation/FishAnimations.cs | 3 +- .../Characters/Params/CharacterParams.cs | 20 +- .../Characters/Params/EditableParams.cs | 6 +- .../Params/Ragdoll/RagdollParams.cs | 50 +-- .../SharedSource/Characters/SkillSettings.cs | 7 + .../AbilityConditionals/AbilityCondition.cs | 5 +- .../AbilityConditionAffliction.cs | 5 +- .../AbilityConditionAttackData.cs | 3 +- .../AbilityConditionCharacter.cs | 59 +++- .../AbilityConditionCharacterNotLooted.cs | 8 +- .../AbilityConditionCharacterUnconcious.cs | 8 +- .../AbilityConditionData.cs | 6 +- .../AbilityConditionItem.cs | 3 +- .../AbilityConditionItemIsStatic.cs | 19 + .../AbilityConditionMission.cs | 3 +- .../AbilityConditionHasPermanentStat.cs | 16 +- .../AbilityConditionHasStatusTag.cs | 3 +- .../AbilityConditionLowestLevel.cs | 13 +- .../Talents/Abilities/CharacterAbility.cs | 26 +- .../Abilities/CharacterAbilityApplyForce.cs | 3 +- ...ilityApplyStatusEffectsToApprenticeship.cs | 3 +- .../CharacterAbilityGainSimultaneousSkill.cs | 3 +- .../CharacterAbilityGiveAffliction.cs | 6 +- .../CharacterAbilityGiveExperience.cs | 22 +- .../Abilities/CharacterAbilityGiveItemStat.cs | 4 +- .../CharacterAbilityGiveItemStatToTags.cs | 4 +- .../Abilities/CharacterAbilityGiveMoney.cs | 3 +- .../CharacterAbilityGivePermanentStat.cs | 41 ++- .../CharacterAbilityGiveReputation.cs | 6 +- .../CharacterAbilityGiveResistance.cs | 6 +- .../CharacterAbilityGiveTalentPoints.cs | 3 +- ...haracterAbilityGiveTalentPointsToAllies.cs | 3 +- .../CharacterAbilityIncreaseSkill.cs | 6 +- .../Abilities/CharacterAbilityMarkAsLooted.cs | 3 +- .../CharacterAbilityModifyResistance.cs | 6 +- .../Abilities/CharacterAbilityModifyValue.cs | 3 +- .../Abilities/CharacterAbilityPutItem.cs | 9 +- .../CharacterAbilityReduceAffliction.cs | 3 +- .../CharacterAbilityResetPermanentStat.cs | 3 +- .../CharacterAbilitySetMetadataInt.cs | 3 +- ...erAbilityUnlockApprenticeshipTalentTree.cs | 3 +- .../AbilityGroups/CharacterAbilityGroup.cs | 48 ++- .../Characters/Talents/CharacterTalent.cs | 17 +- .../Characters/Talents/TalentPrefab.cs | 3 +- .../Characters/Talents/TalentTree.cs | 12 +- .../CircuitBox/ItemSlotIndexPair.cs | 19 +- .../ContentFile/AfflictionsFile.cs | 8 +- .../ContentFile/CharacterFile.cs | 7 +- .../ContentFile/ContentFile.cs | 3 +- .../ContentFile/GenericPrefabFile.cs | 2 +- .../ContentFile/NPCConversationsFile.cs | 1 + .../ContentFile/OrdersFile.cs | 3 +- .../ContentFile/RandomEventsFile.cs | 3 +- .../ContentManagement/ContentFile/TextFile.cs | 25 +- .../ContentPackage/ContentPackage.cs | 5 +- .../ContentPackageManager.cs | 1 + .../ContentManagement/ContentXElement.cs | 3 +- .../SharedSource/DebugConsole.cs | 135 ++++--- .../BarotraumaShared/SharedSource/Enums.cs | 27 +- .../SharedSource/Events/ArtifactEvent.cs | 10 +- .../SharedSource/Events/Event.cs | 2 +- .../EventActions/CheckConditionalAction.cs | 116 ++++-- .../Events/EventActions/CheckDataAction.cs | 9 +- .../Events/EventActions/CheckItemAction.cs | 6 +- .../Events/EventActions/CheckOrderAction.cs | 3 +- .../EventActions/CheckReputationAction.cs | 8 +- .../EventActions/CheckSelectedAction.cs | 4 +- .../CheckTraitorEventStateAction.cs | 3 +- .../EventActions/CheckTraitorVoteAction.cs | 3 +- .../EventActions/CheckVisibilityAction.cs | 19 +- .../Events/EventActions/ConversationAction.cs | 3 +- .../Events/EventActions/CountTargetsAction.cs | 6 +- .../Events/EventActions/EventAction.cs | 39 ++- .../Events/EventActions/EventLogAction.cs | 6 +- .../EventActions/EventObjectiveAction.cs | 8 +- .../Events/EventActions/GiveExpAction.cs | 3 +- .../Events/EventActions/GiveSkillExpAction.cs | 3 +- .../SharedSource/Events/EventActions/GoTo.cs | 18 +- .../Events/EventActions/HighlightAction.cs | 43 +++ .../Events/EventActions/MissionAction.cs | 19 +- .../Events/EventActions/MissionStateAction.cs | 3 +- .../EventActions/ModifyLocationAction.cs | 15 +- .../EventActions/NPCChangeTeamAction.cs | 5 +- .../Events/EventActions/RNGAction.cs | 6 +- .../Events/EventActions/ReputationAction.cs | 6 +- .../SetTraitorEventStateAction.cs | 3 +- .../Events/EventActions/SkillCheckAction.cs | 3 +- .../Events/EventActions/SpawnAction.cs | 9 +- .../Events/EventActions/TagAction.cs | 81 +++-- .../Events/EventActions/TriggerEventAction.cs | 3 +- .../EventActions/TutorialHighlightAction.cs | 33 -- .../WaitForItemFabricatedAction.cs | 3 +- .../EventActions/WaitForItemUsedAction.cs | 31 +- .../SharedSource/Events/EventManager.cs | 17 +- .../SharedSource/Events/EventPrefab.cs | 38 +- .../SharedSource/Events/EventSet.cs | 109 +++++- .../Missions/AbandonedOutpostMission.cs | 16 +- .../Events/Missions/AlienRuinMission.cs | 18 +- .../Events/Missions/BeaconMission.cs | 4 +- .../Events/Missions/CargoMission.cs | 5 +- .../Events/Missions/CombatMission.cs | 2 +- .../Events/Missions/EndMission.cs | 21 +- .../Events/Missions/EscortMission.cs | 16 +- .../Events/Missions/MineralMission.cs | 6 +- .../SharedSource/Events/Missions/Mission.cs | 37 +- .../Events/Missions/MissionPrefab.cs | 18 +- .../Events/Missions/MonsterMission.cs | 12 +- .../Events/Missions/NestMission.cs | 9 +- .../Events/Missions/PirateMission.cs | 54 ++- .../Events/Missions/SalvageMission.cs | 15 +- .../Events/Missions/ScanMission.cs | 15 +- .../SharedSource/Events/MonsterEvent.cs | 99 ++++-- .../SharedSource/Events/ScriptedEvent.cs | 113 ++++-- .../Extensions/IEnumerableExtensions.cs | 5 + .../SharedSource/GameSession/CargoManager.cs | 174 ++++++--- .../SharedSource/GameSession/CrewManager.cs | 11 +- .../GameSession/Data/CampaignMetadata.cs | 9 +- .../GameSession/Data/Reputation.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 85 +++-- .../GameSession/GameModes/CampaignSettings.cs | 6 + .../GameModes/MultiPlayerCampaign.cs | 7 +- .../SharedSource/GameSession/GameSession.cs | 11 +- .../SharedSource/GameSession/HireManager.cs | 20 +- .../GameSession/UpgradeManager.cs | 34 +- .../SharedSource/Items/CharacterInventory.cs | 28 +- .../SharedSource/Items/Components/Door.cs | 34 +- .../Items/Components/ElectricalDischarger.cs | 3 +- .../Items/Components/GeneticMaterial.cs | 6 +- .../SharedSource/Items/Components/Growable.cs | 3 +- .../Items/Components/Holdable/IdCard.cs | 3 - .../Items/Components/Holdable/MeleeWeapon.cs | 5 +- .../Items/Components/Holdable/Pickable.cs | 2 +- .../Items/Components/Holdable/RangedWeapon.cs | 3 +- .../Items/Components/Holdable/RepairTool.cs | 20 +- .../Items/Components/Holdable/Throwable.cs | 2 +- .../Items/Components/ItemComponent.cs | 56 ++- .../Items/Components/ItemContainer.cs | 35 +- .../Items/Components/Machines/Controller.cs | 5 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Engine.cs | 4 +- .../Items/Components/Machines/Fabricator.cs | 125 +++++-- .../Items/Components/Machines/Pump.cs | 4 +- .../Items/Components/Machines/Reactor.cs | 4 +- .../Items/Components/Machines/Steering.cs | 14 +- .../Items/Components/Power/PowerContainer.cs | 2 +- .../Items/Components/Power/PowerTransfer.cs | 3 +- .../Items/Components/Projectile.cs | 8 +- .../SharedSource/Items/Components/Quality.cs | 3 +- .../Items/Components/Repairable.cs | 6 +- .../SharedSource/Items/Components/Scanner.cs | 3 +- .../Items/Components/Signal/ButtonTerminal.cs | 3 +- .../Items/Components/Signal/CircuitBox.cs | 7 +- .../Components/Signal/ConnectionPanel.cs | 6 + .../Items/Components/Signal/LightComponent.cs | 36 +- .../Items/Components/TriggerComponent.cs | 12 +- .../SharedSource/Items/Components/Turret.cs | 136 +++++++- .../SharedSource/Items/Components/Wearable.cs | 4 +- .../SharedSource/Items/Inventory.cs | 11 +- .../SharedSource/Items/Item.cs | 89 ++++- .../SharedSource/Items/ItemEventData.cs | 3 +- .../SharedSource/Items/ItemPrefab.cs | 132 +++++-- .../SharedSource/Items/ItemStatManager.cs | 87 ++++- .../SharedSource/Items/RelatedItem.cs | 4 +- .../SharedSource/Map/DummyFireSource.cs | 3 +- .../SharedSource/Map/FireSource.cs | 17 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 5 + .../SharedSource/Map/IDamageable.cs | 2 +- .../Map/Levels/DestructibleLevelWall.cs | 2 +- .../SharedSource/Map/Levels/Level.cs | 126 +++++-- .../SharedSource/Map/Levels/LevelData.cs | 2 +- .../Map/Levels/LevelObjects/LevelObject.cs | 2 +- .../Levels/LevelObjects/LevelObjectManager.cs | 4 +- .../SharedSource/Map/Map/Location.cs | 189 +++++++--- .../SharedSource/Map/Map/LocationType.cs | 100 +++++- .../Map/Map/LocationTypeChange.cs | 14 +- .../SharedSource/Map/Map/Map.cs | 22 +- .../SharedSource/Map/MapEntity.cs | 10 +- ...onStationInfo.cs => ExtraSubmarineInfo.cs} | 64 +++- .../Map/Outposts/OutpostGenerationParams.cs | 2 +- .../SharedSource/Map/Structure.cs | 183 +++++++--- .../SharedSource/Map/StructurePrefab.cs | 9 +- .../SharedSource/Map/Submarine.cs | 165 +++++---- .../SharedSource/Map/SubmarineBody.cs | 58 +-- .../SharedSource/Map/SubmarineInfo.cs | 18 +- .../SharedSource/Networking/BanList.cs | 10 + .../Networking/ChildServerRelay.cs | 20 +- .../SharedSource/Networking/Client.cs | 6 +- .../SharedSource/Networking/EntitySpawner.cs | 6 +- .../Networking/OrderChatMessage.cs | 2 +- .../SharedSource/Networking/RespawnManager.cs | 7 + .../SharedSource/Networking/ServerSettings.cs | 14 + .../Editable/ConditionallyEditable.cs | 66 ++++ .../Serialization/Editable/Editable.cs | 55 +++ .../SerializableProperty.cs | 135 +------ .../Serialization/StructSerialization.cs | 2 +- .../SharedSource/Settings/GameSettings.cs | 29 +- .../SharedSource/Sprite/Sprite.cs | 3 +- .../StatusEffects/PropertyConditional.cs | 4 +- .../StatusEffects/StatusEffect.cs | 54 +-- .../SharedSource/SteamAchievementManager.cs | 1 + .../BarotraumaShared/SharedSource/Tags.cs | 14 +- .../Text/LocalizedString/TagLString.cs | 16 +- .../SharedSource/Text/RichString.cs | 13 +- .../SharedSource/Text/TextManager.cs | 63 +++- .../SharedSource/Text/TextPack.cs | 55 ++- .../SharedSource/Traitors/TraitorEvent.cs | 4 +- .../Traitors/TraitorEventPrefab.cs | 5 +- .../SharedSource/Upgrades/Upgrade.cs | 3 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 26 +- .../SharedSource/Utils/MathUtils.cs | 102 +++--- .../SharedSource/Utils/SaveUtil.cs | 9 +- .../SharedSource/Utils/Shapes/Quad2D.cs | 113 ++++++ .../SharedSource/Utils/Shapes/Triangle2D.cs | 21 ++ Barotrauma/BarotraumaShared/changelog.txt | 115 ++++++ .../FabricatorQualityRollTests.cs | 66 ++++ 375 files changed, 7771 insertions(+), 2874 deletions(-) rename Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/{TutorialHighlightAction.cs => HighlightAction.cs} (69%) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs rename Barotrauma/BarotraumaClient/ClientSource/Utils/{Quad.cs => GraphicsQuad.cs} (98%) create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemIsStatic.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs rename Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/{BeaconStationInfo.cs => ExtraSubmarineInfo.cs} (54%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs rename Barotrauma/BarotraumaShared/SharedSource/Serialization/{ => SerializableProperty}/SerializableProperty.cs (91%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Quad2D.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Triangle2D.cs create mode 100644 Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 272aace14..9ec4022c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -6,7 +6,7 @@ using System; namespace Barotrauma { - class Camera : IDisposable + class Camera { public static bool FollowSub = true; @@ -147,21 +147,10 @@ namespace Barotrauma position = Vector2.Zero; CreateMatrices(); - // TODO: this has the potential to cause a resource leak - // by sneakily creating a reference to cameras that we might - // fail to release. - GameMain.Instance.ResolutionChanged += CreateMatrices; UpdateTransform(false); } - private bool disposed = false; - public void Dispose() - { - if (!disposed) { GameMain.Instance.ResolutionChanged -= CreateMatrices; } - disposed = true; - } - public Vector2 TargetPos { get; set; } public Vector2 GetPosition() @@ -207,6 +196,12 @@ namespace Barotrauma public void UpdateTransform(bool interpolate = true, bool updateListener = true) { + if (GameMain.GraphicsWidth != Resolution.X || + GameMain.GraphicsHeight != Resolution.Y) + { + CreateMatrices(); + } + Vector2 interpolatedPosition = interpolate ? Timing.Interpolate(prevPosition, position) : position; float interpolatedZoom = interpolate ? Timing.Interpolate(prevZoom, zoom) : zoom; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index a223fe878..8df8deea0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -136,6 +136,7 @@ namespace Barotrauma set { if (!MathUtils.IsValid(value)) { return; } + if (this != Controlled) { return; } if (Screen.Selected?.Cam != null) { Screen.Selected.Cam.Shake = value; @@ -229,6 +230,8 @@ namespace Barotrauma } } + private float pressureEffectTimer; + private readonly List activeObjectiveEntities = new List(); public IEnumerable ActiveObjectiveEntities { @@ -333,18 +336,22 @@ namespace Barotrauma { if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) { - float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; - if (pressure > 0.0f) + //wait until the character has been in pressure for one second so the zoom doesn't + //"flicker" in and out if the pressure fluctuates around the minimum threshold + pressureEffectTimer += deltaTime; + if (pressureEffectTimer > 1.0f) { - //lerp in during the 1st second of the pressure timer so the zoom doesn't - //"flicker" in and out if the pressure fluctuates around the minimum threshold - float timerMultiplier = (PressureTimer / 100.0f); - float zoomInEffectStrength = MathHelper.Clamp(pressure / 100.0f * timerMultiplier, 0.0f, 1.0f); + float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; + float zoomInEffectStrength = MathHelper.Clamp(pressure / 100.0f, 0.0f, 1.0f); cam.Zoom = MathHelper.Lerp(cam.Zoom, cam.DefaultZoom + (Math.Max(pressure, 10) / 150.0f) * Rand.Range(0.9f, 1.1f), zoomInEffectStrength); } } + else + { + pressureEffectTimer = 0.0f; + } if (IsHumanoid) { @@ -521,22 +528,25 @@ namespace Barotrauma if (controlled == this) { controlled = null; - if (!(Screen.Selected?.Cam is null)) + if (Screen.Selected?.Cam is not null) { Screen.Selected.Cam.TargetPos = Vector2.Zero; Lights.LightManager.ViewTarget = null; } } + sounds.ForEach(s => s.Sound?.Dispose()); + sounds.Clear(); + if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.GetCharacters().Contains(this)) { GameMain.GameSession.CrewManager.RemoveCharacter(this); } - - if (GameMain.Client?.Character == this) GameMain.Client.Character = null; - if (Lights.LightManager.ViewTarget == this) Lights.LightManager.ViewTarget = null; + if (GameMain.Client?.Character == this) { GameMain.Client.Character = null; } + + if (Lights.LightManager.ViewTarget == this) { Lights.LightManager.ViewTarget = null; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 5f0c45241..348cdd874 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -894,7 +894,7 @@ namespace Barotrauma if (!orderIndicatorCount.ContainsKey(target)) { orderIndicatorCount.Add(target, 0); } - Vector2 drawPos = target is Entity ? (target as Entity).DrawPosition : + Vector2 drawPos = target is Entity entity ? entity.DrawPosition : target.Submarine == null ? target.Position : target.Position + target.Submarine.DrawPosition; drawPos += Vector2.UnitX * order.SymbolSprite.size.X * 1.5f * orderIndicatorCount[target]; GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, order.SymbolSprite, order.Color * iconAlpha, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index dce0ec9cd..e18d1e4e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -217,6 +217,7 @@ namespace Barotrauma if ((int)newLevel > (int)prevLevel) { + Character.Controlled?.SelectedItem?.OnPlayerSkillsChanged(); int increase = Math.Max((int)newLevel - (int)prevLevel, 1); Character?.AddMessage( @@ -518,7 +519,7 @@ namespace Barotrauma attachment.Sprite.Draw(spriteBatch, drawPos, color ?? Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); } - public static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc) + public static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc, bool requireJobPrefabFound = true) { ushort infoID = inc.ReadUInt16(); string newName = inc.ReadString(); @@ -554,14 +555,19 @@ namespace Barotrauma if (jobIdentifier > 0) { jobPrefab = JobPrefab.Prefabs.Find(jp => jp.UintIdentifier == jobIdentifier); - if (jobPrefab == null) + if (jobPrefab == null && requireJobPrefabFound) { 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)) + byte skillCount = inc.ReadByte(); + List jobSkills = jobPrefab?.Skills.OrderBy(s => s.Identifier).ToList(); + for (int i = 0; i < skillCount; i++) { float skillLevel = inc.ReadSingle(); - skillLevels.Add(skillPrefab.Identifier, skillLevel); + if (jobSkills != null && i < jobSkills.Count) + { + skillLevels.Add(jobSkills[i].Identifier, skillLevel); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 76919ce12..b0a721fc3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -2170,6 +2170,8 @@ namespace Barotrauma medUIExtra?.Remove(); medUIExtra = null; + Character.OnAttacked -= OnAttacked; + limbIndicatorOverlay?.Remove(); limbIndicatorOverlay = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index f6f7a7331..32ce3df20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -1216,7 +1216,7 @@ namespace Barotrauma pos: new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), srcRect: w.Sprite.SourceRect, color: Color.White, - rotation: rotation, + rotationRad: rotation, origin: origin, scale: new Vector2(scale, scale), effects: spriteEffect, diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index d355c7cb9..d08948b86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -34,7 +34,7 @@ namespace Barotrauma bool allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is { IsEditor: true }); if (!allowCheats && !CheatsEnabled && IsCheat) { - NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red); + NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + Names[0] + "\".", Color.Red); #if USE_STEAM NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red); #endif @@ -215,9 +215,9 @@ namespace Barotrauma SoundPlayer.PlayUISound(GUISoundType.Select); } - private static bool IsCommandPermitted(string command, GameClient client) + private static bool IsCommandPermitted(Identifier command, GameClient client) { - switch (command) + switch (command.Value.ToLowerInvariant()) { case "kick": return client.HasPermission(ClientPermissions.Kick); @@ -304,7 +304,7 @@ namespace Barotrauma } }; var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) }, - msg.Text, textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) + RichString.Rich(msg.Text), textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = msg.Color @@ -346,7 +346,7 @@ namespace Barotrauma CanBeFocused = false }; var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 170, 0), textContainer.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(20, 0) }, - command.help, textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) + command.Help, textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = Color.White @@ -354,7 +354,7 @@ namespace Barotrauma textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5); textBlock.SetTextPos(); new GUITextBlock(new RectTransform(new Point(150, textContainer.Rect.Height), textContainer.RectTransform), - command.names[0], textAlignment: Alignment.TopLeft); + command.Names[0].Value, textAlignment: Alignment.TopLeft); listBox.UpdateScrollBarSize(); listBox.BarScroll = 1.0f; @@ -364,7 +364,7 @@ namespace Barotrauma private static void AssignOnClientExecute(string names, Action onClientExecute) { - Command command = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0); + Command command = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any()); if (command == null) { throw new Exception("AssignOnClientExecute failed. Command matching the name(s) \"" + names + "\" not found."); @@ -378,7 +378,7 @@ namespace Barotrauma private static void AssignRelayToServer(string names, bool relay) { - Command command = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0); + Command command = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any()); if (command == null) { DebugConsole.Log("Could not assign to relay to server: " + names); @@ -681,6 +681,7 @@ namespace Barotrauma AssignRelayToServer("savebinds", false); AssignRelayToServer("spreadsheetexport", false); #if DEBUG + AssignRelayToServer("listspamfilters", false); AssignRelayToServer("crash", false); AssignRelayToServer("showballastflorasprite", false); AssignRelayToServer("simulatedlatency", false); @@ -706,6 +707,8 @@ namespace Barotrauma AssignRelayToServer("showmoney", true); AssignRelayToServer("setskill", true); AssignRelayToServer("readycheck", true); + commands.Add(new Command("debugjobassignment", "", (string[] args) => { })); + AssignRelayToServer("debugjobassignment", true); AssignRelayToServer("givetalent", true); AssignRelayToServer("unlocktalents", true); @@ -2232,6 +2235,30 @@ namespace Barotrauma })); #if DEBUG + commands.Add(new Command("listspamfilters", "Lists filters that are in the global spam filter.", (string[] args) => + { + if (!SpamServerFilters.GlobalSpamFilter.TryUnwrap(out var filter)) + { + ThrowError("Global spam list is not initialized."); + return; + } + + if (!filter.Filters.Any()) + { + NewMessage("Global spam list is empty.", GUIStyle.Green); + return; + } + + StringBuilder sb = new(); + + foreach (var f in filter.Filters) + { + sb.AppendLine(f.ToString()); + } + + NewMessage(sb.ToString(), GUIStyle.Green); + })); + commands.Add(new Command("setplanthealth", "setplanthealth [value]: Sets the health of the selected plant in sub editor.", (string[] args) => { if (1 > args.Length || Screen.Selected != GameMain.SubEditorScreen) { return; } @@ -3092,7 +3119,7 @@ namespace Barotrauma int i = 0; foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections) { - NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).Name, Color.White); + NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).DisplayName, Color.White); i++; } ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 45893dc8b..efbdb6712 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -374,13 +374,12 @@ namespace Barotrauma btn.RectTransform.MinSize = new Point(0, (int)(btn.TextBlock.Rect.Height * 1.2f)); } - textContent.RectTransform.MinSize = new Point(0, textContent.Children.Sum(c => c.Rect.Height) + GUI.IntScale(16)); + textContent.RectTransform.MinSize = new Point(0, textContent.Children.Sum(c => c.Rect.Height + textContent.AbsoluteSpacing) + GUI.IntScale(16)); content.RectTransform.MinSize = new Point(0, content.Children.Sum(c => c.Rect.Height)); // Recalculate the text size as it is scaled up and no longer matching the text height due to the textContent's minSize increasing textBlock.CalculateHeightFromText(); textBlock.TextAlignment = Alignment.TopLeft; - //content.RectTransform.MinSize = new Point(0, textContent.Rect.Height); return buttons; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs index 2686bd1af..3c81ad491 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/EventObjectiveAction.cs @@ -16,6 +16,11 @@ partial class EventObjectiveAction : EventAction int width = 450, int height = 80) { + if (Type == SegmentActionType.AddIfNotFound) + { + if (ObjectiveManager.IsSegmentActive(Identifier)) { return; } + } + ObjectiveManager.Segment? segment = null; // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) if (Type == SegmentActionType.Trigger) @@ -24,7 +29,8 @@ partial class EventObjectiveAction : EventAction new ObjectiveManager.Segment.Text(TextTag, width, height, Anchor.Center), new ObjectiveManager.Segment.Video(videoFile, TextTag, width, height)); } - else if (Type == SegmentActionType.Add) + else if (Type == SegmentActionType.Add || + Type == SegmentActionType.AddIfNotFound) { segment = ObjectiveManager.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); } @@ -33,10 +39,12 @@ partial class EventObjectiveAction : EventAction segment.CanBeCompleted = CanBeCompleted; segment.ParentId = ParentObjectiveId; } + switch (Type) { case SegmentActionType.Trigger: case SegmentActionType.Add: + case SegmentActionType.AddIfNotFound: ObjectiveManager.TriggerSegment(segment); break; case SegmentActionType.Complete: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/HighlightAction.cs similarity index 69% rename from Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs rename to Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/HighlightAction.cs index 732c1a480..cce9ce464 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialHighlightAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/HighlightAction.cs @@ -1,22 +1,19 @@ +#nullable enable using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma; -partial class TutorialHighlightAction : EventAction +partial class HighlightAction : EventAction { - private static readonly Color highlightColor = Color.Orange; - - partial void UpdateProjSpecific() + partial void SetHighlightProjSpecific(Entity entity, IEnumerable? targetCharacters) { - if (GameMain.GameSession?.GameMode is not TutorialMode) { return; } - foreach (var target in ParentEvent.GetTargets(TargetTag)) + if (targetCharacters != null && !targetCharacters.Contains(Character.Controlled)) { - SetHighlight(target); + return; } - } - private void SetHighlight(Entity entity) - { if (entity is Item i) { SetItemHighlight(i); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 27007040d..d5687b38a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -47,7 +47,7 @@ namespace Barotrauma } float theoreticalMaxMonsterStrength = 10000; - float relativeMaxMonsterStrength = theoreticalMaxMonsterStrength * (GameMain.GameSession?.LevelData?.Difficulty ?? 0f) / 100; + float relativeMaxMonsterStrength = theoreticalMaxMonsterStrength * (GameMain.GameSession?.Level?.Difficulty ?? 0f) / 100; float absoluteMonsterStrength = monsterStrength / theoreticalMaxMonsterStrength; float relativeMonsterStrength = monsterStrength / relativeMaxMonsterStrength; @@ -581,7 +581,14 @@ namespace Barotrauma StatusEffect effect = StatusEffect.Load(subElement, $"EventManager.ClientRead ({eventIdentifier})"); foreach (Entity target in targets) { - effect.Apply(effect.type, 1.0f, target, target as ISerializableEntity); + if (target is Item item) + { + effect.Apply(effect.type, 1.0f, item, item.AllPropertyObjects); + } + else + { + effect.Apply(effect.type, 1.0f, target, target as ISerializableEntity); + } } } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 1a9941f42..3e9fdd7cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -51,7 +51,8 @@ namespace Barotrauma if (requiredDeliveryAmount == 0) { requiredDeliveryAmount = items.Count; } if (requiredDeliveryAmount > items.Count) { - DebugConsole.AddWarning($"Error in mission \"{Prefab.Identifier}\". Required delivery amount is {requiredDeliveryAmount} but there's only {items.Count} items to deliver."); + DebugConsole.AddWarning($"Error in mission \"{Prefab.Identifier}\". Required delivery amount is {requiredDeliveryAmount} but there's only {items.Count} items to deliver.", + contentPackage: Prefab.ContentPackage); requiredDeliveryAmount = items.Count; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index fdf6ccfa8..c244b7a22 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -126,7 +126,13 @@ namespace Barotrauma void GiveMissionExperience(CharacterInfo info) { if (info == null) { return; } - var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f, info.Character); + //check if anyone else in the crew has talents that could give a bonus to this one + foreach (var c in crew) + { + if (c == info.Character) { continue; } + c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplierIndividual); + } info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); info.GiveExperience((int)(experienceGain * experienceGainMultiplierIndividual.Value)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index c04c21183..374e92d5e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -247,30 +247,33 @@ namespace Barotrauma UpdateCrew(); } + public void UpdateHireables() + { + UpdateHireables(campaign?.CurrentLocation); + } + private void UpdateHireables(Location location) { - if (hireableList != null) + if (hireableList == null) { return; } + hireableList.Content.Children.ToList().ForEach(c => hireableList.RemoveChild(c)); + var hireableCharacters = location.GetHireableCharacters(); + if (hireableCharacters.None()) { - hireableList.Content.Children.ToList().ForEach(c => hireableList.RemoveChild(c)); - var hireableCharacters = location.GetHireableCharacters(); - if (hireableCharacters.None()) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), hireableList.Content.RectTransform), TextManager.Get("HireUnavailable"), textAlignment: Alignment.Center) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), hireableList.Content.RectTransform), TextManager.Get("HireUnavailable"), textAlignment: Alignment.Center) - { - CanBeFocused = false - }; - } - else - { - foreach (CharacterInfo c in hireableCharacters) - { - if (c == null) { continue; } - CreateCharacterFrame(c, hireableList); - } - } - sortingDropDown.SelectItem(SortingMethod.JobAsc); - hireableList.UpdateScrollBarSize(); + CanBeFocused = false + }; } + else + { + foreach (CharacterInfo c in hireableCharacters) + { + if (c == null) { continue; } + CreateCharacterFrame(c, hireableList); + } + } + sortingDropDown.SelectItem(SortingMethod.JobAsc); + hireableList.UpdateScrollBarSize(); } public void SetHireables(Location location, List availableHires) @@ -434,7 +437,7 @@ namespace Barotrauma if (listBox != crewList) { new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), - TextManager.FormatCurrency(characterInfo.Salary), + TextManager.FormatCurrency(HireManager.GetSalaryFor(characterInfo)), textAlignment: Alignment.Center) { CanBeFocused = false @@ -692,11 +695,8 @@ namespace Barotrauma private void SetTotalHireCost() { if (pendingList == null || totalBlock == null || validateHiresButton == null) { return; } - int total = 0; - pendingList.Content.Children.ForEach(c => - { - total += ((InfoSkill)c.UserData).CharacterInfo.Salary; - }); + var infos = pendingList.Content.Children.Select(static c => ((InfoSkill)c.UserData).CharacterInfo).ToArray(); + int total = HireManager.GetSalaryFor(infos); totalBlock.Text = TextManager.FormatCurrency(total); bool enoughMoney = campaign == null || campaign.CanAfford(total); totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; @@ -718,14 +718,14 @@ namespace Barotrauma if (nonDuplicateHires.None()) { return false; } - int total = nonDuplicateHires.Aggregate(0, (total, info) => total + info.Salary); + int total = HireManager.GetSalaryFor(nonDuplicateHires); if (!campaign.CanAfford(total)) { return false; } bool atLeastOneHired = false; foreach (CharacterInfo ci in nonDuplicateHires) { - if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci)) + if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci, Character.Controlled)) { atLeastOneHired = true; } @@ -741,7 +741,7 @@ namespace Barotrauma SelectCharacter(null, null, null); var dialog = new GUIMessageBox( TextManager.Get("newcrewmembers"), - TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), + TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName), new LocalizedString[] { TextManager.Get("Ok") }); dialog.Buttons[0].OnClicked += dialog.Close; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 3a0bff6d9..4a1bee7c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -512,10 +512,18 @@ namespace Barotrauma soundStr += " (stopped)"; clr *= 0.5f; } - else if (playingSoundChannel.Muffled) + else { - soundStr += " (muffled)"; - clr = Color.Lerp(clr, Color.LightGray, 0.5f); + if (playingSoundChannel.Muffled) + { + soundStr += " (muffled)"; + clr = Color.Lerp(clr, Color.LightGray, 0.5f); + } + if (playingSoundChannel.FadingOutAndDisposing) + { + soundStr += ". Fading out..."; + clr = Color.Lerp(clr, Color.Black, 0.15f); + } } } @@ -2163,10 +2171,10 @@ namespace Barotrauma }; } - public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Action onConfirm, Action onDeny = null) + public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Action onConfirm, Action onDeny = null, Vector2? relativeSize = null, Point? minSize = null) { LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; - GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, relativeSize: relativeSize ?? new Vector2(0.2f, 0.175f), minSize: minSize ?? new Point(300, 175)); // Cancel button msgBox.Buttons[1].OnClicked = delegate diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index e3ea42eaa..762c4053c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -775,23 +775,30 @@ namespace Barotrauma toolTipBlock.UserData = toolTip; } - toolTipBlock.RectTransform.AbsoluteOffset = - RectTransform.CalculateAnchorPoint(anchor, targetElement) + - RectTransform.CalculatePivotOffset(pivot, toolTipBlock.RectTransform.NonScaledSize); + CalculateOffset(); if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10) { - toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width + targetElement.Width, 0); + anchor = RectTransform.MoveAnchorLeft(anchor); + pivot = (Pivot)RectTransform.MoveAnchorRight((Anchor)pivot); + CalculateOffset(); } if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10) { - toolTipBlock.RectTransform.AbsoluteOffset -= new Point( - 0, - toolTipBlock.Rect.Bottom - (GameMain.GraphicsHeight - 10)); + anchor = RectTransform.MoveAnchorTop(anchor); + pivot = (Pivot)RectTransform.MoveAnchorBottom((Anchor)pivot); + CalculateOffset(); } toolTipBlock.SetTextPos(); toolTipBlock.DrawManually(spriteBatch); + + void CalculateOffset() + { + toolTipBlock.RectTransform.AbsoluteOffset = + RectTransform.CalculateAnchorPoint(anchor, targetElement) + + RectTransform.CalculatePivotOffset(pivot, toolTipBlock.RectTransform.NonScaledSize); + } } #endregion diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index c5e6fda31..de557c274 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; +using Microsoft.Xna.Framework.Input; namespace Barotrauma { @@ -81,6 +82,8 @@ namespace Barotrauma public bool FlashOnAutoCloseCondition { get; set; } + public Action OnEnterPressed { get; set; } + public Type MessageBoxType => type; public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); @@ -89,6 +92,10 @@ namespace Barotrauma : this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize, type: type) { this.Buttons[0].OnClicked = Close; + OnEnterPressed = () => + { + Buttons[0].OnClicked(Buttons[0], Buttons[0].UserData); + }; } public GUIMessageBox(RichString headerText, RichString text, LocalizedString[] buttons, @@ -516,6 +523,11 @@ namespace Barotrauma protected override void Update(float deltaTime) { + if (PlayerInput.KeyHit(Keys.Enter)) + { + OnEnterPressed?.Invoke(); + } + if (Draggable) { GUIComponent parent = GUI.MouseOn?.Parent?.Parent; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 9edfae736..488f9a83d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -18,6 +18,20 @@ namespace Barotrauma public GUIButton PlusButton { get; private set; } public GUIButton MinusButton { get; private set; } + private void UpdatePlusMinusButtonVisibility() + { + if (ForceShowPlusMinusButtons + || inputType == NumberType.Int + || (inputType == NumberType.Float && MinValueFloat > float.MinValue && MaxValueFloat < float.MaxValue)) + { + ShowPlusMinusButtons(); + } + else + { + HidePlusMinusButtons(); + } + } + private NumberType inputType; public NumberType InputType { @@ -26,15 +40,7 @@ namespace Barotrauma { if (inputType == value) { return; } inputType = value; - if (inputType == NumberType.Int || - (inputType == NumberType.Float && MinValueFloat > float.MinValue && MaxValueFloat < float.MaxValue)) - { - ShowPlusMinusButtons(); - } - else - { - HidePlusMinusButtons(); - } + UpdatePlusMinusButtonVisibility(); } } @@ -46,15 +52,7 @@ namespace Barotrauma { minValueFloat = value; ClampFloatValue(); - if (inputType == NumberType.Int || - (inputType == NumberType.Float && MinValueFloat > float.MinValue && MaxValueFloat < float.MaxValue)) - { - ShowPlusMinusButtons(); - } - else - { - HidePlusMinusButtons(); - } + UpdatePlusMinusButtonVisibility(); } } public float? MaxValueFloat @@ -64,15 +62,7 @@ namespace Barotrauma { maxValueFloat = value; ClampFloatValue(); - if (inputType == NumberType.Int || - (inputType == NumberType.Float && MinValueFloat > float.MinValue && MaxValueFloat < float.MaxValue)) - { - ShowPlusMinusButtons(); - } - else - { - HidePlusMinusButtons(); - } + UpdatePlusMinusButtonVisibility(); } } @@ -96,6 +86,19 @@ namespace Barotrauma } } + private bool forceShowPlusMinusButtons; + + public bool ForceShowPlusMinusButtons + { + get { return forceShowPlusMinusButtons; } + set + { + if (forceShowPlusMinusButtons == value) { return; } + forceShowPlusMinusButtons = value; + UpdatePlusMinusButtonVisibility(); + } + } + private int decimalsToDisplay = 1; public int DecimalsToDisplay { @@ -184,7 +187,7 @@ namespace Barotrauma /// public bool WrapAround; - public float valueStep; + public float ValueStep; private float pressedTimer; private readonly float pressedDelay = 0.5f; @@ -339,12 +342,12 @@ namespace Barotrauma { if (inputType == NumberType.Int) { - IntValue -= valueStep > 0 ? (int)valueStep : 1; + IntValue -= ValueStep > 0 ? (int)ValueStep : 1; ClampIntValue(); } else if (maxValueFloat.HasValue && minValueFloat.HasValue) { - FloatValue -= valueStep > 0 ? valueStep : Round(); + FloatValue -= ValueStep > 0 ? ValueStep : Round(); ClampFloatValue(); } } @@ -353,12 +356,12 @@ namespace Barotrauma { if (inputType == NumberType.Int) { - IntValue += valueStep > 0 ? (int)valueStep : 1; + IntValue += ValueStep > 0 ? (int)ValueStep : 1; ClampIntValue(); } else if (inputType == NumberType.Float) { - FloatValue += valueStep > 0 ? valueStep : Round(); + FloatValue += ValueStep > 0 ? ValueStep : Round(); ClampFloatValue(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index 8d77932bc..d1d4a470f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -313,13 +313,18 @@ namespace Barotrauma public class GUIColor : GUISelector { - public GUIColor(string identifier) : base(identifier) { } + private readonly Color fallbackColor; + + public GUIColor(string identifier, Color fallbackColor) : base(identifier) + { + this.fallbackColor = fallbackColor; + } public Color Value { get { - return Prefabs.ActivePrefab.Color; + return Prefabs?.ActivePrefab?.Color ?? fallbackColor; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 05d7b129c..f6e5612d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -75,72 +75,72 @@ namespace Barotrauma /// /// General green color used for elements whose colors are set from code /// - public readonly static GUIColor Green = new GUIColor("Green"); + public readonly static GUIColor Green = new GUIColor("Green", new Color(154, 213, 163, 255)); /// /// General red color used for elements whose colors are set from code /// - public readonly static GUIColor Orange = new GUIColor("Orange"); + public readonly static GUIColor Orange = new GUIColor("Orange", new Color(243, 162, 50, 255)); /// /// General red color used for elements whose colors are set from code /// - public readonly static GUIColor Red = new GUIColor("Red"); + public readonly static GUIColor Red = new GUIColor("Red", new Color(245, 105, 105, 255)); /// /// General blue color used for elements whose colors are set from code /// - public readonly static GUIColor Blue = new GUIColor("Blue"); + public readonly static GUIColor Blue = new GUIColor("Blue", new Color(126, 211, 224, 255)); /// /// General yellow color used for elements whose colors are set from code /// - public readonly static GUIColor Yellow = new GUIColor("Yellow"); + public readonly static GUIColor Yellow = new GUIColor("Yellow", new Color(255, 255, 0, 255)); /// /// Color to display the name of modded servers in the server list. /// - public readonly static GUIColor ModdedServerColor = new GUIColor("ModdedServerColor"); + public readonly static GUIColor ModdedServerColor = new GUIColor("ModdedServerColor", new Color(154, 185, 160, 255)); - public readonly static GUIColor ColorInventoryEmpty = new GUIColor("ColorInventoryEmpty"); - public readonly static GUIColor ColorInventoryHalf = new GUIColor("ColorInventoryHalf"); - public readonly static GUIColor ColorInventoryFull = new GUIColor("ColorInventoryFull"); - public readonly static GUIColor ColorInventoryBackground = new GUIColor("ColorInventoryBackground"); - public readonly static GUIColor ColorInventoryEmptyOverlay = new GUIColor("ColorInventoryEmptyOverlay"); + public readonly static GUIColor ColorInventoryEmpty = new GUIColor("ColorInventoryEmpty", new Color(245, 105, 105, 255)); + public readonly static GUIColor ColorInventoryHalf = new GUIColor("ColorInventoryHalf", new Color(243, 162, 50, 255)); + public readonly static GUIColor ColorInventoryFull = new GUIColor("ColorInventoryFull", new Color(96, 222, 146, 255)); + public readonly static GUIColor ColorInventoryBackground = new GUIColor("ColorInventoryBackground", new Color(56, 56, 56, 255)); + public readonly static GUIColor ColorInventoryEmptyOverlay = new GUIColor("ColorInventoryEmptyOverlay", new Color(125, 125, 125, 255)); - public readonly static GUIColor TextColorNormal = new GUIColor("TextColorNormal"); - public readonly static GUIColor TextColorBright = new GUIColor("TextColorBright"); - public readonly static GUIColor TextColorDark = new GUIColor("TextColorDark"); - public readonly static GUIColor TextColorDim = new GUIColor("TextColorDim"); + public readonly static GUIColor TextColorNormal = new GUIColor("TextColorNormal", new Color(228, 217, 167, 255)); + public readonly static GUIColor TextColorBright = new GUIColor("TextColorBright", new Color(255, 255, 255, 255)); + public readonly static GUIColor TextColorDark = new GUIColor("TextColorDark", new Color(0, 0, 0, 230)); + public readonly static GUIColor TextColorDim = new GUIColor("TextColorDim", new Color(153, 153, 153, 153)); - public readonly static GUIColor ItemQualityColorPoor = new GUIColor("ItemQualityColorPoor"); - public readonly static GUIColor ItemQualityColorNormal = new GUIColor("ItemQualityColorNormal"); - public readonly static GUIColor ItemQualityColorGood = new GUIColor("ItemQualityColorGood"); - public readonly static GUIColor ItemQualityColorExcellent = new GUIColor("ItemQualityColorExcellent"); - public readonly static GUIColor ItemQualityColorMasterwork = new GUIColor("ItemQualityColorMasterwork"); + public readonly static GUIColor ItemQualityColorPoor = new GUIColor("ItemQualityColorPoor", new Color(128, 128, 128, 255)); + public readonly static GUIColor ItemQualityColorNormal = new GUIColor("ItemQualityColorNormal", new Color(255, 255, 255, 255)); + public readonly static GUIColor ItemQualityColorGood = new GUIColor("ItemQualityColorGood", new Color(144, 238, 144, 255)); + public readonly static GUIColor ItemQualityColorExcellent = new GUIColor("ItemQualityColorExcellent", new Color(173, 216, 230, 255)); + public readonly static GUIColor ItemQualityColorMasterwork = new GUIColor("ItemQualityColorMasterwork", new Color(147, 112, 219, 255)); - public readonly static GUIColor ColorReputationVeryLow = new GUIColor("ColorReputationVeryLow"); - public readonly static GUIColor ColorReputationLow = new GUIColor("ColorReputationLow"); - public readonly static GUIColor ColorReputationNeutral = new GUIColor("ColorReputationNeutral"); - public readonly static GUIColor ColorReputationHigh = new GUIColor("ColorReputationHigh"); - public readonly static GUIColor ColorReputationVeryHigh = new GUIColor("ColorReputationVeryHigh"); + public readonly static GUIColor ColorReputationVeryLow = new GUIColor("ColorReputationVeryLow", new Color(192, 60, 60, 255)); + public readonly static GUIColor ColorReputationLow = new GUIColor("ColorReputationLow", new Color(203, 145, 23, 255)); + public readonly static GUIColor ColorReputationNeutral = new GUIColor("ColorReputationNeutral", new Color(228, 217, 167, 255)); + public readonly static GUIColor ColorReputationHigh = new GUIColor("ColorReputationHigh", new Color(51, 152, 64, 255)); + public readonly static GUIColor ColorReputationVeryHigh = new GUIColor("ColorReputationVeryHigh", new Color(71, 160, 164, 255)); // Inventory - public readonly static GUIColor EquipmentSlotIconColor = new GUIColor("EquipmentSlotIconColor"); + public readonly static GUIColor EquipmentSlotIconColor = new GUIColor("EquipmentSlotIconColor", new Color(99, 70, 64, 255)); // Health HUD - public readonly static GUIColor BuffColorLow = new GUIColor("BuffColorLow"); - public readonly static GUIColor BuffColorMedium = new GUIColor("BuffColorMedium"); - public readonly static GUIColor BuffColorHigh = new GUIColor("BuffColorHigh"); + public readonly static GUIColor BuffColorLow = new GUIColor("BuffColorLow", new Color(66, 170, 73, 255)); + public readonly static GUIColor BuffColorMedium = new GUIColor("BuffColorMedium", new Color(110, 168, 118, 255)); + public readonly static GUIColor BuffColorHigh = new GUIColor("BuffColorHigh", new Color(154, 213, 163, 255)); - public readonly static GUIColor DebuffColorLow = new GUIColor("DebuffColorLow"); - public readonly static GUIColor DebuffColorMedium = new GUIColor("DebuffColorMedium"); - public readonly static GUIColor DebuffColorHigh = new GUIColor("DebuffColorHigh"); + public readonly static GUIColor DebuffColorLow = new GUIColor("DebuffColorLow", new Color(243, 162, 50, 255)); + public readonly static GUIColor DebuffColorMedium = new GUIColor("DebuffColorMedium", new Color(155, 55, 55, 255)); + public readonly static GUIColor DebuffColorHigh = new GUIColor("DebuffColorHigh", new Color(228, 27, 27, 255)); - public readonly static GUIColor HealthBarColorLow = new GUIColor("HealthBarColorLow"); - public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium"); - public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); - public readonly static GUIColor HealthBarColorPoisoned = new GUIColor("HealthBarColorPoisoned"); + public readonly static GUIColor HealthBarColorLow = new GUIColor("HealthBarColorLow", new Color(255, 0, 0, 255)); + public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium", new Color(255, 165, 0, 255)); + public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh", new Color(78, 114, 88)); + public readonly static GUIColor HealthBarColorPoisoned = new GUIColor("HealthBarColorPoisoned", new Color(100, 150, 0, 255)); private readonly static Point defaultItemFrameMargin = new Point(50, 56); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 168f53a7e..c9b9d0626 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -461,14 +461,16 @@ namespace Barotrauma } private ImmutableArray cachedCaretPositions = ImmutableArray.Empty; - + //which text were the cached caret positions calculated for? + private string cachedCaretPositionsText; public ImmutableArray GetAllCaretPositions() { - if (cachedCaretPositions.Any()) + string textDrawn = Censor ? CensoredText : Text.SanitizedValue; + if (cachedCaretPositions.Any() && + textDrawn == cachedCaretPositionsText) { return cachedCaretPositions; } - string textDrawn = Censor ? CensoredText : Text.SanitizedValue; float w = Wrap ? (Rect.Width - Padding.X - Padding.Z) / TextScale : float.PositiveInfinity; @@ -482,6 +484,7 @@ namespace Barotrauma .Select(p => p - new Vector2(alignmentXDiff, 0)) .Select(p => p * TextScale + TextPos - Origin * TextScale) .ToImmutableArray(); + cachedCaretPositionsText = textDrawn; return cachedCaretPositions; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 55fc849e9..600d92bb5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -353,6 +353,10 @@ namespace Barotrauma { CaretIndex = Math.Clamp(CaretIndex, 0, textBlock.Text.Length); var caretPositions = textBlock.GetAllCaretPositions(); + if (CaretIndex >= caretPositions.Length) + { + throw new Exception($"Caret index was outside the bounds of the calculated caret positions. Index: {CaretIndex}, caret positions: {caretPositions.Length}, text: {textBlock.Text}"); + } caretPos = caretPositions[CaretIndex]; caretPosDirty = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index e7902d5ca..06a9b05a4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -784,11 +784,95 @@ namespace Barotrauma #region Static methods public static Pivot MatchPivotToAnchor(Anchor anchor) { - if (!Enum.TryParse(anchor.ToString(), out Pivot pivot)) + return (Pivot)anchor; + } + public static Anchor MatchAnchorToPivot(Pivot pivot) + { + return (Anchor)pivot; + } + + /// + /// Moves the anchor to the left, keeping the vertical position unchanged (e.g. CenterRight -> CenterLeft) + /// + public static Anchor MoveAnchorLeft(Anchor anchor) + { + switch (anchor) { - throw new Exception($"[RectTransform] Cannot match pivot to anchor {anchor}"); + case Anchor.TopCenter: + case Anchor.TopRight: + return Anchor.TopLeft; + case Anchor.Center: + case Anchor.CenterRight: + return Anchor.CenterLeft; + case Anchor.BottomCenter: + case Anchor.BottomRight: + return Anchor.BottomLeft; + default: + return anchor; + } + } + + /// + /// Moves the anchor to the right, keeping the vertical position unchanged (e.g. CenterLeft -> CenterRight) + /// + public static Anchor MoveAnchorRight(Anchor anchor) + { + switch (anchor) + { + case Anchor.TopCenter: + case Anchor.TopLeft: + return Anchor.TopRight; + case Anchor.Center: + case Anchor.CenterLeft: + return Anchor.CenterRight; + case Anchor.BottomCenter: + case Anchor.BottomLeft: + return Anchor.BottomRight; + default: + return anchor; + } + } + + /// + /// Moves the anchor to the top, keeping the horizontal position unchanged (e.g. BottomCenter -> TopCenter) + /// + public static Anchor MoveAnchorTop(Anchor anchor) + { + switch (anchor) + { + case Anchor.CenterLeft: + case Anchor.BottomLeft: + return Anchor.TopLeft; + case Anchor.Center: + case Anchor.BottomCenter: + return Anchor.TopCenter; + case Anchor.CenterRight: + case Anchor.BottomRight: + return Anchor.TopRight; + default: + return anchor; + } + } + + /// + /// Moves the anchor to the bottom, keeping the horizontal position unchanged (e.g. TopCenter -> BottomCenter) + /// + public static Anchor MoveAnchorBottom(Anchor anchor) + { + switch (anchor) + { + case Anchor.CenterLeft: + case Anchor.TopLeft: + return Anchor.BottomLeft; + case Anchor.Center: + case Anchor.TopCenter: + return Anchor.BottomCenter; + case Anchor.CenterRight: + case Anchor.TopRight: + return Anchor.BottomRight; + default: + return anchor; } - return pivot; } /// @@ -811,11 +895,11 @@ namespace Barotrauma } } - public static Point CalculatePivotOffset(Pivot pivot, Point size) + public static Point CalculatePivotOffset(Pivot anchor, Point size) { int width = size.X; int height = size.Y; - switch (pivot) + switch (anchor) { case Pivot.TopLeft: return Point.Zero; @@ -836,7 +920,7 @@ namespace Barotrauma case Pivot.BottomRight: return new Point(-width, -height); default: - throw new NotImplementedException(pivot.ToString()); + throw new NotImplementedException(anchor.ToString()); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 34b935f84..5264b13b5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -40,6 +40,8 @@ namespace Barotrauma private readonly List itemsToSell = new List(); private readonly List itemsToSellFromSub = new List(); + private GUIMessageBox deliveryPrompt; + private StoreTab activeTab = StoreTab.Buy; private MapEntityCategory? selectedItemCategory; private bool suppressBuySell; @@ -341,9 +343,9 @@ namespace Barotrauma }; var panelMaxWidth = (int)(GUI.xScale * (GUI.HorizontalAspectRatio < 1.4f ? 650 : 560)); - var storeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform) + var storeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform, Anchor.BottomLeft) { - MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height) + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height - HUDLayoutSettings.ButtonAreaTop.Bottom) }) { Stretch = true, @@ -583,9 +585,9 @@ namespace Barotrauma // Shopping Crate ------------------------------------------------------------------------------------------------------------------------------------------ - var shoppingCrateContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform, anchor: Anchor.TopRight) + var shoppingCrateContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform, anchor: Anchor.BottomRight) { - MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height) + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height - HUDLayoutSettings.ButtonAreaTop.Bottom) }) { Stretch = true, @@ -922,15 +924,12 @@ 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) : storeBuyList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab); - if (CargoManager.GetPurchasedItem(ActiveStore, itemPrefab) is { } purchasedItem) - { - quantity = Math.Max(quantity - purchasedItem.Quantity, 0); - } + + quantity = Math.Max(quantity - CargoManager.GetPurchasedItemCount(ActiveStore, itemPrefab), 0); if (CargoManager.GetBuyCrateItem(ActiveStore, itemPrefab) is { } buyCrateItem) { quantity = Math.Max(quantity - buyCrateItem.Quantity, 0); @@ -1245,9 +1244,9 @@ namespace Barotrauma int totalPrice = 0; if (ActiveStore != null) { - foreach (PurchasedItem item in items) + foreach (PurchasedItem item in items.ToList()) { - if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; } + if (item.ItemPrefab.GetPriceInfo(ActiveStore) is not { } priceInfo) { continue; } GUINumberInput numInput = null; if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame)) { @@ -1749,7 +1748,7 @@ namespace Barotrauma } // Add items already purchased - CargoManager?.GetPurchasedItems(ActiveStore).ForEach(pi => AddNonEmptyOwnedItems(pi)); + CargoManager?.GetPurchasedItems(ActiveStore).Where(pi => !pi.DeliverImmediately).ForEach(pi => AddNonEmptyOwnedItems(pi)); ownedItemsUpdateTimer = 0.0f; @@ -1959,14 +1958,13 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.LogError($"Error getting item availability: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error getting item availability: Unknown store tab type. {e.StackTrace.CleanupStackTrace()}"); } if (list != null && list.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem item) { if (mode == StoreTab.Buy) { - var purchasedItem = CargoManager.GetPurchasedItem(ActiveStore, item.ItemPrefab); - if (purchasedItem != null) { return Math.Max(item.Quantity - purchasedItem.Quantity, 0); } + return Math.Max(item.Quantity - CargoManager.GetPurchasedItemCount(ActiveStore, item.ItemPrefab), 0); } return item.Quantity; } @@ -2093,16 +2091,57 @@ namespace Barotrauma } itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); if (itemsToPurchase.None() || Balance < totalPrice) { return false; } - CargoManager.PurchaseItems(ActiveStore.Identifier, itemsToPurchase, true); - GameMain.Client?.SendCampaignState(); - var dialog = new GUIMessageBox( - TextManager.Get("newsupplies"), - TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), - new LocalizedString[] { TextManager.Get("Ok") }); - dialog.Buttons[0].OnClicked += dialog.Close; + + if (CampaignMode.AllowImmediateItemDelivery()) + { + deliveryPrompt = new GUIMessageBox( + TextManager.Get("newsupplies"), + TextManager.Get("suppliespurchased.deliverymethod"), + new LocalizedString[] + { + TextManager.Get("suppliespurchased.deliverymethod.deliverimmediately"), + TextManager.Get("suppliespurchased.deliverymethod.delivertosub") + }); + deliveryPrompt.Buttons[0].OnClicked = (btn, userdata) => + { + ConfirmPurchase(deliverImmediately: true); + deliveryPrompt.Close(); + return true; + }; + deliveryPrompt.Buttons[1].OnClicked = (btn, userdata) => + { + ConfirmPurchase(deliverImmediately: false); + deliveryPrompt.Close(); + return true; + }; + } + else + { + ConfirmPurchase(deliverImmediately: false); + } + + void ConfirmPurchase(bool deliverImmediately) + { + itemsToPurchase.ForEach(it => it.DeliverImmediately = deliverImmediately); + CargoManager.PurchaseItems(ActiveStore.Identifier, itemsToPurchase, removeFromCrate: true); + GameMain.Client?.SendCampaignState(); + if (!deliverImmediately) + { + var dialog = new GUIMessageBox( + TextManager.Get("newsupplies"), + TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName)); + dialog.Buttons[0].OnClicked += dialog.Close; + } + } return false; } + public void OnDeselected() + { + deliveryPrompt?.Close(); + deliveryPrompt = null; + } + private bool SellItems() { if (!HasActiveTabPermissions()) { return false; } @@ -2118,7 +2157,7 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.LogError($"Error confirming the store transaction: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error confirming the store transaction: Unknown store tab type. {e.StackTrace.CleanupStackTrace()}"); return false; } var itemsToRemove = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 64f9c9ac9..325b3d3f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -130,7 +130,7 @@ namespace Barotrauma }; content = new GUILayoutGroup(new RectTransform(new Point(background.Rect.Width - HUDLayoutSettings.Padding * 4, background.Rect.Height - HUDLayoutSettings.Padding * 4), background.RectTransform, Anchor.Center)) { AbsoluteSpacing = (int)(HUDLayoutSettings.Padding * 1.5f) }; - GUITextBlock header = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), content.RectTransform), transferService ? TextManager.Get("switchsubmarineheader") : TextManager.GetWithVariable("outpostshipyard", "[location]", GameMain.GameSession.Map.CurrentLocation.Name), font: GUIStyle.LargeFont); + GUITextBlock header = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), content.RectTransform), transferService ? TextManager.Get("switchsubmarineheader") : TextManager.GetWithVariable("outpostshipyard", "[location]", GameMain.GameSession.Map.CurrentLocation.DisplayName), font: GUIStyle.LargeFont); header.CalculateHeightFromText(0, true); playerBalanceElement = CampaignUI.AddBalanceElement(header, new Vector2(1.0f, 1.5f)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 24362e63c..4e78f979b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -165,6 +165,11 @@ namespace Barotrauma public TabMenu() { if (!initialized) { Initialize(); } + if (Level.Loaded == null) + { + //make sure we're not trying to view e.g. mission or reputation info if the tab menu is opened in the test mode + SelectedTab = InfoFrameTab.Crew; + } CreateInfoFrame(SelectedTab); SelectInfoFrameTab(SelectedTab); } @@ -303,7 +308,7 @@ namespace Barotrauma { var missionBtn = createTabButton(InfoFrameTab.Mission, "mission"); eventLogNotification = GameSession.CreateNotificationIcon(missionBtn); - eventLogNotification.Visible = GameMain.GameSession.EventManager?.EventLog?.UnreadEntries ?? false; + eventLogNotification.Visible = GameMain.GameSession?.EventManager?.EventLog?.UnreadEntries ?? false; if (eventLogNotification.Visible) { eventLogNotification.Pulsate(Vector2.One, Vector2.One * 2, 1.0f); @@ -1508,7 +1513,7 @@ 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.DisplayName, 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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs index 90c860332..1e8b12e76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs @@ -163,7 +163,7 @@ namespace Barotrauma else if (Tile) { Vector2 startPos = new Vector2(rect.X, rect.Y); - Sprite.DrawTiled(spriteBatch, startPos, new Vector2(rect.Width, rect.Height), color, startOffset: uvOffset); + Sprite.DrawTiled(spriteBatch, startPos, new Vector2(rect.Width, rect.Height), color: color, startOffset: uvOffset); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index cff4b0a2f..3a8e2cb68 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -1110,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, characterList); + int price = prefab.Price.GetBuyPrice(prefab, 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)); } @@ -1267,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, characterList).ToString())); + ("[amount]", prefab.Price.GetBuyPrice(prefab, Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation, characterList).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -1682,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, characterList); + int price = prefab.Price.GetBuyPrice(prefab, campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList); if (!WaitForServerUpdate) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 4f4e5a284..50dbc4178 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -123,6 +123,10 @@ namespace Barotrauma private Viewport defaultViewport; + /// + /// NOTE: Use very carefully. You need to ensure that you ALWAYS unsubscribe from this when you no longer need the subscriber! + /// If you're subscribing to this from something else than a singleton or something that there's only ever one instance of, you're probably in dangerous territory. + /// public event Action ResolutionChanged; private bool exiting; @@ -404,7 +408,7 @@ namespace Barotrauma //do this here because we need it for the loading screen WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice); - Quad.Init(GraphicsDevice); + GraphicsQuad.Init(GraphicsDevice); loadingScreenOpen = true; TitleScreen = new LoadingScreen(GraphicsDevice) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 4655e13d5..0d1df98e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -147,7 +147,7 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.LogError($"Error selling items: uknown store tab type \"{sellingMode}\".\n{e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error selling items: unknown store tab type \"{sellingMode}\".\n{e.StackTrace.CleanupStackTrace()}"); return; } bool canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 28556fd9d..feb6eb7d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -72,6 +72,9 @@ namespace Barotrauma case InteractionType.MedicalClinic: CampaignUI.MedicalClinic?.OnDeselected(); break; + case InteractionType.Store: + CampaignUI.Store?.OnDeselected(); + break; } } @@ -121,6 +124,16 @@ namespace Barotrauma { return AllowedToManageCampaign(ClientPermissions.ManageMoney); } + + public static bool AllowImmediateItemDelivery() + { + if (GameMain.Client == null) { return true; } + return + GameMain.Client.ServerSettings.AllowImmediateItemDelivery || + GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || + GameMain.Client.IsServerOwner; + } + protected GUIButton CreateEndRoundButton() { int buttonWidth = (int)(450 * GUI.xScale * (GUI.IsUltrawide ? 3.0f : 1.0f)); @@ -182,12 +195,12 @@ namespace Barotrauma if (Level.Loaded.EndOutpost == null || !Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) { string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation"; - buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); + buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.DisplayName ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; } break; case TransitionType.LeaveLocation: - buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); + buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.DisplayName ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; break; case TransitionType.ReturnToPreviousLocation: @@ -195,7 +208,7 @@ namespace Barotrauma if (Level.Loaded.StartOutpost == null || !Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub)) { string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation"; - buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); + buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.DisplayName ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; } break; @@ -211,7 +224,7 @@ namespace Barotrauma endRoundButton.Color = GUIStyle.Red * 0.7f; endRoundButton.HoverColor = GUIStyle.Red; } - buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); + buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.DisplayName ?? "[ERROR]"); allowEndingRound = !ForceMapUI && !ShowCampaignUI; } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index a3be9801c..ce6972067 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -535,7 +535,7 @@ namespace Barotrauma bool refreshCampaignUI = false; - if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaignID != campaign.CampaignID) + if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaignID != campaign.CampaignID) { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer); @@ -614,7 +614,7 @@ namespace Barotrauma { if (availableMission.ConnectionIndex < 0 || availableMission.ConnectionIndex >= campaign.Map.CurrentLocation.Connections.Count) { - DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.Identifier}\" out of range (index: {availableMission.ConnectionIndex}, current location: {campaign.Map.CurrentLocation.Name}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); + DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.Identifier}\" out of range (index: {availableMission.ConnectionIndex}, current location: {campaign.Map.CurrentLocation.DisplayName}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); continue; } LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.ConnectionIndex]; @@ -647,7 +647,15 @@ namespace Barotrauma { if (ownedSubIndex >= GameMain.Client.ServerSubmarines.Count) { - string errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds. Index: {ownedSubIndex}, submarines: {string.Join(", ", GameMain.Client.ServerSubmarines.Select(s => s.Name))}"; + string errorMsg; + if (GameMain.Client.ServerSubmarines.None()) + { + errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds (list of server submarines is empty)."; + } + else + { + errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds. Index: {ownedSubIndex}, submarines: {string.Join(", ", GameMain.Client.ServerSubmarines.Select(s => s.Name))}"; + } DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "MultiPlayerCampaign.ClientRead.OwnerSubIndexOutOfBounds" + ownedSubIndex, @@ -822,11 +830,12 @@ namespace Barotrauma UInt16 id = msg.ReadUInt16(); bool hasCharacterData = msg.ReadBoolean(); CharacterInfo myCharacterInfo = null; + bool waitForModsDownloaded = Screen.Selected is ModDownloadScreen; if (hasCharacterData) { - myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); + myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg, requireJobPrefabFound: !waitForModsDownloaded); } - if (ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true)) + if (!waitForModsDownloaded && ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true)) { if (myCharacterInfo != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index fa274ad8f..cc4695375 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -548,9 +548,12 @@ namespace Barotrauma } 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 && !Level.Loaded.StartOutpost.ExitPoints.Any()) + //force the map to open if the sub is somehow not at the start of the outpost level + //UNLESS the level has specific exit points, in that case the sub needs to get to those + if (!Submarine.MainSub.AtStartExit && + /*there should normally always be a start outpost in outpost levels, + * but that might not always be the case e.g. mods or outdated saves (see #13042)*/ + Level.Loaded.StartOutpost is not { ExitPoints.Count: > 0 }) { ForceMapUI = true; CampaignUI.SelectTab(InteractionType.Map); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 9c42d6beb..7ce88ca26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -48,6 +48,8 @@ namespace Barotrauma private GUIImage eventLogNotification; + private Point prevTopLeftButtonsResolution; + private void CreateTopLeftButtons() { if (topLeftButtonGroup != null) @@ -61,10 +63,6 @@ namespace Barotrauma AbsoluteSpacing = HUDLayoutSettings.Padding, CanBeFocused = false }; - topLeftButtonGroup.RectTransform.ParentChanged += (_) => - { - GameMain.Instance.ResolutionChanged -= CreateTopLeftButtons; - }; int buttonHeight = GUI.IntScale(40); Vector2 buttonSpriteSize = GUIStyle.GetComponentStyle("CrewListToggleButton").GetDefaultSprite().size; int buttonWidth = (int)((buttonHeight / buttonSpriteSize.Y) * buttonSpriteSize.X); @@ -98,8 +96,6 @@ namespace Barotrauma talentPointNotification = CreateNotificationIcon(tabMenuButton); eventLogNotification = CreateNotificationIcon(tabMenuButton); - GameMain.Instance.ResolutionChanged += CreateTopLeftButtons; - respawnInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), parent: topLeftButtonGroup.RectTransform) { MaxSize = new Point(HUDLayoutSettings.ButtonAreaTop.Width / 3, int.MaxValue) }, style: null) { @@ -121,6 +117,7 @@ namespace Barotrauma return true; } }; + prevTopLeftButtonsResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } public void AddToGUIUpdateList() @@ -133,7 +130,8 @@ namespace Barotrauma if ((GameMode is not CampaignMode campaign || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) && !CoroutineManager.IsCoroutineRunning("LevelTransition") && !CoroutineManager.IsCoroutineRunning("SubmarineTransition")) { - if (topLeftButtonGroup == null) + if (topLeftButtonGroup == null || + prevTopLeftButtonsResolution.X != GameMain.GraphicsWidth || prevTopLeftButtonsResolution.Y != GameMain.GraphicsHeight) { CreateTopLeftButtons(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs index 0a70bc1d9..33fc06086 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs @@ -139,6 +139,11 @@ static class ObjectiveManager VideoPlayer.AddToGUIUpdateList(order: 100); } + public static bool IsSegmentActive(Identifier segmentId) + { + return activeObjectives.Any(o => o.Id == segmentId); + } + public static void TriggerSegment(Segment segment, bool connectObjective = false) { if (segment.SegmentType != SegmentType.InfoBox) @@ -361,9 +366,18 @@ static class ObjectiveManager activeObjectives.IndexOf(parentSegment) + activeObjectives.Count(s => s.ParentId == segment.ParentId); if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex) { - frameRt.RepositionChildInHierarchy(childIndex); - activeObjectives.Remove(segment); - activeObjectives.Insert(childIndex, segment); + if (childIndex < 0 || childIndex >= frameRt.Parent.CountChildren) + { + DebugConsole.ThrowError( + $"Error in {nameof(ObjectiveManager.AddToObjectiveList)}. " + + $"Failed to reposition an objective in the list. Text \"{segment.ObjectiveText}\", parentId: {segment.ParentId}, childIndex: {childIndex}"); + } + else + { + frameRt.RepositionChildInHierarchy(childIndex); + activeObjectives.Remove(segment); + activeObjectives.Insert(childIndex, segment); + } } } frameRt.AbsoluteOffset = GetObjectiveHiddenPosition(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 33f02d0fe..068477a41 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -564,7 +564,7 @@ namespace Barotrauma private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) { - string locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.Name : startLocation?.Name; + LocalizedString locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.DisplayName : startLocation?.DisplayName; string textTag; if (gameOver) @@ -576,23 +576,23 @@ namespace Barotrauma switch (transitionType) { case CampaignMode.TransitionType.LeaveLocation: - locationName = startLocation?.Name; + locationName = startLocation?.DisplayName; textTag = "RoundSummaryLeaving"; break; case CampaignMode.TransitionType.ProgressToNextLocation: - locationName = endLocation?.Name; + locationName = endLocation?.DisplayName; textTag = "RoundSummaryProgress"; break; case CampaignMode.TransitionType.ProgressToNextEmptyLocation: - locationName = endLocation?.Name; + locationName = endLocation?.DisplayName; textTag = "RoundSummaryProgressToEmptyLocation"; break; case CampaignMode.TransitionType.ReturnToPreviousLocation: - locationName = startLocation?.Name; + locationName = startLocation?.DisplayName; textTag = "RoundSummaryReturn"; break; case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: - locationName = startLocation?.Name; + locationName = startLocation?.DisplayName; textTag = "RoundSummaryReturnToEmptyLocation"; break; default: @@ -603,14 +603,14 @@ namespace Barotrauma if (startLocation?.Biome != null && startLocation.Biome.IsEndBiome) { - locationName ??= startLocation.Name; + locationName ??= startLocation.DisplayName; } if (textTag == null) { return ""; } if (locationName == null) { - DebugConsole.ThrowError($"Error while creating round summary: could not determine destination location. Start location: {startLocation?.Name ?? "null"}, end location: {endLocation?.Name ?? "null"}"); + DebugConsole.ThrowError($"Error while creating round summary: could not determine destination location. Start location: {startLocation?.DisplayName ?? "null"}, end location: {endLocation?.DisplayName ?? "null"}"); locationName = "[UNKNOWN]"; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 54b27c3e0..705b59f8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -568,7 +568,7 @@ namespace Barotrauma itemContainer.KeepOpenWhenEquippedBy(character) && !DraggingItems.Contains(item) && character.CanAccessInventory(itemContainer.Inventory) && - !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory)) + !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory && s.SlotIndex == i)) { ShowSubInventory(new SlotReference(this, visualSlots[i], i, false, itemContainer.Inventory), deltaTime, cam, hideSubInventories, true); } @@ -709,11 +709,11 @@ namespace Barotrauma private void ShowSubInventory(SlotReference slotRef, float deltaTime, Camera cam, List hideSubInventories, bool isEquippedSubInventory) { Rectangle hoverArea = GetSubInventoryHoverArea(slotRef); - if (isEquippedSubInventory) + if (isEquippedSubInventory && slotRef.Inventory is not ItemInventory { Container.MovableFrame: true, Container.KeepOpenWhenEquipped: true }) { foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots) { - if (highlightedSubInventorySlot == slotRef) continue; + if (highlightedSubInventorySlot == slotRef) { continue; } if (hoverArea.Intersects(GetSubInventoryHoverArea(highlightedSubInventorySlot))) { return; // If an equipped one intersects with a currently active hover one, do not open @@ -818,7 +818,8 @@ namespace Barotrauma if (selectedContainer != null && selectedContainer.Inventory != null && - !selectedContainer.Inventory.Locked && + !selectedContainer.Inventory.Locked && + selectedContainer.DrawInventory && allowInventorySwap) { //player has selected the inventory of another item -> attempt to move the item there @@ -841,6 +842,7 @@ namespace Barotrauma } else if (character.HeldItems.FirstOrDefault(i => i.OwnInventory != null && + i.OwnInventory.Container.DrawInventory && (i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))) is { } equippedContainer) { if (allowEquip) @@ -1027,7 +1029,7 @@ namespace Barotrauma //order by the condition of the contained item to prefer putting into the item with the emptiest ammo/battery/tank foreach (Item heldItem in character.HeldItems.OrderByDescending(heldItem => GetContainPriority(item, heldItem))) { - if (heldItem.OwnInventory == null) { continue; } + if (heldItem.OwnInventory == null || !heldItem.OwnInventory.Container.DrawInventory) { continue; } //don't allow swapping if we're moving items into an item with 1 slot holding a stack of items //(in that case, the quick action should just fill up the stack) bool disallowSwapping = diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs index 590e908f9..7d5981608 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs @@ -54,7 +54,9 @@ namespace Barotrauma.Items.Components { if (deconstructor.InputContainer.Inventory.AllItems.Count() == 2) { - if (!deconstructor.InputContainer.Inventory.AllItems.All(it => it.Prefab == item.Prefab)) + var otherGeneticMaterial = + deconstructor.InputContainer.Inventory.AllItems.FirstOrDefault(it => it != item && it.Prefab == item.Prefab)?.GetComponent(); + if (otherGeneticMaterial == null) { buttonText = TextManager.Get("researchstation.combine"); infoText = TextManager.Get("researchstation.combine.infotext"); @@ -62,7 +64,7 @@ namespace Barotrauma.Items.Components else { buttonText = TextManager.Get("researchstation.refine"); - int taintedProbability = (int)(GetTaintedProbabilityOnRefine(Character.Controlled) * 100); + int taintedProbability = (int)(GetTaintedProbabilityOnRefine(otherGeneticMaterial, Character.Controlled) * 100); infoText = TextManager.GetWithVariable("researchstation.refine.infotext", "[taintedprobability]", taintedProbability.ToString()); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 32dca442a..e9f8847e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -260,6 +260,12 @@ namespace Barotrauma.Items.Components if (!hasSoundsOfType[(int)type]) { return; } if (GameMain.Client?.MidRoundSyncing ?? false) { return; } + //above the top boundary of the level (in an inactive respawn shuttle?) + if (item.Submarine != null && Level.Loaded != null && item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) + { + return; + } + if (loopingSound != null) { if (Vector3.DistanceSquared(GameMain.SoundManager.ListenerPosition, new Vector3(item.WorldPosition, 0.0f)) > loopingSound.Range * loopingSound.Range || @@ -388,12 +394,9 @@ namespace Barotrauma.Items.Components } } - public void StopSounds(ActionType type) + public void StopLoopingSound() { if (loopingSound == null) { return; } - - if (loopingSound.Type != type) { return; } - if (loopingSoundChannel != null) { loopingSoundChannel.FadeOutAndDispose(); @@ -402,6 +405,12 @@ namespace Barotrauma.Items.Components } } + public void StopSounds(ActionType type) + { + if (loopingSound == null || loopingSound.Type != type) { return; } + StopLoopingSound(); + } + private float GetSoundVolume(ItemSound sound) { if (sound == null) { return 0.0f; } @@ -497,7 +506,8 @@ namespace Barotrauma.Items.Components case "guiframe": if (subElement.GetAttribute("rect") != null) { - DebugConsole.ThrowError($"Error in item config \"{item.ConfigFilePath}\" - GUIFrame defined as rect, use RectTransform instead."); + DebugConsole.ThrowError($"Error in item config \"{item.ConfigFilePath}\" - GUIFrame defined as rect, use RectTransform instead.", + contentPackage: subElement.ContentPackage); break; } GuiFrameSource = subElement; @@ -516,7 +526,8 @@ namespace Barotrauma.Items.Components if (filePath.IsNullOrEmpty()) { DebugConsole.ThrowError( - $"Error when instantiating item \"{item.Name}\" - sound with no file path set"); + $"Error when instantiating item \"{item.Name}\" - sound with no file path set", + contentPackage: subElement.ContentPackage); break; } @@ -528,7 +539,8 @@ namespace Barotrauma.Items.Components } catch (Exception e) { - DebugConsole.ThrowError($"Invalid sound type \"{typeStr}\" in item \"{item.Prefab.Identifier}\"!", e); + DebugConsole.ThrowError($"Invalid sound type \"{typeStr}\" in item \"{item.Prefab.Identifier}\"!", e, + contentPackage: subElement.ContentPackage); break; } @@ -758,6 +770,8 @@ namespace Barotrauma.Items.Components } } + public virtual void OnPlayerSkillsChanged() { } + public virtual void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 830703d01..21b3c9c76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -158,7 +158,8 @@ namespace Barotrauma.Items.Components IndicatorStyle = GUIStyle.GetComponentStyle("ContainedStateIndicator." + ContainedStateIndicatorStyle); if (ContainedStateIndicator != null || ContainedStateIndicatorEmpty != null) { - DebugConsole.AddWarning($"Item \"{item.Name}\" defines both a contained state indicator style and a custom indicator sprite. Will use the custom sprite..."); + DebugConsole.AddWarning($"Item \"{item.Name}\" defines both a contained state indicator style and a custom indicator sprite. Will use the custom sprite...", + contentPackage: item.Prefab.ContentPackage); } } if (GuiFrame == null) @@ -345,9 +346,9 @@ namespace Barotrauma.Items.Components public bool KeepOpenWhenEquippedBy(Character character) { - if (!character.CanAccessInventory(Inventory) || - !KeepOpenWhenEquipped || - !character.HasEquippedItem(Item)) + if (!KeepOpenWhenEquipped || + !character.HasEquippedItem(Item) || + !character.CanAccessInventory(Inventory)) { return false; } @@ -570,11 +571,13 @@ namespace Barotrauma.Items.Components { spriteRotation = contained.Rotation; } - if ((item.body != null && item.body.Dir == -1) || item.FlippedX) + bool flipX = (item.body != null && item.body.Dir == -1) || item.FlippedX; + if (flipX) { spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally; } - if (item.FlippedY) + bool flipY = item.FlippedY; + if (flipY) { spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; } @@ -588,6 +591,7 @@ namespace Barotrauma.Items.Components contained.Item.Scale, spriteEffects, depth: containedSpriteDepth); + contained.Item.DrawDecorativeSprites(spriteBatch, itemPos, flipX,flipY, (contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), containedSpriteDepth); foreach (ItemContainer ic in contained.Item.GetComponents()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index a6ff486bb..7f81d3a3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -227,7 +227,7 @@ namespace Barotrauma.Items.Components switch (text) { case "[CurrentLocationName]": - SetDisplayText(Level.Loaded?.StartLocation?.Name ?? string.Empty); + SetDisplayText(Level.Loaded?.StartLocation?.DisplayName.Value ?? string.Empty); break; case "[CurrentBiomeName]": SetDisplayText(Level.Loaded?.LevelData?.Biome?.DisplayName.Value ?? string.Empty); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 540bc1759..9e4c1b164 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -67,7 +67,7 @@ namespace Barotrauma.Items.Components Light.Position = item.Position; } PhysicsBody body = Light.ParentBody; - if (body != null) + if (body != null && body.Enabled) { Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi; Light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 8c3d920c6..24fbcfb36 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -66,7 +66,11 @@ namespace Barotrauma.Items.Components new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), sliderArea.RectTransform, Anchor.TopCenter), "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center) { AutoScaleHorizontal = true, - TextGetter = () => { return TextManager.AddPunctuation(':', powerLabel, (int)(targetForce) + " %"); } + TextGetter = () => + { + return TextManager.AddPunctuation(':', powerLabel, + TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(targetForce)).ToString())); + } }; forceSlider = new GUIScrollBar(new RectTransform(new Vector2(0.95f, 0.45f), sliderArea.RectTransform, Anchor.Center), barSize: 0.1f, style: "DeviceSlider") { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index c86d674e5..96f708e8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Barotrauma.Items.Components @@ -393,6 +394,8 @@ namespace Barotrauma.Items.Components partial void SelectProjSpecific(Character character) { + if (character != Character.Controlled) { return; } + var nonItems = itemList.Content.Children.Where(c => c.UserData is not FabricationRecipe).ToList(); nonItems.ForEach(i => itemList.Content.RemoveChild(i)); @@ -784,6 +787,7 @@ namespace Barotrauma.Items.Components private void HideEmptyItemListCategories() { + bool visibleElementsChanged = false; //go through the elements backwards, and disable the labels ("insufficient skills to fabricate", "recipe required...") if there's no items below them bool recipeVisible = false; foreach (GUIComponent child in itemList.Content.Children.Reverse()) @@ -792,7 +796,11 @@ namespace Barotrauma.Items.Components { if (child.Enabled) { - child.Visible = recipeVisible; + if (child.Visible != recipeVisible) + { + child.Visible = recipeVisible; + visibleElementsChanged = true; + } } recipeVisible = false; } @@ -802,8 +810,11 @@ namespace Barotrauma.Items.Components } } - itemList.UpdateScrollBarSize(); - itemList.BarScroll = 0.0f; + if (visibleElementsChanged) + { + itemList.UpdateScrollBarSize(); + itemList.BarScroll = 0.0f; + } } public bool ClearFilter() @@ -815,11 +826,22 @@ namespace Barotrauma.Items.Components return true; } + private readonly record struct SelectedRecipe(Character User, FabricationRecipe SelectedItem, Option OverrideRequiredTime); + private Option LastSelectedRecipe = Option.None; + private bool SelectItem(Character user, FabricationRecipe selectedItem, float? overrideRequiredTime = null) { this.selectedItem = selectedItem; displayingForCharacter = user; + var selectedRecipe = new SelectedRecipe(user, selectedItem, overrideRequiredTime is null ? Option.None : Option.Some(overrideRequiredTime.Value)); + LastSelectedRecipe = Option.Some(selectedRecipe); + CreateSelectedItemUI(selectedRecipe); + return true; + } + private void CreateSelectedItemUI(SelectedRecipe recipe) + { + var (user, selectedItem, overrideRequiredTime) = recipe; int max = Math.Max(selectedItem.TargetItem.GetMaxStackSize(outputContainer.Inventory) / selectedItem.Amount, 1); if (amountInput != null) @@ -843,8 +865,10 @@ namespace Barotrauma.Items.Components LocalizedString itemName = GetRecipeNameAndAmount(selectedItem); LocalizedString name = itemName; - float quality = selectedItem.Quality ?? GetFabricatedItemQuality(selectedItem, user); - if (quality > 0) + QualityResult result = GetFabricatedItemQuality(selectedItem, user); + + float quality = selectedItem.Quality ?? result.Quality; + if (quality > 0 || result.HasRandomQualityRollChance) { name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName + '\n') .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName + '\n')); @@ -855,6 +879,49 @@ namespace Barotrauma.Items.Components { AutoScaleHorizontal = true }; + + if (result.HasRandomQualityRollChance) + { + var iconLayout = new GUIFrame(new RectTransform(new Vector2(0.4f, 1f), selectedItemFrame.RectTransform, anchor: Anchor.TopRight), style: null); + var icon = GameSession.CreateNotificationIcon(iconLayout, offset: true); + + float percentage1 = result.TotalPlusOnePercentage; + float percentage2 = result.TotalPlusTwoPercentage; + + string chance1text = percentage1.ToString("F1", CultureInfo.InvariantCulture); + string chance2text = percentage2.ToString("F1", CultureInfo.InvariantCulture); + + int quality1 = Math.Clamp(result.Quality + 1, min: 0, max: 3); + int quality2 = Math.Clamp(result.Quality + 2, min: 0, max: 3); + + LocalizedString quality1Text = TextManager.Get($"quality{quality1}"); + LocalizedString quality2Text = TextManager.Get($"quality{quality2}"); + + string localizationTag = percentage2 > 0f && percentage1 > 0 && quality1 != quality2 ? "meetsbonusrequirementtwice" : "meetsbonusrequirement"; + + var variables = new (string Key, LocalizedString Value)[] + { + ("[chance]", chance1text), ("[quality]", quality1Text), + ("[chance2]", chance2text), ("[quality2]", quality2Text) + }; + + if (MathUtils.NearlyEqual(percentage1, 0)) + { + variables = new[] { ("[chance]", chance2text), ("[quality]", quality2Text) }; + } + + if (quality1 == quality2) + { + LocalizedString rawPercentage = result.PlusOnePercentage.ToString("F1", CultureInfo.InvariantCulture); + variables = new[] { ("[chance]", rawPercentage), ("[quality]", quality1Text) }; + } + + LocalizedString qualityTooltip = TextManager.GetWithVariables(localizationTag, variables); + + icon.ToolTip = RichString.Rich(qualityTooltip); + icon.Visible = icon.CanBeFocused = true; + } + nameBlock.Padding = new Vector4(0, nameBlock.Padding.Y, GUI.IntScale(5), nameBlock.Padding.W); if (nameBlock.TextScale < 0.7f) { @@ -865,15 +932,15 @@ namespace Barotrauma.Items.Components nameBlock.Wrap = true; nameBlock.SetTextPos(); nameBlock.RectTransform.MinSize = new Point(0, (int)(nameBlock.TextSize.Y * nameBlock.TextScale)); - } - + } + if (!selectedItem.TargetItem.Description.IsNullOrEmpty()) { var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), selectedItem.TargetItem.Description, font: GUIStyle.SmallFont, wrap: true); description.Padding = new Vector4(0, description.Padding.Y, description.Padding.Z, description.Padding.W); - + while (description.Rect.Height + nameBlock.Rect.Height > paddedFrame.Rect.Height) { var lines = description.WrappedText.Split('\n'); @@ -884,13 +951,13 @@ namespace Barotrauma.Items.Components description.ToolTip = selectedItem.TargetItem.Description; } } - + IEnumerable inadequateSkills = Enumerable.Empty(); if (user != null) { inadequateSkills = selectedItem.RequiredSkills.Where(skill => user.GetSkillLevel(skill.Identifier) < Math.Round(skill.Level * SkillRequirementMultiplier)); } - + if (selectedItem.RequiredSkills.Any()) { LocalizedString text = ""; @@ -911,9 +978,10 @@ namespace Barotrauma.Items.Components float degreeOfSuccess = user == null ? 0.0f : FabricationDegreeOfSuccess(user, selectedItem.RequiredSkills); if (degreeOfSuccess > 0.5f) { degreeOfSuccess = 1.0f; } - float requiredTime = overrideRequiredTime ?? - (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user)); - + float requiredTime = overrideRequiredTime.TryUnwrap(out var time) + ? time + : (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user)); + if ((int)requiredTime > 0) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), @@ -936,7 +1004,6 @@ namespace Barotrauma.Items.Components font: GUIStyle.SmallFont); } - return true; } public void HighlightRecipe(string identifier, Color color) @@ -1046,6 +1113,15 @@ namespace Barotrauma.Items.Components } } + public override void OnPlayerSkillsChanged() + => RefreshSelectedItem(); + + public void RefreshSelectedItem() + { + if (!LastSelectedRecipe.TryUnwrap(out var lastSelected)) { return; } + CreateSelectedItemUI(lastSelected); + } + partial void UpdateRequiredTimeProjSpecific() { if (requiredTimeBlock == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index e06de7a92..1e636ec15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -66,7 +66,15 @@ namespace Barotrauma.Items.Components private float prevPassivePingRadius; private Vector2 center; - private float displayScale; + + /// + /// Current scale of the display, taking zoom into account. In other words, the scaling factor of world coordinates to coordinates on the display. + /// + public float DisplayScale + { + get; + private set; + } = 1.0f; private const float DisruptionUpdateInterval = 0.2f; private float disruptionUpdateTimer; @@ -751,9 +759,9 @@ namespace Barotrauma.Items.Components { var activePing = activePings[pingIndex]; float pingRadius = DisplayRadius * activePing.State / zoom; - if (disruptionUpdateTimer <= 0.0f) { UpdateDisruptions(transducerCenter, pingRadius / displayScale); } + if (disruptionUpdateTimer <= 0.0f) { UpdateDisruptions(transducerCenter, pingRadius / DisplayScale); } Ping(transducerCenter, transducerCenter, - pingRadius, activePing.PrevPingRadius, displayScale, range / zoom, passive: false, pingStrength: 2.0f); + pingRadius, activePing.PrevPingRadius, DisplayScale, range / zoom, passive: false, pingStrength: 2.0f); activePing.PrevPingRadius = pingRadius; } if (disruptionUpdateTimer <= 0.0f) @@ -770,7 +778,7 @@ namespace Barotrauma.Items.Components if (c.Params.HideInSonar) { continue; } if (!c.IsUnconscious && c.Params.DistantSonarRange > 0.0f && - ((c.WorldPosition - transducerCenter) * displayScale).LengthSquared() > DisplayRadius * DisplayRadius) + ((c.WorldPosition - transducerCenter) * DisplayScale).LengthSquared() > DisplayRadius * DisplayRadius) { Vector2 targetVector = c.WorldPosition - transducerCenter; if (targetVector.LengthSquared() > MathUtils.Pow2(c.Params.DistantSonarRange)) { continue; } @@ -818,7 +826,7 @@ namespace Barotrauma.Items.Components if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500) { Ping(t.WorldPosition, transducerCenter, - t.SoundRange * displayScale, 0, displayScale, range, + t.SoundRange * DisplayScale, 0, DisplayScale, range, passive: true, pingStrength: 0.5f, needsToBeInSector: t); if (t.IsWithinSector(transducerCenter)) { @@ -857,7 +865,7 @@ namespace Barotrauma.Items.Components displayBorderSize = 0.2f; center = rect.Center.ToVector2(); DisplayRadius = (rect.Width / 2.0f) * (1.0f - displayBorderSize); - displayScale = DisplayRadius / range * zoom; + DisplayScale = DisplayRadius / range * zoom; screenBackground?.Draw(spriteBatch, center, 0.0f, rect.Width / screenBackground.size.X); @@ -972,7 +980,7 @@ namespace Barotrauma.Items.Components aiTarget.SonarIconIdentifier, aiTarget, aiTarget.WorldPosition, transducerCenter, - displayScale, center, DisplayRadius * 0.975f); + DisplayScale, center, DisplayRadius * 0.975f); } } @@ -983,21 +991,21 @@ namespace Barotrauma.Items.Components if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true }) { DrawMarker(spriteBatch, - Level.Loaded.StartLocation.Name, + Level.Loaded.StartLocation.DisplayName.Value, (Level.Loaded.StartOutpost != null ? "outpost" : "location").ToIdentifier(), "startlocation", Level.Loaded.StartExitPosition, transducerCenter, - displayScale, center, DisplayRadius); + DisplayScale, center, DisplayRadius); } if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection }) { DrawMarker(spriteBatch, - Level.Loaded.EndLocation.Name, + Level.Loaded.EndLocation.DisplayName.Value, (Level.Loaded.EndOutpost != null ? "outpost" : "location").ToIdentifier(), "endlocation", Level.Loaded.EndExitPosition, transducerCenter, - displayScale, center, DisplayRadius); + DisplayScale, center, DisplayRadius); } for (int i = 0; i < Level.Loaded.Caves.Count; i++) @@ -1009,7 +1017,7 @@ namespace Barotrauma.Items.Components "cave".ToIdentifier(), "cave" + i, cave.StartPos.ToVector2(), transducerCenter, - displayScale, center, DisplayRadius); + DisplayScale, center, DisplayRadius); } } @@ -1026,7 +1034,7 @@ namespace Barotrauma.Items.Components mission.SonarIconIdentifier, "mission" + missionIndex + ":" + i, position, transducerCenter, - displayScale, center, DisplayRadius * 0.95f); + DisplayScale, center, DisplayRadius * 0.95f); } i++; } @@ -1059,7 +1067,7 @@ namespace Barotrauma.Items.Components DrawMarker(spriteBatch, i.Name, "mineral".ToIdentifier(), "mineralcluster" + i, c.center, transducerCenter, - displayScale, center, DisplayRadius * 0.95f, + DisplayScale, center, DisplayRadius * 0.95f, onlyShowTextOnMouseOver: true); } } @@ -1088,19 +1096,19 @@ namespace Barotrauma.Items.Components (sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine").ToIdentifier(), sub, sub.WorldPosition, transducerCenter, - displayScale, center, DisplayRadius * 0.95f); + DisplayScale, center, DisplayRadius * 0.95f); } if (GameMain.DebugDraw) { var steering = item.GetComponent(); - steering?.DebugDrawHUD(spriteBatch, transducerCenter, displayScale, DisplayRadius, center); + steering?.DebugDrawHUD(spriteBatch, transducerCenter, DisplayScale, DisplayRadius, center); } } private void DrawOwnSubmarineBorders(SpriteBatch spriteBatch, Vector2 transducerCenter, float signalStrength) { - float simScale = displayScale * Physics.DisplayToSimRation * zoom; + float simScale = DisplayScale * Physics.DisplayToSimRation; foreach (Submarine submarine in Submarine.Loaded) { @@ -1167,7 +1175,7 @@ namespace Barotrauma.Items.Components private void DrawDockingPorts(SpriteBatch spriteBatch, Vector2 transducerCenter, float signalStrength) { - float scale = displayScale * zoom; + float scale = DisplayScale; Steering steering = item.GetComponent(); if (steering != null && steering.DockingModeEnabled && steering.ActiveDockingSource != null) @@ -1219,7 +1227,7 @@ namespace Barotrauma.Items.Components private void DrawDockingIndicator(SpriteBatch spriteBatch, Steering steering, ref Vector2 transducerCenter) { - float scale = displayScale * zoom; + float scale = DisplayScale; Vector2 worldFocusPos = (steering.ActiveDockingSource.Item.WorldPosition + steering.DockingTarget.Item.WorldPosition) / 2.0f; worldFocusPos.X = steering.DockingTarget.Item.WorldPosition.X; @@ -1591,7 +1599,7 @@ namespace Barotrauma.Items.Components { lineStep /= zoom; zStep /= zoom; - range *= displayScale; + range *= DisplayScale; float length = (point1 - point2).Length(); Vector2 lineDir = (point2 - point1) / length; for (float x = 0; x < length; x += lineStep * Rand.Range(0.8f, 1.2f)) @@ -1602,12 +1610,12 @@ namespace Barotrauma.Items.Components //ignore if outside the display Vector2 transducerDiff = point - transducerPos; - Vector2 transducerDisplayDiff = transducerDiff * displayScale; + Vector2 transducerDisplayDiff = transducerDiff * DisplayScale / zoom; if (transducerDisplayDiff.LengthSquared() > DisplayRadius * DisplayRadius) { continue; } //ignore if the point is not within the ping Vector2 pointDiff = point - pingSource; - Vector2 displayPointDiff = pointDiff * displayScale; + Vector2 displayPointDiff = pointDiff * DisplayScale / zoom; float displayPointDistSqr = displayPointDiff.LengthSquared(); if (displayPointDistSqr < prevPingRadius * prevPingRadius || displayPointDistSqr > pingRadius * pingRadius) { continue; } @@ -1628,9 +1636,9 @@ namespace Barotrauma.Items.Components float displayPointDist = (float)Math.Sqrt(displayPointDistSqr); float alpha = pingStrength * Rand.Range(1.5f, 2.0f); - for (float z = 0; z < DisplayRadius - transducerDist * displayScale; z += zStep) + for (float z = 0; z < DisplayRadius - transducerDist * DisplayScale; z += zStep) { - Vector2 pos = point + Rand.Vector(150.0f / zoom) + pingDirection * z / displayScale; + Vector2 pos = point + Rand.Vector(150.0f / zoom) + pingDirection * z / DisplayScale; float fadeTimer = alpha * (1.0f - displayPointDist / range); if (needsToBeInSector != null) @@ -1697,7 +1705,7 @@ namespace Barotrauma.Items.Components private bool CheckBlipVisibility(SonarBlip blip, Vector2 transducerPos) { - Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom; + Vector2 pos = (blip.Position - transducerPos) * DisplayScale; pos.Y = -pos.Y; float posDistSqr = pos.LengthSquared(); @@ -1731,7 +1739,7 @@ namespace Barotrauma.Items.Components } if (currentPingIndex != -1 && activePings[currentPingIndex].IsDirectional) { - var pos = (resourcePos - transducerPos) * displayScale * zoom; + var pos = (resourcePos - transducerPos) * DisplayScale; pos.Y = -pos.Y; var length = pos.Length(); var dir = pos / length; @@ -1749,7 +1757,7 @@ namespace Barotrauma.Items.Components float distort = 1.0f - item.Condition / item.MaxCondition; - Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom; + Vector2 pos = (blip.Position - transducerPos) * DisplayScale; pos.Y = -pos.Y; if (Rand.Range(0.5f, 2.0f) < distort) { pos.X = -pos.X; } @@ -1825,14 +1833,13 @@ namespace Barotrauma.Items.Components Vector2 position = worldPosition - transducerPosition; - position *= zoom; position *= scale; position.Y = -position.Y; float textAlpha = MathHelper.Clamp(1.5f - dist / 50000.0f, 0.5f, 1.0f); Vector2 dir = Vector2.Normalize(position); - Vector2 markerPos = (linearDist * zoom * scale > radius) ? dir * radius : position; + Vector2 markerPos = (linearDist * scale > radius) ? dir * radius : position; markerPos += center; markerPos.X = (int)markerPos.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index fd5be2193..90d3f85ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -216,7 +216,7 @@ namespace Barotrauma.Items.Components } }; 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), + GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.DisplayName, GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, @@ -243,7 +243,7 @@ namespace Barotrauma.Items.Components }; levelEndTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.BottomCenter), - (GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.Name, GUIStyle.SmallFont, textLimit), + (GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.DisplayName, GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, @@ -389,7 +389,7 @@ namespace Barotrauma.Items.Components if (!ObjectiveManager.AllActiveObjectivesCompleted()) { exitOutpostPrompt = new GUIMessageBox("", - TextManager.GetWithVariable("CampaignExitTutorialOutpostPrompt", "[locationname]", campaign.Map.CurrentLocation.Name), + TextManager.GetWithVariable("CampaignExitTutorialOutpostPrompt", "[locationname]", campaign.Map.CurrentLocation.DisplayName), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); exitOutpostPrompt.Buttons[0].OnClicked += (_, _) => { @@ -509,9 +509,9 @@ namespace Barotrauma.Items.Components noPowerTip = TextManager.Get("SteeringNoPowerTip"); autoPilotMaintainPosTip = TextManager.Get("SteeringAutoPilotMaintainPosTip"); autoPilotLevelStartTip = TextManager.GetWithVariable("SteeringAutoPilotLocationTip", "[locationname]", - GameMain.GameSession?.StartLocation == null ? "Start" : GameMain.GameSession.StartLocation.Name); + GameMain.GameSession?.StartLocation == null ? "Start" : GameMain.GameSession.StartLocation.DisplayName); autoPilotLevelEndTip = TextManager.GetWithVariable("SteeringAutoPilotLocationTip", "[locationname]", - GameMain.GameSession?.EndLocation == null ? "End" : GameMain.GameSession.EndLocation.Name); + GameMain.GameSession?.EndLocation == null ? "End" : GameMain.GameSession.EndLocation.DisplayName); } protected override void OnResolutionChanged() @@ -589,7 +589,8 @@ namespace Barotrauma.Items.Components Sonar sonar = item.GetComponent(); if (sonar != null && controlledSub != null) { - Vector2 displayPosToMaintain = ((posToMaintain.Value - controlledSub.WorldPosition)) / sonar.Range * sonar.DisplayRadius * sonar.Zoom; + Vector2 displayPosToMaintain = ((posToMaintain.Value - controlledSub.WorldPosition)) * sonar.DisplayScale; + displayPosToMaintain.Y = -displayPosToMaintain.Y; displayPosToMaintain = displayPosToMaintain.ClampLength(velRect.Width / 2); displayPosToMaintain = steerArea.Rect.Center.ToVector2() + displayPosToMaintain; @@ -670,14 +671,14 @@ namespace Barotrauma.Items.Components pos2.Y = -pos2.Y; pos2 += center; - GUI.DrawLine(spriteBatch, - pos1, + GUI.DrawLine(spriteBatch, + pos1, pos2, GUIStyle.Red * 0.6f, width: 3); if (obstacle.Intersection.HasValue) { - Vector2 intersectionPos = (obstacle.Intersection.Value - transducerCenter) *displayScale; + Vector2 intersectionPos = (obstacle.Intersection.Value - transducerCenter) * displayScale; intersectionPos.Y = -intersectionPos.Y; intersectionPos += center; GUI.DrawRectangle(spriteBatch, intersectionPos - Vector2.One * 2, Vector2.One * 4, GUIStyle.Red); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index 8f58322ae..1b76a65ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -151,7 +151,7 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, new Vector2(debugRayStartPos.X, -debugRayStartPos.Y), new Vector2(debugRayEndPos.X, -debugRayEndPos.Y), - Color.Yellow); + Color.Yellow, width: 3f); } } #endif diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index c821da993..dc5b84592 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -95,7 +95,7 @@ namespace Barotrauma.Items.Components MaxValueFloat = numberInputMax, FloatValue = Math.Clamp(floatSignal, numberInputMin, numberInputMax), DecimalsToDisplay = ciElement.NumberInputDecimalPlaces, - valueStep = numberInputStep, + ValueStep = numberInputStep, OnValueChanged = (ni) => { if (GameMain.Client == null) @@ -121,7 +121,7 @@ namespace Barotrauma.Items.Components MinValueInt = numberInputMin, MaxValueInt = numberInputMax, IntValue = Math.Clamp(intSignal, numberInputMin, numberInputMax), - valueStep = numberInputStep, + ValueStep = numberInputStep, OnValueChanged = (ni) => { if (GameMain.Client == null) @@ -137,7 +137,8 @@ namespace Barotrauma.Items.Components } else { - DebugConsole.LogError($"Error creating a CustomInterface component: unexpected NumberType \"{(ciElement.NumberType.HasValue ? ciElement.NumberType.Value.ToString() : "none")}\""); + DebugConsole.LogError($"Error creating a CustomInterface component: unexpected NumberType \"{(ciElement.NumberType.HasValue ? ciElement.NumberType.Value.ToString() : "none")}\"", + contentPackage: item.Prefab.ContentPackage); } if (numberInput != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 698db9c7c..a2f10cce8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -349,8 +349,8 @@ namespace Barotrauma.Items.Components } GUI.DrawString(spriteBatch, hudPos, texts[0].Value, textColors[0] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); - hudPos.X += 5.0f; - hudPos.Y += 24.0f * GameSettings.CurrentConfig.Graphics.TextScale; + hudPos.X += 5.0f * GUI.Scale; + hudPos.Y += GUIStyle.SubHeadingFont.MeasureString(texts[0].Value).Y; hudPos.X = (int)hudPos.X; hudPos.Y = (int)hudPos.Y; @@ -358,7 +358,7 @@ namespace Barotrauma.Items.Components for (int i = 1; i < texts.Count; i++) { GUI.DrawString(spriteBatch, hudPos, texts[i], textColors[i] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SmallFont); - hudPos.Y += (int)(18.0f * GameSettings.CurrentConfig.Graphics.TextScale); + hudPos.Y += (int)(GUIStyle.SubHeadingFont.MeasureString(texts[i].Value).Y); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index a4903a351..c43909cc7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -16,6 +16,8 @@ namespace Barotrauma.Items.Components private GUIProgressBar powerIndicator; + private Vector2? debugDrawTargetPos; + public int UIElementHeight { get @@ -422,9 +424,23 @@ namespace Barotrauma.Items.Components if (GameMain.DebugDraw) { Vector2 firingPos = GetRelativeFiringPosition(); + Vector2 endPos = firingPos + 3500 * GetBarrelDir(); firingPos.Y = -firingPos.Y; + endPos.Y = -endPos.Y; GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitX * 5, firingPos + Vector2.UnitX * 5, Color.Red); GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitY * 5, firingPos + Vector2.UnitY * 5, Color.Red); + + if (debugDrawTargetPos.HasValue) + { + Vector2 targetPos = debugDrawTargetPos.Value; + targetPos.Y = -targetPos.Y; + GUI.DrawLine(spriteBatch, targetPos - Vector2.UnitX * 5, targetPos + Vector2.UnitX * 5, Color.Magenta, width: 5); + GUI.DrawLine(spriteBatch, targetPos - Vector2.UnitY * 5, targetPos + Vector2.UnitY * 5, Color.Magenta, width: 5); + + GUI.DrawLine(spriteBatch, firingPos, targetPos, Color.Magenta, width: 2); + + } + GUI.DrawLine(spriteBatch, firingPos, endPos, Color.LightGray, width: 2); } if (!editing || GUI.DisableHUD || !item.IsSelected) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index ecb897421..1846a30b6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -258,7 +258,7 @@ namespace Barotrauma else { LocalizedString description = item.Description; - if (item.HasTag(Tags.IdCard) || item.HasTag(Tags.DespawnContainer)) + if (item.HasTag(Tags.IdCardTag) || item.HasTag(Tags.DespawnContainer)) { string[] readTags = item.Tags.Split(','); string idName = null; @@ -730,7 +730,7 @@ namespace Barotrauma DraggingInventory = null; subInventory.savedPosition = PlayerInput.MousePosition.ToPoint(); } - else + else if (DraggingInventory == subInventory) { subInventory.savedPosition = PlayerInput.MousePosition.ToPoint(); } @@ -901,7 +901,7 @@ namespace Barotrauma if (IsOnInventorySlot(Character.Controlled.SelectedCharacter.Inventory)) { return true; } } - bool IsOnInventorySlot(Inventory inventory) + static bool IsOnInventorySlot(Inventory inventory) { for (var i = 0; i < inventory.visualSlots.Length; i++) { @@ -1107,7 +1107,7 @@ namespace Barotrauma if (container.MovableFrame && !IsInventoryHoverAvailable(Owner as Character, container)) { - if (positionUpdateQueued) // Wait a frame before updating the positioning of the container after a resolution change to have everything working + if (container.Inventory.positionUpdateQueued) // Wait a frame before updating the positioning of the container after a resolution change to have everything working { int height = (int)(movableFrameRectHeight * UIScale); CreateSlots(); @@ -1116,7 +1116,7 @@ namespace Barotrauma draggableIndicatorOffset = DraggableIndicator.size * draggableIndicatorScale / 2f; draggableIndicatorOffset += new Vector2(height / 2f - draggableIndicatorOffset.Y); container.Inventory.originalPos = container.Inventory.savedPosition = container.Inventory.movableFrameRect.Center; - positionUpdateQueued = false; + container.Inventory.positionUpdateQueued = false; } if (container.Inventory.movableFrameRect.Size == Point.Zero || GUI.HasSizeChanged(prevScreenResolution, prevUIScale, prevHUDScale)) @@ -1127,11 +1127,20 @@ namespace Barotrauma prevScreenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); prevUIScale = UIScale; prevHUDScale = GUI.Scale; - positionUpdateQueued = true; + container.Inventory.positionUpdateQueued = true; } else { - GUI.DrawRectangle(spriteBatch, container.Inventory.movableFrameRect, movableFrameRectColor, true); + Color color = movableFrameRectColor; + if (DraggingInventory != null && DraggingInventory != container.Inventory) + { + color *= 0.7f; + } + else if (container.Inventory.movableFrameRect.Contains(PlayerInput.MousePosition)) + { + color = Color.Lerp(color, PlayerInput.PrimaryMouseButtonHeld() ? Color.Black : Color.White, 0.25f); + } + GUI.DrawRectangle(spriteBatch, container.Inventory.movableFrameRect, color, true); DraggableIndicator.Draw(spriteBatch, container.Inventory.movableFrameRect.Location.ToVector2() + draggableIndicatorOffset, 0, draggableIndicatorScale); } } @@ -1269,12 +1278,20 @@ namespace Barotrauma if (DraggingItems.Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1 || selectedInventory.GetItemsAt(slotIndex).Count(it => !it.IsFullCondition && it.Condition > 0.0f) > 1) { - allowCombine = false; + allowCombine = false; } int itemCount = 0; foreach (Item item in DraggingItems) { - bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); + if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container && + container.Inventory.CanBePut(item)) + { + if (!container.AllowDragAndDrop || !container.DrawInventory) + { + allowCombine = false; + } + } + bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); if (success) { anySuccess = true; @@ -1380,18 +1397,17 @@ namespace Barotrauma protected static Rectangle GetSubInventoryHoverArea(SlotReference subSlot) { - Rectangle hoverArea; - if ((Screen.Selected != GameMain.SubEditorScreen || GameMain.SubEditorScreen.DrawCharacterInventory) && - (!subSlot.Inventory.Movable() || - (Character.Controlled?.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item)) || - (subSlot.ParentInventory is CharacterInventory characterInventory && characterInventory.CurrentLayout != CharacterInventory.Layout.Default))) + if (Character.Controlled == null) { - //slot not visible as a separate, movable panel -> just use the area of the slot directly - hoverArea = subSlot.Slot.Rect; - hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); - hoverArea = Rectangle.Union(hoverArea, subSlot.Slot.EquipButtonRect); + return Rectangle.Empty; } - else + + Rectangle hoverArea; + bool isMovable = subSlot.Inventory.Movable() && !subSlot.ParentInventory.IsInventoryHoverAvailable(Character.Controlled, subSlot.Item?.GetComponent()); + bool unEquipped = Character.Controlled.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item); + bool isDefaultLayout = subSlot.ParentInventory is not CharacterInventory characterInventory || characterInventory.CurrentLayout == CharacterInventory.Layout.Default; + bool subEditorCharacterInventoryHidden = Screen.Selected == GameMain.SubEditorScreen && !GameMain.SubEditorScreen.DrawCharacterInventory; + if (subEditorCharacterInventoryHidden || (isMovable && !unEquipped && isDefaultLayout)) { hoverArea = subSlot.Inventory.BackgroundFrame; hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); @@ -1400,6 +1416,13 @@ namespace Barotrauma hoverArea = Rectangle.Union(hoverArea, subSlot.Inventory.movableFrameRect); } } + else + { + //slot not visible as a separate, movable panel -> just use the area of the slot directly + hoverArea = subSlot.Slot.Rect; + hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); + hoverArea = Rectangle.Union(hoverArea, subSlot.Slot.EquipButtonRect); + } if (subSlot.Inventory?.visualSlots != null) { @@ -1584,11 +1607,16 @@ namespace Barotrauma if (DraggingItems.Any() && inventory != null && slotIndex > -1 && slotIndex < inventory.visualSlots.Length) { + var itemInSlot = inventory.slots[slotIndex].FirstOrDefault(); if (inventory.CanBePutInSlot(DraggingItems.First(), slotIndex)) { canBePut = true; } - else if (inventory.slots[slotIndex].FirstOrDefault()?.OwnInventory?.CanBePut(DraggingItems.First()) ?? false) + else if + (itemInSlot?.OwnInventory != null && + itemInSlot.OwnInventory.CanBePut(DraggingItems.First()) && + itemInSlot.OwnInventory.Container.AllowDragAndDrop && + itemInSlot.OwnInventory.Container.DrawInventory) { canBePut = true; } @@ -1922,9 +1950,25 @@ namespace Barotrauma foreach (UInt16 id in receivedItemIDs[i]) { if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; } + + if (Owner is Item thisItem && thisItem.Container == item) + { + //if this item is inside the item we're trying to contain inside it, we need to drop it (both items can't be inside each other!) + //can happen when a player swaps the items to be "the other way around", and we receive a message about the contained item + //before the message about the "parent item" being placed in some other inventory (like the player's inventory) + thisItem.Drop(null); + } + if (!TryPutItem(item, i, false, false, null, false)) { - ForceToSlot(item, i); + try + { + ForceToSlot(item, i); + } + catch (InvalidOperationException e) + { + DebugConsole.AddSafeError(e.Message + "\n" + e.StackTrace.CleanupStackTrace()); + } } for (int j = 0; j < capacity; j++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 0f42a313c..1633cf7bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -132,9 +132,9 @@ namespace Barotrauma return GetDrawDepth(SpriteDepth + DrawDepthOffset, Sprite); } - public Color GetSpriteColor(bool withHighlight = false) + public Color GetSpriteColor(Color? defaultColor = null, bool withHighlight = false) { - Color color = spriteColor; + Color color = defaultColor ?? spriteColor; if (Prefab.UseContainedSpriteColor && ownInventory != null) { foreach (Item item in ContainedItems) @@ -286,7 +286,8 @@ namespace Barotrauma { int padding = 100; - Vector2 min = new Vector2(-rect.Width / 2 - padding, -rect.Height / 2 - padding); + RectangleF boundingBox = GetTransformedQuad().BoundingAxisAlignedRectangle; + Vector2 min = new Vector2(-boundingBox.Width / 2 - padding, -boundingBox.Height / 2 - padding); Vector2 max = -min; foreach (IDrawableComponent drawable in drawableComponents) @@ -333,9 +334,7 @@ namespace Barotrauma else if (!ShowItems) { return; } } - Color color = - overrideColor ?? - (IsIncludedInSelection && editing ? GUIStyle.Blue : GetSpriteColor(withHighlight: true)); + Color color = GetSpriteColor(spriteColor); bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; bool renderTransparent = isWiringMode && GetComponent() == null; @@ -388,9 +387,9 @@ namespace Barotrauma { if (Prefab.ResizeHorizontal || Prefab.ResizeVertical) { - Vector2 size = new Vector2(rect.Width, rect.Height); if (color.A > 0) { + Vector2 size = new Vector2(rect.Width, rect.Height); activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset, size, color: color, textureScale: Vector2.One * Scale, @@ -403,18 +402,7 @@ namespace Barotrauma textureScale: Vector2.One * Scale, depth: d); } - foreach (var decorativeSprite in Prefab.DecorativeSprites) - { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? RotationRad : -RotationRad) * Scale; - if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } - if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } - decorativeSprite.Sprite.DrawTiled(spriteBatch, - new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), - size, color: color, - textureScale: Vector2.One * Scale, - depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); - } + DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth); } } else @@ -434,19 +422,8 @@ namespace Barotrauma Prefab.InfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, Prefab.InfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.001f); Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.002f); } - foreach (var decorativeSprite in Prefab.DecorativeSprites) - { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - bool flipX = flippedX && Prefab.CanSpriteFlipX; - bool flipY = flippedY && Prefab.CanSpriteFlipY; - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flipX ^ flipY ? RotationRad : -RotationRad) * Scale; - if (flipX) { offset.X = -offset.X; } - if (flipY) { offset.Y = -offset.Y; } - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - RotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, - depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); - } + + DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, -RotationRad, depth); } } else if (body.Enabled) @@ -490,21 +467,7 @@ namespace Barotrauma float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, d, Scale); } - foreach (var decorativeSprite in Prefab.DecorativeSprites) - { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - 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 = 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(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)); - } + DrawDecorativeSprites(spriteBatch, body.DrawPosition, flipX: body.Dir < 0, flipY: false, rotation: body.Rotation, depth: depth); } foreach (var upgrade in Upgrades) @@ -522,7 +485,6 @@ namespace Barotrauma rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } - } activeSprite.effects = oldEffects; @@ -567,8 +529,14 @@ namespace Barotrauma Vector2 drawPos = new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)); Vector2 drawSize = new Vector2(MathF.Ceiling(rect.Width + Math.Abs(drawPos.X - (int)drawPos.X)), MathF.Ceiling(rect.Height + Math.Abs(drawPos.Y - (int)drawPos.Y))); drawPos = new Vector2(MathF.Floor(drawPos.X), MathF.Floor(drawPos.Y)); - GUI.DrawRectangle(spriteBatch, drawPos, drawSize, - Color.White, false, 0, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); + GUI.DrawRectangle(sb: spriteBatch, + center: drawPos + drawSize * 0.5f, + width: drawSize.X, + height: drawSize.Y, + rotation: RotationRad, + clr: Color.White, + depth: 0, + thickness: 2f / Screen.Selected.Cam.Zoom); foreach (Rectangle t in Prefab.Triggers) { @@ -618,6 +586,62 @@ namespace Barotrauma } return origin; } + + Color GetSpriteColor(Color defaultColor) + { + return + overrideColor ?? + (IsIncludedInSelection && editing ? GUIStyle.Blue : this.GetSpriteColor(defaultColor: defaultColor, withHighlight: true)); + } + } + + public void DrawDecorativeSprites(SpriteBatch spriteBatch, Vector2 drawPos, bool flipX, bool flipY, float rotation, float depth) + { + foreach (var decorativeSprite in Prefab.DecorativeSprites) + { + Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color).Multiply(GetSpriteColor(spriteColor)); + if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, + flipX ^ flipY ? -rotation : rotation) * Scale; + + if (ResizeHorizontal || ResizeVertical) + { + decorativeSprite.Sprite.DrawTiled(spriteBatch, + new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), + new Vector2(rect.Width, rect.Height), color: decorativeSpriteColor, + textureScale: Vector2.One * Scale, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); + } + else + { + float spriteRotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); + + Vector2 origin = decorativeSprite.Sprite.Origin; + SpriteEffects spriteEffects = SpriteEffects.None; + if (flipX && Prefab.CanSpriteFlipX) + { + offset.X = -offset.X; + origin.X = -origin.X + decorativeSprite.Sprite.size.X; + spriteEffects = SpriteEffects.FlipHorizontally; + } + if (flipY && Prefab.CanSpriteFlipY) + { + offset.Y = -offset.Y; + origin.Y = -origin.Y + decorativeSprite.Sprite.size.Y; + spriteEffects |= SpriteEffects.FlipVertically; + } + if (body != null) + { + var ca = MathF.Cos(-body.DrawRotation); + var sa = MathF.Sin(-body.DrawRotation); + offset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); + } + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(drawPos.X + offset.X, -(drawPos.Y + offset.Y)), decorativeSpriteColor, origin, + -rotation + spriteRotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffects, + depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); + } + } } partial void OnCollisionProjSpecific(float impact) @@ -795,6 +819,19 @@ namespace Barotrauma } } + public override bool IsMouseOn(Vector2 position) + { + Vector2 rectSize = rect.Size.ToVector2(); + + Vector2 bodyPos = WorldPosition; + + Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, RotationRad); + + return + Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f && + Math.Abs(transformedMousePos.Y - bodyPos.Y) < rectSize.Y / 2.0f; + } + public GUIComponent CreateEditingHUD(bool inGame = false) { activeEditors.Clear(); @@ -852,7 +889,12 @@ namespace Barotrauma CanBeFocused = true }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") + GUINumberInput rotationField = + itemEditor.Fields.TryGetValue("Rotation".ToIdentifier(), out var rotationFieldComponents) + ? rotationFieldComponents.OfType().FirstOrDefault() + : null; + + var mirrorX = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityXToolTip"), Enabled = Prefab.CanFlipX, @@ -863,10 +905,13 @@ namespace Barotrauma me.FlipX(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); } + ColorFlipButton(button, FlippedX); + if (rotationField != null) { rotationField.FloatValue = Rotation; } return true; } }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall") + ColorFlipButton(mirrorX, FlippedX); + var mirrorY = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityYToolTip"), Enabled = Prefab.CanFlipY, @@ -877,9 +922,12 @@ namespace Barotrauma me.FlipY(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } + ColorFlipButton(button, FlippedY); + if (rotationField != null) { rotationField.FloatValue = Rotation; } return true; } }; + ColorFlipButton(mirrorY, FlippedY); if (Sprite != null) { var reloadTextureButton = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite"), style: "GUIButtonSmall"); @@ -1540,6 +1588,23 @@ namespace Barotrauma RemoveFromDroppedStack(allowClientExecute: true); } break; + case EventType.SetHighlight: + bool isTargetedForThisClient = msg.ReadBoolean(); + if (isTargetedForThisClient) + { + bool highlight = msg.ReadBoolean(); + ExternalHighlight = highlight; + if (highlight) + { + Color highlightColor = msg.ReadColorR8G8B8A8(); + HighlightColor = highlightColor; + } + else + { + HighlightColor = null; + } + } + break; default: throw new Exception($"Malformed incoming item event: unsupported event type {eventType}"); } @@ -1940,5 +2005,13 @@ namespace Barotrauma Inventory.DraggingSlot = null; } } + + public void OnPlayerSkillsChanged() + { + foreach (ItemComponent ic in components) + { + ic.OnPlayerSkillsChanged(); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 49c3eb0e3..7a7701b9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -321,10 +321,8 @@ namespace Barotrauma } else { - if (ResizeHorizontal) - placeSize.X = Math.Max(position.X - placePosition.X, Size.X); - if (ResizeVertical) - placeSize.Y = Math.Max(placePosition.Y - position.Y, Size.Y); + if (ResizeHorizontal) { placeSize.X = Math.Max(position.X - placePosition.X, Size.X); } + if (ResizeVertical) { placeSize.Y = Math.Max(placePosition.Y - position.Y, Size.Y); } if (PlayerInput.PrimaryMouseButtonReleased()) { @@ -369,15 +367,31 @@ namespace Barotrauma } } - public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, SpriteEffects spriteEffects = SpriteEffects.None) + public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, float rotation = 0.0f, SpriteEffects spriteEffects = SpriteEffects.None) { if (!ResizeHorizontal && !ResizeVertical) { - Sprite.Draw(spriteBatch, new Vector2(placeRect.Center.X, -(placeRect.Y - placeRect.Height / 2)), SpriteColor * 0.8f, scale: scale); + sprite.Draw( + spriteBatch: spriteBatch, + pos: new Vector2(placeRect.Center.X, + -(placeRect.Y - placeRect.Height / 2)), + color: SpriteColor * 0.8f, + scale: scale, + rotate: rotation, + spriteEffect: spriteEffects ^ sprite.effects); } else { - Sprite.DrawTiled(spriteBatch, new Vector2(placeRect.X, -placeRect.Y), placeRect.Size.ToVector2(), SpriteColor * 0.8f); + Vector2 position = placeRect.Location.ToVector2(); + Vector2 placeSize = placeRect.Size.ToVector2(); + sprite?.DrawTiled( + spriteBatch: spriteBatch, + position: new Vector2(position.X, -position.Y), + targetSize: placeSize, + rotation: rotation, + textureScale: Vector2.One * scale, + color: SpriteColor * 0.8f, + spriteEffects: spriteEffects ^ sprite.effects); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 25f13500c..b4786982f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -277,12 +277,21 @@ namespace Barotrauma Rectangle drawRect = Submarine == null ? rect : new Rectangle((int)(Submarine.DrawPosition.X + rect.X), (int)(Submarine.DrawPosition.Y + rect.Y), rect.Width, rect.Height); - if ((IsSelected || IsHighlighted) && editing) + if (editing) { + if (IsSelected || IsHighlighted) + { + GUI.DrawRectangle(spriteBatch, + new Vector2(drawRect.X, -drawRect.Y), + new Vector2(rect.Width, rect.Height), + (IsHighlighted ? Color.LightBlue * 0.8f : GUIStyle.Red * 0.5f) * alpha, false, 0, (int)Math.Max(5.0f / Screen.Selected.Cam.Zoom, 1.0f)); + } + + float waterHeight = WaterVolume / rect.Width; GUI.DrawRectangle(spriteBatch, - new Vector2(drawRect.X, -drawRect.Y), - new Vector2(rect.Width, rect.Height), - (IsHighlighted ? Color.LightBlue * 0.8f : GUIStyle.Red * 0.5f) * alpha, false, 0, (int)Math.Max(5.0f / Screen.Selected.Cam.Zoom, 1.0f)); + new Vector2(drawRect.X, -drawRect.Y + drawRect.Height - waterHeight), + new Vector2(drawRect.Width, waterHeight), + Color.Blue * 0.25f, isFilled: true); } GUI.DrawRectangle(spriteBatch, @@ -746,7 +755,7 @@ namespace Barotrauma var newFire = i < FireSources.Count ? FireSources[i] : - new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true); + new FireSource(Submarine == null ? pos : pos + Submarine.Position, sourceCharacter: null, isNetworkMessage: true); newFire.Position = pos; newFire.Size = new Vector2(size, newFire.Size.Y); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index 797801633..52ba077ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -86,7 +86,7 @@ namespace Barotrauma MathUtils.RoundTowardsClosest(center.Y, Submarine.GridSize.Y) - center.Y - Submarine.GridSize.Y / 2); MapEntity.SelectedList.Clear(); - assemblyEntities.ForEach(e => MapEntity.AddSelection(e)); + entities.ForEach(e => MapEntity.AddSelection(e)); foreach (MapEntity mapEntity in assemblyEntities) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs index 7dc0b6ad4..1bdf40355 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs @@ -43,7 +43,7 @@ namespace Barotrauma { mainElement = mainElement.FirstElement(); prefabs.Clear(); - DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.Yellow); + DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.MediumPurple); } else if (prefabs.Any()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 38299827b..5fe2aeca2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -1,10 +1,9 @@ using Barotrauma.Networking; +using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Linq; using System.Collections.Generic; -using FarseerPhysics; namespace Barotrauma { @@ -53,7 +52,7 @@ namespace Barotrauma foreach (InterestingPosition pos in PositionsOfInterest) { Color color = Color.Yellow; - if (pos.PositionType == PositionType.Cave) + if (pos.PositionType == PositionType.Cave || pos.PositionType == PositionType.AbyssCave) { color = Color.DarkOrange; } @@ -61,6 +60,10 @@ namespace Barotrauma { color = Color.LightGray; } + if (!pos.IsValid) + { + color = Color.Red; + } GUI.DrawRectangle(spriteBatch, new Vector2(pos.Position.X - 15.0f, -pos.Position.Y - 15.0f), new Vector2(30.0f, 30.0f), color, true); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index 745af094f..91a25af00 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -162,6 +162,8 @@ namespace Barotrauma CanBeVisible = Sprite != null || Prefab.DeformableSprite != null || + ParticleEmitters is { Length: > 0 } || + (GameMain.DebugDraw && Triggers is { Count: > 0 }) || Prefab.OverrideProperties.Any(p => p != null && (p.Sprites.Any() || p.DeformableSprite != null)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 4ec014826..80027c367 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -23,7 +23,14 @@ namespace Barotrauma private Rectangle currentGridIndices; public bool ForceRefreshVisibleObjects; - + + partial void RemoveProjSpecific() + { + visibleObjectsBack.Clear(); + visibleObjectsMid.Clear(); + visibleObjectsFront.Clear(); + } + partial void UpdateProjSpecific(float deltaTime) { foreach (LevelObject obj in visibleObjectsBack) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 82ad83280..bb2024258 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -133,6 +133,8 @@ namespace Barotrauma.Lights public Rectangle BoundingBox { get; private set; } + public bool IsInvalid { get; private set; } + public ConvexHull(Rectangle rect, bool isHorizontal, MapEntity parent) { shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice) @@ -481,15 +483,34 @@ namespace Barotrauma.Lights for (int i = 0; i < 4; i++) { vertices[i].WorldPos = vertices[i].Pos; + ValidateVertex(vertices[i].WorldPos, "vertices[i].Pos"); segments[i].Start.WorldPos = segments[i].Start.Pos; + ValidateVertex(segments[i].Start.WorldPos, "segments[i].Start.Pos"); segments[i].End.WorldPos = segments[i].End.Pos; + ValidateVertex(segments[i].End.WorldPos, "segments[i].End.Pos"); } if (ParentEntity == null || ParentEntity.Submarine == null) { return; } for (int i = 0; i < 4; i++) { vertices[i].WorldPos += ParentEntity.Submarine.DrawPosition; + ValidateVertex(vertices[i].WorldPos, "vertices[i].WorldPos"); segments[i].Start.WorldPos += ParentEntity.Submarine.DrawPosition; + ValidateVertex(segments[i].Start.WorldPos, "segments[i].Start.WorldPos"); segments[i].End.WorldPos += ParentEntity.Submarine.DrawPosition; + ValidateVertex(segments[i].End.WorldPos, "segments[i].End.WorldPos"); + } + + void ValidateVertex(Vector2 vertex, string debugName) + { + if (!MathUtils.IsValid(vertex)) + { + IsInvalid = true; + string errorMsg = $"Invalid vertex on convex hull ({debugName}: {vertex}, parent entity: {ParentEntity?.ToString() ?? "null"})."; +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#endif + GameAnalyticsManager.AddErrorEventOnce("ConvexHull.RefreshWorldPositions:InvalidVertex", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index c04db86d6..4de4c7810 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -234,6 +234,15 @@ namespace Barotrauma.Lights } } + public void DebugDrawVertices(SpriteBatch spriteBatch) + { + foreach (LightSource light in lights) + { + if (!light.Enabled) { continue; } + light.DebugDrawVertices(spriteBatch); + } + } + public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) { if (!LightingEnabled) { return; } @@ -269,6 +278,9 @@ namespace Barotrauma.Lights light.Position = pos; } + //above the top boundary of the level (in an inactive respawn shuttle?) + if (Level.Loaded != null && light.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + float range = light.LightSourceParams.TextureRange; if (light.LightSprite != null) { @@ -801,6 +813,8 @@ namespace Barotrauma.Lights public void ClearLights() { + activeLights.Clear(); + activeLightsWithLightVolume.Clear(); lights.Clear(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 7606b40c6..5e9a1f267 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -37,9 +37,9 @@ namespace Barotrauma.Lights TextureRange = range; if (OverrideLightTexture != null) { - TextureRange += Math.Max( - Math.Abs(OverrideLightTexture.RelativeOrigin.X - 0.5f) * OverrideLightTexture.size.X, - Math.Abs(OverrideLightTexture.RelativeOrigin.Y - 0.5f) * OverrideLightTexture.size.Y); + TextureRange *= 1.0f + Math.Max( + Math.Abs(OverrideLightTexture.RelativeOrigin.X - 0.5f), + Math.Abs(OverrideLightTexture.RelativeOrigin.Y - 0.5f)); } } } @@ -238,7 +238,11 @@ namespace Barotrauma.Lights private bool needsRecalculationWhenUpToDate; public bool NeedsRecalculation { - get { return needsRecalculation; } + get + { + if (ParentBody?.UserData is Item it && it.Prefab.Identifier == "flashlight") { return true; } + return needsRecalculation; + } set { if (!needsRecalculation && value) @@ -708,6 +712,7 @@ namespace Barotrauma.Lights { foreach (ConvexHull hull in chList.List) { + if (hull.IsInvalid) { continue; } if (!chList.IsHidden.Contains(hull)) { //find convexhull segments that are close enough and facing towards the light source @@ -735,6 +740,7 @@ namespace Barotrauma.Lights GameMain.LightManager.AddRayCastTask(this, drawPos, rotation); } + const float MinPointDistance = 6; public void RayCastTask(Vector2 drawPos, float rotation) { @@ -877,12 +883,11 @@ namespace Barotrauma.Lights } } - const float MinPointDistance = 6; - //remove points that are very close to each other - for (int i = 0; i < points.Count; i++) + //+= 2 because the points are added in pairs above, i.e. 0 and 1 belong to the same segment + for (int i = 0; i < points.Count; i += 2) { - for (int j = Math.Min(i + 4, points.Count - 1); j > i; j--) + for (int j = Math.Min(i + 2, points.Count - 1); j > i; j--) { if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < MinPointDistance && Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < MinPointDistance) @@ -892,14 +897,14 @@ namespace Barotrauma.Lights } } - var compareCCW = new CompareSegmentPointCW(drawPos); try { - points.Sort(compareCCW); + var compareCW = new CompareSegmentPointCW(drawPos); + points.Sort(compareCW); } catch (Exception e) { - StringBuilder sb = new StringBuilder("Constructing light volumes failed! Light pos: " + drawPos + ", Hull verts:\n"); + StringBuilder sb = new StringBuilder($"Constructing light volumes failed ({nameof(CompareSegmentPointCW)})! Light pos: {drawPos}, Hull verts:\n"); foreach (SegmentPoint sp in points) { sb.AppendLine(sp.Pos.ToString()); @@ -914,7 +919,11 @@ namespace Barotrauma.Lights verts.Clear(); foreach (SegmentPoint p in points) { - Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); + Vector2 diff = p.WorldPos - drawPos; + float dist = diff.Length(); + //light source exactly at the segment point, don't cast a shadow (normalizing the vector would lead to NaN) + if (dist <= 0.0001f) { continue; } + Vector2 dir = diff / dist; Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * MinPointDistance; //do two slightly offset raycasts to hit the segment itself and whatever's behind it @@ -940,9 +949,36 @@ namespace Barotrauma.Lights { //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); - markAsVisible = true; + if (isPoint1) + { + TryAddPoints(intersection2.pos, p.WorldPos, drawPos, verts); + markAsVisible = true; + } + else if (isPoint2) + { + TryAddPoints(intersection1.pos, p.WorldPos, drawPos, verts); + markAsVisible = true; + } + else + { + //didn't hit either point, completely obstructed + verts.Add(intersection1.pos); + verts.Add(intersection2.pos); + } + static void TryAddPoints(Vector2 intersection, Vector2 point, Vector2 refPos, List verts) + { + //* 0.8f because we don't care about obstacles that are very close (intersecting walls), + //only about obstacles that are clearly between the point and the refPos + bool intersectionCloserThanPoint = Vector2.DistanceSquared(intersection, refPos) < Vector2.DistanceSquared(point, refPos) * 0.8f; + //if the raycast hit a segment that's closer than the point we're aiming towards, + //it means we didn't hit a segment behind the point, but something that's obstructing it + //= we don't want to add vertex at that obstructed point, it could make the light go through obstacles + if (!intersectionCloserThanPoint) + { + verts.Add(point); + } + verts.Add(intersection); + } } if (markAsVisible) { @@ -959,15 +995,32 @@ namespace Barotrauma.Lights //remove points that are very close to each other for (int i = 0; i < verts.Count - 1; i++) { - for (int j = Math.Min(i + 4, verts.Count - 1); j > i; j--) + for (int j = verts.Count - 1; j > i; j--) { - if (Math.Abs(verts[i].X - verts[j].X) < 6 && - Math.Abs(verts[i].Y - verts[j].Y) < 6) + if (Math.Abs(verts[i].X - verts[j].X) < MinPointDistance && + Math.Abs(verts[i].Y - verts[j].Y) < MinPointDistance) { verts.RemoveAt(j); } } } + + try + { + var compareCW = new CompareCW(drawPos); + verts.Sort(compareCW); + } + catch (Exception e) + { + StringBuilder sb = new StringBuilder($"Constructing light volumes failed ({nameof(CompareSegmentPointCW)})! Light pos: {drawPos}, verts:\n"); + foreach (Vector2 v in verts) + { + sb.AppendLine(v.ToString()); + } + DebugConsole.ThrowError(sb.ToString(), e); + } + + calculatedDrawPos = drawPos; state = LightVertexState.PendingVertexRecalculation; } @@ -1114,7 +1167,7 @@ namespace Barotrauma.Lights //add the normals together and use some magic numbers to create //a somewhat useful/good-looking blur - float blurDistance = 40.0f; + float blurDistance = 25.0f; Vector2 nDiff = nDiff1 * blurDistance; if (MathUtils.GetLineIntersection(vertex + (nDiff1 * blurDistance), nextVertex + (nDiff1 * blurDistance), vertex + (nDiff2 * blurDistance), prevVertex + (nDiff2 * blurDistance), true, out Vector2 intersection)) { @@ -1230,7 +1283,8 @@ namespace Barotrauma.Lights /// public void DrawSprite(SpriteBatch spriteBatch, Camera cam) { - if (GameMain.DebugDraw) + //uncomment if you want to visualize the bounds of the light volume + /*if (GameMain.DebugDraw) { Vector2 drawPos = position; if (ParentSub != null) @@ -1269,7 +1323,7 @@ namespace Barotrauma.Lights { GUI.DrawLine(spriteBatch, boundaryCorners[i].Pos, boundaryCorners[(i + 1) % 4].Pos, Color.White, 0, 3); } - } + }*/ if (DeformableLightSprite != null) { @@ -1318,7 +1372,7 @@ namespace Barotrauma.Lights if (LightTextureTargetSize != Vector2.Zero) { - LightSprite.DrawTiled(spriteBatch, drawPos, LightTextureTargetSize, color, startOffset: LightTextureOffset, textureScale: LightTextureScale); + LightSprite.DrawTiled(spriteBatch, drawPos, LightTextureTargetSize, color: color, startOffset: LightTextureOffset, textureScale: LightTextureScale); } else { @@ -1367,6 +1421,38 @@ namespace Barotrauma.Lights } } + public void DebugDrawVertices(SpriteBatch spriteBatch) + { + if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; } + + //commented out because this is mostly just useful in very specific situations, otherwise it just makes debugdraw very messy + //(you may also need to add a condition here that only draws this for the specific light you're interested in) + if (GameMain.DebugDraw && vertices != null) + { + if (ParentBody?.UserData is Item it && it.Prefab.Identifier == "flashlight") + + for (int i = 1; i < vertices.Length - 1; i += 2) + { + Vector2 vert1 = new Vector2(vertices[i].Position.X, vertices[i].Position.Y); + int nextIndex = (i + 2) % vertices.Length; + //the first vertex is the one at the position of the light source, skip that one + //(we just want to draw lines between the vertices at the circumference of the light volume) + if (nextIndex == 0) { nextIndex++; } + Vector2 vert2 = new Vector2(vertices[nextIndex].Position.X, vertices[nextIndex].Position.Y); + if (ParentSub != null) + { + vert1 += ParentSub.DrawPosition; + vert2 += ParentSub.DrawPosition; + } + vert1.Y = -vert1.Y; + vert2.Y = -vert2.Y; + + var randomColor = ToolBox.GradientLerp(i / (float)vertices.Length, Color.Magenta, Color.Blue, Color.Yellow, Color.Green, Color.Cyan, Color.Red, Color.Purple, Color.Yellow); + GUI.DrawLine(spriteBatch, vert1, vert2, randomColor * 0.8f, width: 2); + } + } + } + public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform, bool allowRecalculation, ref int recalculationCount) { if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 783c6d66a..3b847fece 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -291,14 +291,14 @@ namespace Barotrauma private readonly List mapNotifications = new List(); - partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change) + partial void ChangeLocationTypeProjSpecific(Location location, LocalizedString prevName, LocationTypeChange change) { 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‖"); + .Replace("[name]", $"‖color:gui.yellow‖{location.DisplayName}‖end‖"); location.LastTypeChangeMessage = msg; mapNotifications.Add(new MapNotification(msg, GUIStyle.SubHeadingFont, mapNotifications, location)); @@ -377,7 +377,7 @@ namespace Barotrauma 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 }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.DisplayName, 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 }; @@ -1080,6 +1080,7 @@ namespace Barotrauma } float dist = Vector2.Distance(start, end); var connectionSprite = connection.Passed ? generationParams.PassedConnectionSprite : generationParams.ConnectionSprite; + if (connectionSprite?.Texture == null) { continue; } Color segmentColor = connectionColor; int segmentWidth = width; @@ -1092,9 +1093,6 @@ namespace Barotrauma segmentWidth /= 2; segmentColor = connection.Passed ? generationParams.ConnectionColor : generationParams.UnvisitedConnectionColor; } - else - { - } } spriteBatch.Draw(connectionSprite.Texture, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs index 2ed19962f..5f8fdb8ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs @@ -29,7 +29,7 @@ namespace Barotrauma Vector2 spriteScale = new Vector2(zoom); - uiSprite.Sprite.DrawTiled(spriteBatch, topLeft, size, Params.RadiationAreaColor, Vector2.Zero, textureScale: spriteScale); + uiSprite.Sprite.DrawTiled(spriteBatch, topLeft, size, color: Params.RadiationAreaColor, startOffset: Vector2.Zero, textureScale: spriteScale); Vector2 topRight = topLeft + Vector2.UnitX * size.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 7fefcda46..4e97d4929 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Barotrauma.Lights; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -65,9 +66,7 @@ namespace Barotrauma disableSelect = value; if (disableSelect) { - startMovingPos = Vector2.Zero; - selectionSize = Vector2.Zero; - selectionPos = Vector2.Zero; + StopSelection(); } } } @@ -163,21 +162,9 @@ namespace Barotrauma { if (SelectedAny) { - if (SelectedList.Any(static t => t is Item it && it.GetComponent() is not null)) - { - GUI.AskForConfirmation(SubEditorScreen.CircuitBoxDeletionWarningHeader, SubEditorScreen.CircuitBoxDeletionWarningBody, onConfirm: Delete); - } - else - { - Delete(); - } - - void Delete() - { - SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(SelectedList), true)); - SelectedList.ForEach(static e => { if (!e.Removed) { e.Remove(); } }); - SelectedList.Clear(); - } + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(SelectedList), true)); + SelectedList.ForEachMod(static e => { if (!e.Removed) { e.Remove(); } }); + SelectedList.Clear(); } } @@ -494,6 +481,13 @@ namespace Barotrauma } } + public static void StopSelection() + { + startMovingPos = Vector2.Zero; + selectionSize = Vector2.Zero; + selectionPos = Vector2.Zero; + } + public static Vector2 GetNudgeAmount(bool doHold = true) { Vector2 nudgeAmount = Vector2.Zero; @@ -792,12 +786,16 @@ namespace Barotrauma foreach (MapEntity e in SelectedList) { SpriteEffects spriteEffects = SpriteEffects.None; + float spriteRotation = 0.0f; + float rectangleRotation = 0.0f; switch (e) { case Item item: { if (item.FlippedX && item.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; } - if (item.flippedY && item.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + if (item.FlippedY && item.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + spriteRotation = MathHelper.ToRadians(item.Rotation); + rectangleRotation = spriteRotation; var wire = item.GetComponent(); if (wire != null && wire.Item.body != null && !wire.Item.body.Enabled) { @@ -809,7 +807,10 @@ namespace Barotrauma case Structure structure: { if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; } - if (structure.flippedY && structure.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + if (structure.FlippedY && structure.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } + spriteRotation = MathHelper.ToRadians(structure.Rotation); + rectangleRotation = spriteRotation; + if (structure.FlippedX != structure.FlippedY) { rectangleRotation = -rectangleRotation; } break; } case WayPoint wayPoint: @@ -831,11 +832,12 @@ namespace Barotrauma } } e.Prefab?.DrawPlacing(spriteBatch, - new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteEffects); + new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteRotation, spriteEffects: spriteEffects); GUI.DrawRectangle(spriteBatch, - new Vector2(e.WorldRect.X, -e.WorldRect.Y) + moveAmount, - new Vector2(e.rect.Width, e.rect.Height), - Color.White, false, 0, (int)Math.Max(3.0f / GameScreen.Selected.Cam.Zoom, 2.0f)); + center: e.WorldRect.Center.ToVector2().FlipY() + moveAmount + new Vector2(0f, e.WorldRect.Height), + width: e.WorldRect.Width, height: e.WorldRect.Height, + rotation: rectangleRotation, clr: Color.White, + depth: 0f, thickness: Math.Max(3.0f / GameScreen.Selected.Cam.Zoom, 2.0f)); } //stop dragging the "selection rectangle" @@ -877,6 +879,23 @@ namespace Barotrauma spriteBatch.DrawLine(corners[3] + offset, corners[0] - offset, color, thickness); } + protected static void ColorFlipButton(GUIButton btn, bool flip) + { + var color = flip ? GUIStyle.Green : Color.White; + var hsv = ToolBox.RGBToHSV(color); + + // Boost saturation and reduce value a bit because our default colors are too muted for this button's style + var hsvBase = hsv; + hsvBase.Y *= 4f; + hsvBase.Z *= 0.8f; + btn.Color = ToolBox.HSVToRGB(hsvBase.X, hsvBase.Y, hsvBase.Z); + btn.SelectedColor = ToolBox.HSVToRGB(hsvBase.X, hsvBase.Y, hsvBase.Z); + + var hsvHover = hsv; + hsvHover.Z *= 1.2f; + btn.HoverColor = ToolBox.HSVToRGB(hsvHover.X, hsvHover.Y, hsvHover.Z); + } + public static List FilteredSelectedList { get; private set; } = new List(); public static void UpdateEditor(Camera cam, float deltaTime) @@ -1105,6 +1124,25 @@ namespace Barotrauma public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { } + private float RotationRad + => MathHelper.ToRadians( + this switch + { + Structure s => s.Rotation, + Item it => it.Rotation, + _ => 0.0f + }); + + private Vector2 GetEditingHandlePos(int x, int y, Camera cam) + { + Vector2 handleDiff = new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f)); + var rotation = -RotationRad; + handleDiff = MathUtils.RotatePoint(handleDiff, rotation); + if (FlippedX) { handleDiff = handleDiff.FlipX(); } + if (FlippedY) { handleDiff = handleDiff.FlipY(); } + return cam.WorldToScreen(Position + handleDiff); + } + float ResizeHandleSize => 10 * GUI.Scale; float ResizeHandleHighlightDistance => 8 * GUI.Scale; @@ -1119,9 +1157,10 @@ namespace Barotrauma { for (int y = startY; y < 2; y += 2) { - Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f))); + Vector2 handlePos = GetEditingHandlePos(x, y, cam); bool highlighted = Vector2.DistanceSquared(PlayerInput.MousePosition, handlePos) < ResizeHandleHighlightDistance * ResizeHandleHighlightDistance; + if (highlighted && PlayerInput.PrimaryMouseButtonDown()) { selectionPos = Vector2.Zero; @@ -1138,44 +1177,83 @@ namespace Barotrauma { if (prevRect == null) { - prevRect = new Rectangle(Rect.Location, Rect.Size); + prevRect = Rect; } - Vector2 placePosition = new Vector2(rect.X, rect.Y); - Vector2 placeSize = new Vector2(rect.Width, rect.Height); + Vector2 placePosition = prevRect.Value.Location.ToVector2(); + Vector2 placeSize = prevRect.Value.Size.ToVector2(); - Vector2 mousePos = Submarine.MouseToWorldGrid(cam, Submarine.MainSub, round: true); - - if (PlayerInput.IsShiftDown()) + static Vector2 flipThenRotate(Vector2 point, Vector2 center, float angle, bool flipX, bool flipY) { - mousePos = cam.ScreenToWorld(PlayerInput.MousePosition); + if (flipX) { point = (point - center).FlipX() + center; } + if (flipY) { point = (point - center).FlipY() + center; } + + point = MathUtils.RotatePointAroundTarget(point, center, angle); + + return point; + } + + static Vector2 rotateThenFlip(Vector2 point, Vector2 center, float angle, bool flipX, bool flipY) + { + point = MathUtils.RotatePointAroundTarget(point, center, angle); + + if (flipX) { point = (point - center).FlipX() + center; } + if (flipY) { point = (point - center).FlipY() + center; } + + return point; + } + + Vector2 mousePos = cam.ScreenToWorld(PlayerInput.MousePosition); + Vector2 prevPos = placePosition; + Vector2 prevOppositeCorner = prevPos + placeSize.FlipY(); + Vector2 prevCenter = placePosition + placeSize.FlipY() * 0.5f; + mousePos = flipThenRotate(mousePos, prevCenter, RotationRad, FlippedX, FlippedY); + + if (!PlayerInput.IsShiftDown()) + { + mousePos = Submarine.VectorToWorldGrid(mousePos, Submarine.MainSub, round: true); } if (resizeDirX > 0) { - mousePos.X = Math.Max(mousePos.X, rect.X + Submarine.GridSize.X); + mousePos.X = Math.Max(mousePos.X, prevRect.Value.X + Submarine.GridSize.X); placeSize.X = mousePos.X - placePosition.X; } else if (resizeDirX < 0) { - mousePos.X = Math.Min(mousePos.X, rect.Right - Submarine.GridSize.X); + mousePos.X = Math.Min(mousePos.X, prevRect.Value.Right - Submarine.GridSize.X); placeSize.X = MathF.Round((placePosition.X + placeSize.X) - mousePos.X); placePosition.X = MathF.Round(mousePos.X); } if (resizeDirY < 0) { - mousePos.Y = Math.Min(mousePos.Y, rect.Y - Submarine.GridSize.Y); + mousePos.Y = Math.Min(mousePos.Y, prevRect.Value.Y - Submarine.GridSize.Y); placeSize.Y = placePosition.Y - mousePos.Y; } else if (resizeDirY > 0) { - mousePos.Y = Math.Max(mousePos.Y, rect.Y - rect.Height + Submarine.GridSize.X); + mousePos.Y = Math.Max(mousePos.Y, prevRect.Value.Y - prevRect.Value.Height + Submarine.GridSize.Y); - placeSize.Y = mousePos.Y - (rect.Y - rect.Height); + placeSize.Y = mousePos.Y - (prevRect.Value.Y - prevRect.Value.Height); placePosition.Y = mousePos.Y; } + Vector2 newPos = placePosition; + Vector2 newOppositeCorner = placePosition + placeSize.FlipY(); + + Vector2 transformedCornerDiff = rotateThenFlip(newPos-prevPos, Vector2.Zero, -RotationRad, FlippedX, FlippedY); + Vector2 transformedOppositeCornerDiff = rotateThenFlip(newOppositeCorner-prevOppositeCorner, Vector2.Zero, -RotationRad, FlippedX, FlippedY); + + Vector2 newPosTransformed = rotateThenFlip(prevPos, prevCenter, -RotationRad, FlippedX, FlippedY) + + transformedCornerDiff; + Vector2 newOppositeTransformed = rotateThenFlip(prevOppositeCorner, prevCenter, -RotationRad, FlippedX, FlippedY) + + transformedOppositeCornerDiff; + Vector2 newTransformedCenter = (newPosTransformed + newOppositeTransformed) * 0.5f; + + var newDiff = (newOppositeCorner - newPos) * 0.5f; + placePosition = newTransformedCenter - newDiff; + if ((int)placePosition.X != rect.X || (int)placePosition.Y != rect.Y || (int)placeSize.X != rect.Width || (int)placeSize.Y != rect.Height) { Rect = new Rectangle((int)placePosition.X, (int)placePosition.Y, (int)placeSize.X, (int)placeSize.Y); @@ -1210,15 +1288,16 @@ namespace Barotrauma IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; - int StartY = ResizeVertical ? -1 : 0; + int startY = ResizeVertical ? -1 : 0; for (int x = startX; x < 2; x += 2) { - for (int y = StartY; y < 2; y += 2) + for (int y = startY; y < 2; y += 2) { - Vector2 handlePos = cam.WorldToScreen(Position + new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f))); + Vector2 handlePos = GetEditingHandlePos(x, y, cam); bool highlighted = Vector2.DistanceSquared(PlayerInput.MousePosition, handlePos) < ResizeHandleHighlightDistance * ResizeHandleHighlightDistance; + var color = Color.White * (highlighted ? 1.0f : 0.6f); if (highlighted && !PlayerInput.PrimaryMouseButtonHeld()) { GUI.MouseCursor = CursorState.Hand; @@ -1226,7 +1305,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, handlePos - new Vector2(ResizeHandleSize / 2), new Vector2(ResizeHandleSize), - Color.White * (highlighted ? 1.0f : 0.6f), + color, true, 0, (int)Math.Max(1.5f / GameScreen.Selected.Cam.Zoom, 1.0f)); } @@ -1241,12 +1320,15 @@ namespace Barotrauma HashSet foundEntities = new HashSet(); Rectangle selectionRect = Submarine.AbsRect(pos, size); + Quad2D selectionQuad = Quad2D.FromSubmarineRectangle(selectionRect); foreach (MapEntity entity in MapEntityList) { if (!entity.SelectableInEditor) { continue; } - if (Submarine.RectsOverlap(selectionRect, entity.rect)) + Quad2D entityQuad = entity.GetTransformedQuad(); + + if (selectionQuad.Intersects(entityQuad)) { foundEntities.Add(entity); entity.IsIncludedInSelection = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs index d70dc4087..5007fbecc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs @@ -84,7 +84,7 @@ namespace Barotrauma } } - public virtual void DrawPlacing(SpriteBatch spriteBatch, Rectangle drawRect, float scale = 1.0f, SpriteEffects spriteEffects = SpriteEffects.None) + public virtual void DrawPlacing(SpriteBatch spriteBatch, Rectangle drawRect, float scale = 1.0f, float rotation = 0.0f, SpriteEffects spriteEffects = SpriteEffects.None) { if (Submarine.MainSub != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs index 18e2bef75..1b3c99d28 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -1,10 +1,9 @@ #nullable enable +using Barotrauma.Sounds; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; -using System.Xml.Linq; -using Barotrauma.Sounds; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -45,7 +44,8 @@ namespace Barotrauma } if (FrequencyMultiplierRange.Y > 4.0f) { - DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")"); + DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")", + contentPackage: element.ContentPackage); } IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); } @@ -65,7 +65,8 @@ namespace Barotrauma if (filename is null) { string errorMsg = "Error when loading round sound (" + element + ") - file path not set"; - DebugConsole.ThrowError(errorMsg); + DebugConsole.ThrowError(errorMsg, + contentPackage: element.ContentPackage); GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FilePathEmpty" + element.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return null; } @@ -86,7 +87,8 @@ namespace Barotrauma catch (System.IO.FileNotFoundException e) { string errorMsg = "Failed to load sound file \"" + filename + "\" (file not found)."; - DebugConsole.ThrowError(errorMsg, e); + DebugConsole.ThrowError(errorMsg, e, + contentPackage: element.ContentPackage); if (!ContentPackageManager.ModsEnabled) { GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); @@ -96,7 +98,8 @@ namespace Barotrauma catch (System.IO.InvalidDataException e) { string errorMsg = "Failed to load sound file \"" + filename + "\" (invalid data)."; - DebugConsole.ThrowError(errorMsg, e); + DebugConsole.ThrowError(errorMsg, e, + contentPackage: element.ContentPackage); GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:InvalidData" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return null; } @@ -123,7 +126,8 @@ namespace Barotrauma catch (System.IO.FileNotFoundException e) { string errorMsg = "Failed to load sound file \"" + roundSound.Filename + "\"."; - DebugConsole.ThrowError(errorMsg, e); + DebugConsole.ThrowError(errorMsg, e, + contentPackage: roundSound.Sound?.XElement?.ContentPackage); GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + roundSound.Filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 3c28cbe17..4fe5ce8e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -150,7 +150,8 @@ namespace Barotrauma Stretch = true, RelativeSpacing = 0.01f }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") + + var mirrorX = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityXToolTip"), OnClicked = (button, data) => @@ -160,10 +161,12 @@ namespace Barotrauma me.FlipX(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); } + ColorFlipButton(button, FlippedX); return true; } }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall") + ColorFlipButton(mirrorX, FlippedX); + var mirrorY = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityYToolTip"), OnClicked = (button, data) => @@ -173,9 +176,11 @@ namespace Barotrauma me.FlipY(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } + ColorFlipButton(button, FlippedY); return true; } }; + ColorFlipButton(mirrorY, FlippedY); new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite"), style: "GUIButtonSmall") { OnClicked = (button, data) => @@ -231,11 +236,14 @@ namespace Barotrauma public override bool IsVisible(Rectangle worldView) { - Rectangle worldRect = WorldRect; + RectangleF worldRect = Quad2D.FromSubmarineRectangle(WorldRect).Rotated( + FlippedX != FlippedY + ? rotationRad + : -rotationRad).BoundingAxisAlignedRectangle; Vector2 worldPos = WorldPosition; - Vector2 min = new Vector2(worldRect.X, worldRect.Y - worldRect.Height); - Vector2 max = new Vector2(worldRect.Right, worldRect.Y); + Vector2 min = new Vector2(worldRect.X, worldRect.Y); + Vector2 max = new Vector2(worldRect.Right, worldRect.Y + worldRect.Height); foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) { float scale = decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale; @@ -307,7 +315,12 @@ namespace Barotrauma Vector2 bodyPos = WorldPosition + BodyOffset * Scale; - GUI.DrawRectangle(spriteBatch, new Vector2(bodyPos.X, -bodyPos.Y), rectSize.X, rectSize.Y, BodyRotation, Color.White, + GUI.DrawRectangle(sb: spriteBatch, + center: new Vector2(bodyPos.X, -bodyPos.Y), + width: rectSize.X, + height: rectSize.Y, + rotation: BodyRotation, + clr: Color.White, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); } @@ -357,8 +370,10 @@ namespace Barotrauma Prefab.BackgroundSprite.DrawTiled( spriteBatch, - new Vector2(rect.X + drawOffset.X, -(rect.Y + drawOffset.Y)), + new Vector2(rect.X + rect.Width / 2 + drawOffset.X, -(rect.Y - rect.Height / 2 + drawOffset.Y)), new Vector2(rect.Width, rect.Height), + rotation: rotationRad, + origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: Prefab.BackgroundSpriteColor, textureScale: TextureScale * Scale, startOffset: backGroundOffset, @@ -368,8 +383,10 @@ namespace Barotrauma { Prefab.BackgroundSprite.DrawTiled( spriteBatch, - new Vector2(rect.X + drawOffset.X, -(rect.Y + drawOffset.Y)) + dropShadowOffset, + new Vector2(rect.X + rect.Width / 2 + drawOffset.X, -(rect.Y - rect.Height / 2 + drawOffset.Y)) + dropShadowOffset, new Vector2(rect.Width, rect.Height), + rotation: rotationRad, + origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: Color.Black * 0.5f, textureScale: TextureScale * Scale, startOffset: backGroundOffset, @@ -385,6 +402,13 @@ namespace Barotrauma SpriteEffects oldEffects = Prefab.Sprite.effects; Prefab.Sprite.effects ^= SpriteEffects; + Vector2 advanceX = MathUtils.RotatedUnitXRadians(this.rotationRad).FlipY(); + Vector2 advanceY = advanceX.YX().FlipX(); + if (FlippedX != FlippedY) + { + advanceX = advanceX.FlipY(); + advanceY = advanceY.FlipX(); + } for (int i = 0; i < Sections.Length; i++) { Rectangle drawSection = Sections[i].rect; @@ -409,7 +433,7 @@ namespace Barotrauma drawSection = new Rectangle( drawSection.X, drawSection.Y, - Sections[Sections.Length -1 ].rect.Right - drawSection.X, + Sections[Sections.Length - 1].rect.Right - drawSection.X, drawSection.Y - (Sections[Sections.Length - 1].rect.Y - Sections[Sections.Length - 1].rect.Height)); i = Sections.Length; } @@ -424,10 +448,18 @@ namespace Barotrauma sectionOffset.X += MathUtils.PositiveModulo((int)-textureOffset.X, Prefab.Sprite.SourceRect.Width); sectionOffset.Y += MathUtils.PositiveModulo((int)-textureOffset.Y, Prefab.Sprite.SourceRect.Height); + Vector2 pos = new Vector2(drawSection.X, drawSection.Y); + pos -= rect.Location.ToVector2(); + pos = advanceX * pos.X + advanceY * pos.Y; + pos += rect.Location.ToVector2(); + pos = new Vector2(pos.X + rect.Width / 2 + drawOffset.X, -(pos.Y - rect.Height / 2 + drawOffset.Y)); + Prefab.Sprite.DrawTiled( spriteBatch, - new Vector2(drawSection.X + drawOffset.X, -(drawSection.Y + drawOffset.Y)), + pos, new Vector2(drawSection.Width, drawSection.Height), + rotation: rotationRad, + origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: color, startOffset: sectionOffset, depth: depth, @@ -437,7 +469,7 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor) + this.rotationRad; Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, Prefab.Sprite.effects, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index f031e7fab..587e21dab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -94,19 +94,20 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle(newRect.X, -newRect.Y - GameMain.GraphicsHeight, newRect.Width, newRect.Height + GameMain.GraphicsHeight * 2), Color.White); } - public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, SpriteEffects spriteEffects = SpriteEffects.None) + public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, float rotation = 0.0f, SpriteEffects spriteEffects = SpriteEffects.None) { - SpriteEffects oldEffects = Sprite.effects; - Sprite.effects ^= spriteEffects; - + var position = placeRect.Location.ToVector2().FlipY(); + position += placeRect.Size.ToVector2() * 0.5f; + Sprite.DrawTiled( spriteBatch, - new Vector2(placeRect.X, -placeRect.Y), - new Vector2(placeRect.Width, placeRect.Height), - color: Color.White * 0.8f, - textureScale: TextureScale * scale); - - Sprite.effects = oldEffects; + position, + placeRect.Size.ToVector2(), + color: Color.White * 0.8f, + origin: placeRect.Size.ToVector2() * 0.5f, + rotation: rotation, + textureScale: TextureScale * scale, + spriteEffects: spriteEffects ^ Sprite.effects); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 6d330e43e..3d9e53dc6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -25,7 +25,7 @@ namespace Barotrauma /// /// Margin applied around the view area when culling entities (i.e. entities that are this far outside the view are still considered visible) /// - private const int CullMargin = 500; + private const int CullMargin = 50; /// /// Update entity culling when any corner of the view has moved more than this /// @@ -713,18 +713,12 @@ namespace Barotrauma return GameMain.LightManager.Lights.Count(l => l.CastShadows && !l.IsBackground) - disabledItemLightCount; } - public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub, bool round = false) + public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub, Vector2? mousePos = null, bool round = false) { - Vector2 position = PlayerInput.MousePosition; + Vector2 position = mousePos ?? PlayerInput.MousePosition; position = cam.ScreenToWorld(position); - Vector2 worldGridPos = VectorToWorldGrid(position, round); - - if (sub != null) - { - worldGridPos.X += sub.Position.X % GridSize.X; - worldGridPos.Y += sub.Position.Y % GridSize.Y; - } + Vector2 worldGridPos = VectorToWorldGrid(position, sub, round); return worldGridPos; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index e056cc997..24c77837e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -421,7 +421,7 @@ namespace Barotrauma float scale = element.GetAttributeFloat("scale", 1f); Color color = element.GetAttributeColor("spritecolor", Color.White); - float rotation = element.GetAttributeFloat("rotation", 0f); + float rotationRad = MathHelper.ToRadians(element.GetAttributeFloat("rotation", 0f)); MapEntityPrefab prefab; if (element.NameAsIdentifier() == "item" @@ -455,7 +455,7 @@ namespace Barotrauma ItemPrefab itemPrefab = prefab as ItemPrefab; if (itemPrefab != null) { - BakeItemComponents(itemPrefab, rect, color, scale, rotation, depth, out overrideSprite); + BakeItemComponents(itemPrefab, rect, color, scale, rotationRad, depth, out overrideSprite); } if (!overrideSprite) @@ -485,13 +485,15 @@ namespace Barotrauma MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.Sprite.SourceRect.Height)); prefab.Sprite.DrawTiled( - spriteRecorder, - rect.Location.ToVector2() * new Vector2(1f, -1f), - rect.Size.ToVector2(), + spriteBatch: spriteRecorder, + position: new Vector2(rect.X + rect.Width / 2, -(rect.Y - rect.Height / 2)), + targetSize: rect.Size.ToVector2(), + origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: color, startOffset: backGroundOffset, textureScale: textureScale * scale, - depth: depth); + depth: depth, + rotation: rotationRad); } else if (itemPrefab != null) { @@ -552,7 +554,7 @@ namespace Barotrauma spritePos * new Vector2(1f, -1f), color, prefab.Sprite.Origin, - rotation, + rotationRad, scale, prefab.Sprite.effects, depth); @@ -564,7 +566,7 @@ namespace Barotrauma if (flippedX) { offset.X = -offset.X; } if (flippedY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, - MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, + rotationRad + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); } } @@ -577,7 +579,7 @@ namespace Barotrauma private void BakeItemComponents( ItemPrefab prefab, Rectangle rect, Color color, - float scale, float rotation, float depth, + float scale, float rotationRad, float depth, out bool overrideSprite) { overrideSprite = false; @@ -607,7 +609,7 @@ namespace Barotrauma Vector2 relativeBarrelPos = barrelPos * prefab.Scale - new Vector2(rect.Width / 2, rect.Height / 2); var transformedBarrelPos = MathUtils.RotatePoint( relativeBarrelPos, - MathHelper.ToRadians(rotation)); + rotationRad); Vector2 drawPos = new Vector2(rect.X + rect.Width * relativeScale / 2 + transformedBarrelPos.X * relativeScale, rect.Y - rect.Height * relativeScale / 2 - transformedBarrelPos.Y * relativeScale); drawPos.Y = -drawPos.Y; @@ -615,13 +617,13 @@ namespace Barotrauma railSprite?.Draw(spriteRecorder, drawPos, color, - rotation + MathHelper.PiOver2, scale, + rotationRad, scale, SpriteEffects.None, depth + (railSprite.Depth - prefab.Sprite.Depth)); barrelSprite?.Draw(spriteRecorder, drawPos, color, - rotation + MathHelper.PiOver2, scale, + rotationRad, scale, SpriteEffects.None, depth + (barrelSprite.Depth - prefab.Sprite.Depth)); break; @@ -781,7 +783,7 @@ namespace Barotrauma previewFrame = null; } spriteRecorder?.Dispose(); spriteRecorder = null; - camera?.Dispose(); camera = null; + camera = null; isDisposed = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 6a5bfb3ee..318176714 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -122,12 +122,12 @@ namespace Barotrauma.Networking VoipSound = null; } - public void SetPermissions(ClientPermissions permissions, IEnumerable permittedConsoleCommands) + public void SetPermissions(ClientPermissions permissions, IEnumerable permittedConsoleCommands) { List permittedCommands = new List(); - foreach (string commandName in permittedConsoleCommands) + foreach (Identifier commandName in permittedConsoleCommands) { - var consoleCommand = DebugConsole.Commands.Find(c => c.names.Contains(commandName)); + var consoleCommand = DebugConsole.Commands.Find(c => c.Names.Contains(commandName)); if (consoleCommand != null) { permittedCommands.Add(consoleCommand); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 36740fb62..c6d338147 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -65,7 +65,7 @@ namespace Barotrauma.Networking public bool LateCampaignJoin = false; private ClientPermissions permissions = ClientPermissions.None; - private List permittedConsoleCommands = new List(); + private List permittedConsoleCommands = new List(); private bool connected; @@ -170,9 +170,9 @@ namespace Barotrauma.Networking internal readonly struct PermissionChangedEvent { public readonly ClientPermissions NewPermissions; - public readonly ImmutableArray NewPermittedConsoleCommands; + public readonly ImmutableArray NewPermittedConsoleCommands; - public PermissionChangedEvent(ClientPermissions newPermissions, IReadOnlyList newPermittedConsoleCommands) + public PermissionChangedEvent(ClientPermissions newPermissions, IReadOnlyList newPermittedConsoleCommands) { NewPermissions = newPermissions; NewPermittedConsoleCommands = newPermittedConsoleCommands.ToImmutableArray(); @@ -1211,11 +1211,11 @@ namespace Barotrauma.Networking targetClient?.SetPermissions(permissions, permittedCommands); if (clientId == SessionId) { - SetMyPermissions(permissions, permittedCommands.Select(command => command.names[0])); + SetMyPermissions(permissions, permittedCommands.Select(command => command.Names[0])); } } - private void SetMyPermissions(ClientPermissions newPermissions, IEnumerable permittedConsoleCommands) + private void SetMyPermissions(ClientPermissions newPermissions, IEnumerable permittedConsoleCommands) { if (!(this.permittedConsoleCommands.Any(c => !permittedConsoleCommands.Contains(c)) || permittedConsoleCommands.Any(c => !this.permittedConsoleCommands.Contains(c)))) @@ -1227,7 +1227,7 @@ namespace Barotrauma.Networking permissions.HasFlag(ClientPermissions.ManageRound) != newPermissions.HasFlag(ClientPermissions.ManageRound); permissions = newPermissions; - this.permittedConsoleCommands = new List(permittedConsoleCommands); + this.permittedConsoleCommands = permittedConsoleCommands.ToList(); //don't show the "permissions changed" popup if the client owns the server if (!IsServerOwner) { @@ -1265,10 +1265,10 @@ namespace Barotrauma.Networking var commandsLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUIStyle.SubHeadingFont); var commandList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform)); - foreach (string permittedCommand in permittedConsoleCommands) + foreach (Identifier permittedCommand in permittedConsoleCommands) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), commandList.Content.RectTransform, minSize: new Point(0, 15)), - permittedCommand, font: GUIStyle.SmallFont) + permittedCommand.Value, font: GUIStyle.SmallFont) { CanBeFocused = false }; @@ -1348,6 +1348,7 @@ namespace Barotrauma.Networking bool respawnAllowed = inc.ReadBoolean(); ServerSettings.AllowDisguises = inc.ReadBoolean(); ServerSettings.AllowRewiring = inc.ReadBoolean(); + ServerSettings.AllowImmediateItemDelivery = inc.ReadBoolean(); ServerSettings.AllowFriendlyFire = inc.ReadBoolean(); ServerSettings.LockAllDefaultWires = inc.ReadBoolean(); ServerSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); @@ -2221,7 +2222,7 @@ namespace Barotrauma.Networking } outmsg.WriteByte((byte)MultiplayerPreferences.Instance.TeamPreference); - if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) + if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaign.LastSaveID == 0) { outmsg.WriteUInt16((UInt16)0); } @@ -2551,18 +2552,18 @@ namespace Barotrauma.Networking return permissions.HasFlag(permission); } - public bool HasConsoleCommandPermission(string commandName) + public bool HasConsoleCommandPermission(Identifier commandName) { if (!permissions.HasFlag(ClientPermissions.ConsoleCommands)) { return false; } - if (permittedConsoleCommands.Any(c => c.Equals(commandName, StringComparison.OrdinalIgnoreCase))) { return true; } + if (permittedConsoleCommands.Contains(commandName)) { return true; } //check aliases foreach (DebugConsole.Command command in DebugConsole.Commands) { - if (command.names.Contains(commandName)) + if (command.Names.Contains(commandName)) { - if (command.names.Intersect(permittedConsoleCommands).Any()) { return true; } + if (command.Names.Intersect(permittedConsoleCommands).Any()) { return true; } break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index f7c5d0a44..4ce4dd2e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -75,6 +75,9 @@ namespace Barotrauma.Networking [Serialize("", IsPropertySaveable.Yes)] public LanguageIdentifier Language { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public string SelectedSub { get; set; } = string.Empty; + public Version GameVersion { get; set; } = new Version(0, 0, 0, 0); public Option Ping = Option.None(); @@ -104,6 +107,8 @@ namespace Barotrauma.Networking public ImmutableArray ContentPackages; + public int ContentPackageCount; + public bool IsModded => ContentPackages.Any(p => !GameMain.VanillaContent.NameMatches(p.Name)); public ServerInfo(Endpoint endpoint) @@ -176,33 +181,6 @@ namespace Barotrauma.Networking }; title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f)); - bool isFavorite = serverListScreen.IsFavorite(this); - - static LocalizedString favoriteTickBoxToolTip(bool isFavorite) - => TextManager.Get(isFavorite ? "RemoveFromFavorites" : "AddToFavorites"); - - GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.8f), title.RectTransform, Anchor.CenterRight), - "", null, "GUIServerListFavoriteTickBox") - { - UserData = this, - Selected = isFavorite, - ToolTip = favoriteTickBoxToolTip(isFavorite), - OnSelected = tickbox => - { - ServerInfo info = (ServerInfo)tickbox.UserData; - if (tickbox.Selected) - { - GameMain.ServerListScreen.AddToFavoriteServers(info); - } - else - { - GameMain.ServerListScreen.RemoveFromFavoriteServers(info); - } - tickbox.ToolTip = favoriteTickBoxToolTip(tickbox.Selected); - return true; - } - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), GameVersion == new Version(0, 0, 0, 0) ? TextManager.Get("Unknown") : GameVersion.ToString())) @@ -258,6 +236,59 @@ namespace Barotrauma.Networking { Stretch = true }; + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.25f), playStyleBanner.RectTransform, Anchor.BottomRight), + isHorizontal: true, childAnchor: Anchor.BottomRight); + + //shadow behind the buttons + new GUIFrame(new RectTransform(new Vector2(3.15f, 1.05f), buttonContainer.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest), style: null) + { + Color = Color.Black * 0.7f, + IgnoreLayoutGroups = true + }; + + bool isFavorite = serverListScreen.IsFavorite(this); + static LocalizedString favoriteTickBoxToolTip(bool isFavorite) + => TextManager.Get(isFavorite ? "RemoveFromFavorites" : "AddToFavorites"); + + GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.Smallest), + "", null, "GUIServerListFavoriteTickBox") + { + UserData = this, + Selected = isFavorite, + ToolTip = favoriteTickBoxToolTip(isFavorite), + OnSelected = tickbox => + { + ServerInfo info = (ServerInfo)tickbox.UserData; + if (tickbox.Selected) + { + GameMain.ServerListScreen.AddToFavoriteServers(info); + } + else + { + GameMain.ServerListScreen.RemoveFromFavoriteServers(info); + } + tickbox.ToolTip = favoriteTickBoxToolTip(tickbox.Selected); + return true; + } + }; + + new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "GUIServerListReportServer") + { + ToolTip = TextManager.Get("reportserver"), + OnClicked = (_, _) => {ServerListScreen.CreateReportPrompt(this); return true; } + }; + + new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "GUIServerListHideServer") + { + ToolTip = TextManager.Get("filterserver"), + OnClicked = (_, _) => + { + ServerListScreen.CreateFilterServerPrompt(this); + return true; + } + }; + // playstyle tags ----------------------------------------------------------------------------- var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true) @@ -309,6 +340,14 @@ namespace Barotrauma.Networking TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), textAlignment: Alignment.Right); + if (!string.IsNullOrEmpty(SelectedSub)) + { + var submarineText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("Submarine")); + new GUITextBlock(new RectTransform(Vector2.One, submarineText.RectTransform), + SelectedSub, + textAlignment: Alignment.Right); + } + GUITextBlock playStyleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("serverplaystyle")); new GUITextBlock(new RectTransform(Vector2.One, playStyleText.RectTransform), TextManager.Get("servertag." + playStyle), textAlignment: Alignment.Right); @@ -385,6 +424,15 @@ namespace Barotrauma.Networking } } } + if (ContentPackageCount > ContentPackages.Length) + { + new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform) { MinSize = new Point(0, 15) }, + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (ContentPackageCount - ContentPackages.Length).ToString())) + { + CanBeFocused = false + }; + } } // ----------------------------------------------------------------------------- @@ -423,14 +471,16 @@ namespace Barotrauma.Networking AllowSpectating = getBool("allowspectating"); AllowRespawn = getBool("allowrespawn"); VoipEnabled = getBool("voicechatenabled"); - GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty; if (float.TryParse(valueGetter("traitors"), NumberStyles.Any, CultureInfo.InvariantCulture, out float traitorProbability)) { TraitorProbability = traitorProbability; } if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None; + SelectedSub = valueGetter("submarine") ?? string.Empty; ContentPackages = ExtractContentPackageInfo(ServerName, valueGetter).ToImmutableArray(); - + ContentPackageCount = ContentPackages.Length; + if (int.TryParse(valueGetter("packagecount"), out int packageCount)) { ContentPackageCount = packageCount; } + bool getBool(string key) { string? data = valueGetter(key); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 82f553984..fa077332e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -936,6 +936,10 @@ namespace Barotrauma.Networking TextManager.Get("ServerSettingsAllowVoteKick")); GetPropertyData(nameof(AllowVoteKick)).AssignGUIComponent(voteKickBox); + var allowImmediateItemDeliveryBox = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsImmediateItemDelivery")); + GetPropertyData(nameof(AllowImmediateItemDelivery)).AssignGUIComponent(allowImmediateItemDeliveryBox); + GUITextBlock.AutoScaleAndNormalize(tickBoxContainer.Content.Children.Select(c => ((GUITickBox)c).TextBlock)); tickBoxContainer.RectTransform.MinSize = new Point(0, (int)(tickBoxContainer.Content.Children.First().Rect.Height * 2.0f + tickBoxContainer.Padding.Y + tickBoxContainer.Padding.W)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 4bd6e7605..2eac72fa2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +#nullable enable +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Xml.Linq; @@ -148,7 +149,7 @@ namespace Barotrauma.Particles Prefab = prefab; } - public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, bool mirrorAngle = false, Tuple tracerPoints = null) + public void Emit(float deltaTime, Vector2 position, Hull? hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab? overrideParticle = null, bool mirrorAngle = false, Tuple? tracerPoints = null) { if (GameMain.Client?.MidRoundSyncing ?? false) { return; } @@ -191,16 +192,17 @@ namespace Barotrauma.Particles burstEmitTimer = Prefab.Properties.EmitInterval; for (int i = 0; i < Prefab.Properties.ParticleAmount * amountMultiplier; i++) { - Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, tracerPoints: tracerPoints); + Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, mirrorAngle, tracerPoints: tracerPoints); } } - private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, bool mirrorAngle = false, Tuple tracerPoints = null) + private void Emit(Vector2 position, Hull? hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab? overrideParticle = null, bool mirrorAngle = false, Tuple? tracerPoints = null) { var particlePrefab = overrideParticle ?? Prefab.ParticlePrefab; if (particlePrefab == null) { - DebugConsole.AddWarning($"Could not find the particle prefab \"{Prefab.ParticlePrefabName}\"."); + DebugConsole.AddWarning($"Could not find the particle prefab \"{Prefab.ParticlePrefabName}\".", + contentPackage: Prefab.ContentPackage); return; } @@ -271,7 +273,7 @@ namespace Barotrauma.Particles { public readonly Identifier ParticlePrefabName; - public ParticlePrefab ParticlePrefab + public ParticlePrefab? ParticlePrefab { get { @@ -282,12 +284,16 @@ namespace Barotrauma.Particles public readonly ParticleEmitterProperties Properties; - public bool DrawOnTop => Properties.DrawOnTop || ParticlePrefab.DrawOnTop; + public readonly ContentPackage? ContentPackage; + + public bool DrawOnTop => Properties.DrawOnTop || ParticlePrefab is { DrawOnTop: true }; public ParticleEmitterPrefab(ContentXElement element) { - Properties = new ParticleEmitterProperties(element); + if (element == null) { throw new ArgumentNullException(nameof(element)); } + Properties = new ParticleEmitterProperties(element!); ParticlePrefabName = element.GetAttributeIdentifier("particle", ""); + ContentPackage = element.ContentPackage; } public ParticleEmitterPrefab(ParticlePrefab prefab, ParticleEmitterProperties properties) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index c550fbfee..050ee9ded 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -244,7 +244,8 @@ namespace Barotrauma.Particles if (Sprites.Count == 0) { - DebugConsole.ThrowError($"Particle prefab \"{Name}\" in the file \"{file}\" has no sprites defined!"); + DebugConsole.ThrowError($"Particle prefab \"{Name}\" in the file \"{file}\" has no sprites defined!", + contentPackage: element.ContentPackage); } //if velocity change in water is not given, it defaults to the normal velocity change diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 4dd29adfa..b541794b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -66,15 +66,22 @@ namespace Barotrauma private static void CrashHandler(object sender, UnhandledExceptionEventArgs args) { + Exception unhandledException = args.ExceptionObject as Exception; try { Game?.Exit(); - CrashDump(Game, "crashreport.log", (Exception)args.ExceptionObject); + CrashDump(Game, "crashreport.log", unhandledException); Game?.Dispose(); } - catch (Exception e) + catch (Exception exceptionHandlerError) { - Debug.WriteLine(e.Message); + Debug.WriteLine(exceptionHandlerError.Message); + string slimCrashReport = "Exception handler failed: " + exceptionHandlerError.Message + "\n" + exceptionHandlerError.StackTrace; + if (unhandledException != null) + { + slimCrashReport += "\n\nInitial exception: " + unhandledException.Message + "\n" + unhandledException.StackTrace; + } + File.WriteAllText("crashreportslim.log", slimCrashReport); //exception handler is broken, we have a serious problem here!! return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 1cefb2a67..67be291c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -40,7 +40,8 @@ namespace Barotrauma public CampaignMode Campaign { get; } public CrewManagement CrewManagement { get; set; } - private Store Store { get; set; } + + public Store Store { get; private set; } public UpgradeStore UpgradeStore { get; set; } @@ -254,7 +255,7 @@ namespace Barotrauma RelativeSpacing = 0.02f, }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUIStyle.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.DisplayName, font: GUIStyle.LargeFont) { AutoScaleHorizontal = true }; @@ -598,9 +599,10 @@ namespace Barotrauma break; case CampaignMode.InteractionType.Crew: CrewManagement.UpdateCrew(); + CrewManagement.UpdateHireables(); break; case CampaignMode.InteractionType.PurchaseSub: - if (submarineSelection == null) submarineSelection = new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); + submarineSelection ??= new SubmarineSelection(false, () => Campaign.ShowCampaignUI = false, tabs[(int)CampaignMode.InteractionType.PurchaseSub].RectTransform); submarineSelection.RefreshSubmarineDisplay(true, setTransferOptionToTrue: true); break; case CampaignMode.InteractionType.Map: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 917bf10f6..bc85fcb99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -258,8 +258,8 @@ namespace Barotrauma graphics.BlendState = BlendState.NonPremultiplied; graphics.SamplerStates[0] = SamplerState.LinearWrap; - Quad.UseBasicEffect(renderTargetBackground); - Quad.Render(); + GraphicsQuad.UseBasicEffect(renderTargetBackground); + GraphicsQuad.Render(); //Draw the rest of the structures, characters and front structures spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); @@ -312,8 +312,8 @@ namespace Barotrauma graphics.BlendState = BlendState.Opaque; graphics.SamplerStates[0] = SamplerState.LinearWrap; - Quad.UseBasicEffect(renderTarget); - Quad.Render(); + GraphicsQuad.UseBasicEffect(renderTarget); + GraphicsQuad.Render(); //draw alpha blended particles that are inside a sub spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.DepthRead, null, null, cam.Transform); @@ -379,8 +379,8 @@ namespace Barotrauma graphics.DepthStencilState = DepthStencilState.None; graphics.SamplerStates[0] = SamplerState.LinearWrap; graphics.BlendState = CustomBlendStates.Multiplicative; - Quad.UseBasicEffect(GameMain.LightManager.LightMap); - Quad.Render(); + GraphicsQuad.UseBasicEffect(GameMain.LightManager.LightMap); + GraphicsQuad.Render(); } spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform); @@ -389,6 +389,8 @@ namespace Barotrauma c.DrawFront(spriteBatch, cam); } + GameMain.LightManager.DebugDrawVertices(spriteBatch); + Level.Loaded?.DrawDebugOverlay(spriteBatch, cam); if (GameMain.DebugDraw) { @@ -437,7 +439,7 @@ namespace Barotrauma graphics.SamplerStates[0] = SamplerState.PointClamp; graphics.SamplerStates[1] = SamplerState.PointClamp; GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply(); - Quad.Render(); + GraphicsQuad.Render(); graphics.SamplerStates[0] = SamplerState.LinearWrap; graphics.SamplerStates[1] = SamplerState.LinearWrap; } @@ -505,7 +507,7 @@ namespace Barotrauma graphics.DepthStencilState = DepthStencilState.None; if (string.IsNullOrEmpty(postProcessTechnique)) { - Quad.UseBasicEffect(renderTargetFinal); + GraphicsQuad.UseBasicEffect(renderTargetFinal); } else { @@ -514,7 +516,7 @@ namespace Barotrauma PostProcessEffect.CurrentTechnique = PostProcessEffect.Techniques[postProcessTechnique]; PostProcessEffect.CurrentTechnique.Passes[0].Apply(); } - Quad.Render(); + GraphicsQuad.Render(); if (fadeToBlackState > 0.0f) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 9d122c629..e0a8da297 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -221,7 +221,17 @@ namespace Barotrauma currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); - Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition); + + if (Submarine.MainSub != null) + { + Vector2 startPos = Level.Loaded.StartPosition; + if (Level.Loaded.StartOutpost != null) + { + startPos.Y -= Level.Loaded.StartOutpost.Borders.Height / 2 + Submarine.MainSub.Borders.Height / 2; + } + Submarine.MainSub?.SetPosition(startPos); + } + GameMain.LightManager.AddLight(pointerLightSource); if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index bd977496b..0982130cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -76,6 +76,8 @@ namespace Barotrauma private GUITextBlock tutorialHeader, tutorialDescription; private GUIListBox tutorialList; + private GUIComponent versionMismatchWarning; + #region Creation public MainMenuScreen(GameMain game) { @@ -105,6 +107,28 @@ namespace Barotrauma } }; + versionMismatchWarning = new GUIFrame(new RectTransform(new Vector2(0.7f, 0.065f), Frame.RectTransform) { AbsoluteOffset = new Point(GUI.IntScale(15)) }, style: "InnerFrame", color: GUIStyle.Red) + { + IgnoreLayoutGroups = true, + Visible = false + }; + var versionMismatchContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), versionMismatchWarning.RectTransform, Anchor.Center), isHorizontal: true) + { + RelativeSpacing = 0.05f, + }; + new GUIImage(new RectTransform(new Vector2(1.0f), versionMismatchContent.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "GUINotificationButton") + { + Color = GUIStyle.Orange + }; + new GUITextBlock(new RectTransform(new Vector2(0.85f, 1.0f), versionMismatchContent.RectTransform), + TextManager.GetWithVariables("versionmismatchwarning", + ("[gameversion]", GameMain.Version.ToString()), + ("[contentversion]", ContentPackageManager.VanillaCorePackage.GameVersion.ToString())), + wrap: true) + { + TextColor = GUIStyle.Orange + }; + new GUIImage(new RectTransform(new Vector2(0.4f, 0.25f), Frame.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.08f, 0.05f), AbsoluteOffset = new Point(-8, -8) }, style: "TitleText") @@ -141,6 +165,7 @@ namespace Barotrauma } } #else + SpamServerFilters.RequestGlobalSpamFilter(); FetchRemoteContent(); #endif @@ -587,7 +612,9 @@ namespace Barotrauma GameMain.SubEditorScreen?.ClearBackedUpSubInfo(); Submarine.Unload(); - + + versionMismatchWarning.Visible = GameMain.Version < ContentPackageManager.VanillaCorePackage.GameVersion; + ResetButtonStates(null); } @@ -663,7 +690,18 @@ namespace Barotrauma .ToArray(); foreach (var newServerExe in newServerExes) { - serverExecutableDropdown.AddItem($"{newServerExe.ContentPackage.Name} - {Path.GetFileNameWithoutExtension(newServerExe.Path.Value)}", userData: newServerExe); + var serverExeEntry = serverExecutableDropdown.AddItem($"{newServerExe.ContentPackage.Name} - {Path.GetFileNameWithoutExtension(newServerExe.Path.Value)}", userData: newServerExe); + if (newServerExe.ContentPackage.GameVersion < GameMain.VanillaContent.GameVersion) + { + serverExeEntry.ToolTip = + TextManager.GetWithVariables("versionmismatchwarning", + ("[gameversion]", newServerExe.ContentPackage.GameVersion.ToString()), + ("[contentversion]", GameMain.VanillaContent.GameVersion.ToString())); + if (serverExeEntry is GUITextBlock serverExeText) + { + serverExeText.TextColor = GUIStyle.Red; + } + } } serverExecutableDropdown.ListBox.Content.Children.ForEach(c => { @@ -1472,34 +1510,58 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { - string name = serverNameBox.Text; - if (string.IsNullOrEmpty(name)) - { - serverNameBox.Flash(); - return false; - } - - if (isPublicBox.Selected && ForbiddenWordFilter.IsForbidden(name, out string forbiddenWord)) - { - var msgBox = new GUIMessageBox("", - TextManager.GetWithVariables("forbiddenservernameverification", ("[forbiddenword]", forbiddenWord), ("[servername]", name)), - new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); - msgBox.Buttons[0].OnClicked += (_, __) => - { - TryStartServer(); - msgBox.Close(); - return true; - }; - msgBox.Buttons[1].OnClicked += msgBox.Close; - } - else - { - TryStartServer(); - } - + CheckServerName(); return true; } }; + + void CheckServerName() + { + string name = serverNameBox.Text; + if (string.IsNullOrEmpty(name)) + { + serverNameBox.Flash(); + return; + } + if (isPublicBox.Selected && ForbiddenWordFilter.IsForbidden(name, out string forbiddenWord)) + { + var msgBox = new GUIMessageBox("", + TextManager.GetWithVariables("forbiddenservernameverification", ("[forbiddenword]", forbiddenWord), ("[servername]", name)), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + msgBox.Buttons[0].OnClicked += (_, __) => + { + CheckServerExe(); + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked += msgBox.Close; + return; + } + CheckServerExe(); + } + + void CheckServerExe() + { + if (serverExecutableDropdown?.SelectedData is ServerExecutableFile serverExe && + serverExe.ContentPackage.GameVersion < GameMain.VanillaContent.GameVersion) + { + var msgBox = new GUIMessageBox(string.Empty, + TextManager.GetWithVariables("versionmismatchwarning", + ("[gameversion]", serverExe.ContentPackage.GameVersion.ToString()), + ("[contentversion]", GameMain.VanillaContent.GameVersion.ToString())) + "\n\n"+ + TextManager.GetWithVariable("versionmismatch.verifylaunch", "[exename]", serverExe.ContentPackage.Name), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + msgBox.Buttons[0].OnClicked += (_, __) => + { + TryStartServer(); + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked += msgBox.Close; + return; + } + TryStartServer(); + } } private void SetServerPlayStyle(PlayStyle playStyle) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index f004779d1..f35e8c9d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1520,6 +1520,7 @@ namespace Barotrauma }; bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; + changesPendingText?.Parent?.RemoveChild(changesPendingText); changesPendingText = null; if (TabMenu.PendingChanges) @@ -2389,10 +2390,20 @@ namespace Barotrauma options.Add(kickOption); } - options.Add(new ContextMenuOption("Ban", isEnabled: canBan, onSelected: delegate + if (GameMain.Client?.ServerSettings?.BanList?.BannedPlayers?.Any(bp => bp.MatchesClient(client)) ?? false) { - GameMain.Client?.CreateKickReasonPrompt(client.Name, true); - })); + options.Add(new ContextMenuOption("clientpermission.unban", isEnabled: canBan, onSelected: delegate + { + GameMain.Client?.UnbanPlayer(client.Name); + })); + } + else + { + options.Add(new ContextMenuOption("Ban", isEnabled: canBan, onSelected: delegate + { + GameMain.Client?.CreateKickReasonPrompt(client.Name, true); + })); + } GUIContextMenu.CreateContextMenu(null, client.Name, headerColor: clientColor, options.ToArray()); } @@ -2591,11 +2602,11 @@ namespace Barotrauma foreach (DebugConsole.Command command in DebugConsole.Commands) { var commandTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), commandList.Content.RectTransform), - command.names[0], font: GUIStyle.SmallFont) + command.Names[0].Value, font: GUIStyle.SmallFont) { Selected = selectedClient.PermittedConsoleCommands.Contains(command), Enabled = !myClient, - ToolTip = command.help, + ToolTip = command.Help, UserData = command }; commandTickBox.OnSelected += (GUITickBox tickBox) => @@ -2630,12 +2641,25 @@ namespace Barotrauma { if (GameMain.Client.HasPermission(ClientPermissions.Ban)) { - var banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), - TextManager.Get("Ban")) + GUIButton banButton; + if (GameMain.Client?.ServerSettings?.BanList?.BannedPlayers?.Any(bp => bp.MatchesClient(selectedClient)) ?? false) { - UserData = selectedClient - }; - banButton.OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; }; + banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), + TextManager.Get("clientpermission.unban")) + { + UserData = selectedClient + }; + banButton.OnClicked = (bt, userdata) => { GameMain.Client?.UnbanPlayer(selectedClient.Name); return true; }; + } + else + { + banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), + TextManager.Get("Ban")) + { + UserData = selectedClient + }; + banButton.OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; }; + } banButton.OnClicked += ClosePlayerFrame; } @@ -3147,12 +3171,12 @@ namespace Barotrauma GUIButton jobButton = null; var availableJobs = JobPrefab.Prefabs.Where(jobPrefab => - jobPrefab.MaxNumber > 0 && JobList.Content.Children.All(c => !(c.UserData is JobVariant prefab) || prefab.Prefab != jobPrefab) + !jobPrefab.HiddenJob && jobPrefab.MaxNumber > 0 && JobList.Content.Children.All(c => c.UserData is not JobVariant prefab || prefab.Prefab != jobPrefab) ).Select(j => new JobVariant(j, 0)); availableJobs = availableJobs.Concat( JobPrefab.Prefabs.Where(jobPrefab => - jobPrefab.MaxNumber > 0 && JobList.Content.Children.Any(c => (c.UserData is JobVariant prefab) && prefab.Prefab == jobPrefab) + !jobPrefab.HiddenJob && jobPrefab.MaxNumber > 0 && JobList.Content.Children.Any(c => (c.UserData is JobVariant prefab) && prefab.Prefab == jobPrefab) ).Select(j => (JobVariant)JobList.Content.FindChild(c => (c.UserData is JobVariant prefab) && prefab.Prefab == j).UserData)); availableJobs = availableJobs.ToList(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index d9a3de1c8..d48c4bc39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -655,7 +655,8 @@ namespace Barotrauma ScrollBarVisible = true, OnSelected = (btn, obj) => { - if (!(obj is ServerInfo serverInfo)) { return false; } + if (GUI.MouseOn is GUIButton) { return false; } + if (obj is not ServerInfo serverInfo) { return false; } joinButton.Enabled = true; selectedServer = Option.Some(serverInfo); @@ -852,6 +853,13 @@ namespace Barotrauma }); } + public void HideServerPreview() + { + serverPreviewContainer.Visible = false; + panelAnimator.RightEnabled = false; + panelAnimator.RightVisible = false; + } + private void InsertServer(ServerInfo serverInfo, GUIComponent component) { var children = serverList.Content.RectTransform.Children.Reverse().ToList(); @@ -973,7 +981,7 @@ namespace Barotrauma } } - private void FilterServers() + public void FilterServers() { RemoveMsgFromServerList(MsgUserData.NoMatchingServers); foreach (GUIComponent child in serverList.Content.Children) @@ -1013,6 +1021,7 @@ namespace Barotrauma return false; } #endif + if (SpamServerFilters.IsFiltered(serverInfo)) { return false; } if (!string.IsNullOrEmpty(searchBox.Text) && !serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase)) { return false; } @@ -1553,15 +1562,169 @@ namespace Barotrauma var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, style: "ListBoxElement") { - UserData = serverInfo + UserData = serverInfo, }; + + serverFrame.OnSecondaryClicked += (_, data) => + { + if (data is not ServerInfo info) { return false; } + CreateContextMenu(info); + return true; + }; + new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = false }; UpdateServerInfoUI(serverInfo); if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } + } + private static readonly Vector2 confirmPopupSize = new Vector2(0.2f, 0.2625f); + private static readonly Point confirmPopupMinSize = new Point(300, 300); + + private void CreateContextMenu(ServerInfo info) + { + var favoriteOption = new ContextMenuOption(IsFavorite(info) ? "removefromfavorites" : "addtofavorites", isEnabled: true, () => + { + if (IsFavorite(info)) + { + RemoveFromFavoriteServers(info); + } + else + { + AddToFavoriteServers(info); + } + FilterServers(); + }); + var reportOption = new ContextMenuOption("reportserver", isEnabled: true, () => { CreateReportPrompt(info); }); + var filterOption = new ContextMenuOption("filterserver", isEnabled: true, () => + { + CreateFilterServerPrompt(info); + }) + { + Tooltip = TextManager.Get("filterservertooltip") + }; + + GUIContextMenu.CreateContextMenu(favoriteOption, filterOption, reportOption); + } + + public static void CreateFilterServerPrompt(ServerInfo info) + { + GUI.AskForConfirmation( + header: TextManager.Get("filterserver"), + body: TextManager.GetWithVariables("filterserverconfirm", ("[server]", info.ServerName), ("[filepath]", SpamServerFilter.SavePath)), + onConfirm: () => + { + SpamServerFilters.AddServerToLocalSpamList(info); + + if (GameMain.ServerListScreen is not { } serverListScreen) { return; } + + if (serverListScreen.selectedServer.TryUnwrap(out var selectedServer) && selectedServer.Equals(info)) + { + serverListScreen.HideServerPreview(); + } + serverListScreen.FilterServers(); + }, relativeSize: confirmPopupSize, minSize: confirmPopupMinSize); + } + + private enum ReportReason + { + Spam, + Advertising, + Inappropriate + } + + public static void CreateReportPrompt(ServerInfo info) + { + if (!GameAnalyticsManager.SendUserStatistics) + { + GUI.NotifyPrompt(TextManager.Get("reportserver"), TextManager.Get("reportserverdisabled")); + return; + } + + var msgBox = new GUIMessageBox( + headerText: TextManager.Get("reportserver"), + text: string.Empty, + relativeSize: new Vector2(0.2f, 0.4f), + minSize: new Point(380, 430), + buttons: Array.Empty()); + + var layout = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform, Anchor.Center)); + + new GUITextBlock(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform), TextManager.GetWithVariable("reportserverexplanation", "[server]", info.ServerName), wrap: true) + { + ToolTip = TextManager.Get("reportserverprompttooltip") + }; + + var listBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform)); + + var enums = Enum.GetValues(); + foreach (ReportReason reason in enums) + { + new GUITickBox(new RectTransform(new Vector2(1f, 1f / enums.Length), listBox.Content.RectTransform), TextManager.Get($"reportreason.{reason}")) + { + UserData = reason + }; + } + + // padding + new GUIFrame(new RectTransform(new Vector2(1f, 0.05f), layout.RectTransform), style: null); + + var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform)) + { + Stretch = true + }; + + var reportAndHideButton = new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("reportoption.reportandhide")) + { + Enabled = false, + OnClicked = (_, _) => + { + CreateFilterServerPrompt(info); + msgBox.Close(); + return true; + } + }; + var reportButton = new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("reportoption.report")) + { + Enabled = false, + OnClicked = (_, _) => + { + ReportServer(info, GetUserSelectedReasons()); + msgBox.Close(); + return true; + } + }; + + new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("cancel")) + { + OnClicked = (_, _) => + { + msgBox.Close(); + return true; + } + }; + + foreach (var child in listBox.Content.GetAllChildren()) + { + child.OnSelected += _ => + { + reportAndHideButton.Enabled = reportButton.Enabled = GetUserSelectedReasons().Any(); + return true; + }; + } + + IEnumerable GetUserSelectedReasons() + => listBox.Content.Children + .Where(static c => c.UserData is ReportReason && c.Selected) + .Select(static c => (ReportReason)c.UserData).ToArray(); + } + + private static void ReportServer(ServerInfo info, IEnumerable reasons) + { + if (!reasons.Any()) { return; } + GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Info, $"[Spam] Reported server: Name: \"{info.ServerName}\", Message: \"{info.ServerMessage}\", Endpoint: \"{info.Endpoint.StringRepresentation}\". Reason: \"{string.Join(", ", reasons)}\"."); } private void UpdateServerInfoUI(ServerInfo serverInfo) @@ -1571,7 +1734,6 @@ namespace Barotrauma serverFrame.UserData = serverInfo; - serverFrame.ToolTip = ""; var serverContent = serverFrame.Children.First() as GUILayoutGroup; serverContent.ClearChildren(); @@ -1583,15 +1745,14 @@ namespace Barotrauma new RectTransform(new Vector2(columns[label].RelativeWidth, 1.0f), serverContent.RectTransform), style: null); } - - void errorTooltip(RichString toolTip) + + void disableElementFocus() { sections.Values.ForEach(c => { c.CanBeFocused = false; c.Children.First().CanBeFocused = false; }); - serverFrame.ToolTip = toolTip; } RectTransform columnRT(ColumnLabel label, float scale = 0.95f) @@ -1611,7 +1772,7 @@ namespace Barotrauma NetworkMember.IsCompatible(GameMain.Version, serverInfo.GameVersion), UserData = "compatible" }; - + var passwordBox = new GUITickBox(columnRT(ColumnLabel.ServerListHasPassword, scale: 0.6f), label: "", style: "GUIServerListPasswordTickBox") { Selected = serverInfo.HasPassword, @@ -1664,9 +1825,10 @@ namespace Barotrauma serverPingText.TextColor = Color.DarkRed; } + LocalizedString toolTip = ""; if (!serverInfo.Checked) { - errorTooltip(TextManager.Get("ServerOffline")); + toolTip = TextManager.Get("ServerOffline"); serverName.TextColor *= 0.8f; serverPlayers.TextColor *= 0.8f; } @@ -1681,7 +1843,6 @@ namespace Barotrauma } else if (!compatibleBox.Selected) { - LocalizedString toolTip = ""; if (serverInfo.GameVersion != GameMain.Version) { toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion.ToString()); @@ -1707,14 +1868,12 @@ namespace Barotrauma toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString()); } } - errorTooltip(toolTip); serverName.TextColor *= 0.5f; serverPlayers.TextColor *= 0.5f; } else { - LocalizedString toolTip = ""; foreach (var contentPackage in serverInfo.ContentPackages) { if (ContentPackageManager.EnabledPackages.All.None(cp => cp.Hash.StringRepresentation == contentPackage.Hash)) @@ -1724,8 +1883,11 @@ namespace Barotrauma break; } } - errorTooltip(toolTip); } + disableElementFocus(); + + string separator = toolTip.IsNullOrWhiteSpace() ? "" : "\n\n"; + serverFrame.ToolTip = RichString.Rich(toolTip + separator + $"‖color:gui.blue‖{TextManager.GetWithVariable("serverlisttooltip", "[button]", PlayerInput.SecondaryMouseLabel)}‖end‖"); foreach (var section in sections.Values) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 1fd3ca751..a9a34256c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -16,9 +16,6 @@ namespace Barotrauma { class SubEditorScreen : EditorScreen { - public const string CircuitBoxDeletionWarningHeader = "Selection contains circuit boxes", - CircuitBoxDeletionWarningBody = "Are you sure you want to delete the selection? Any wiring inside circuit boxes will be lost and cannot be recovered."; - public const int MaxStructures = 2000; public const int MaxWalls = 500; public const int MaxItems = 5000; @@ -1289,7 +1286,8 @@ namespace Barotrauma if (legacy) { textBlock.TextColor *= 0.6f; } if (name.IsNullOrEmpty()) { - DebugConsole.AddWarning($"Entity \"{ep.Identifier.Value}\" has no name!"); + DebugConsole.AddWarning($"Entity \"{ep.Identifier.Value}\" has no name!", + contentPackage: ep.ContentPackage); textBlock.Text = frame.ToolTip = ep.Identifier.Value; textBlock.TextColor = GUIStyle.Red; } @@ -1559,8 +1557,17 @@ namespace Barotrauma if (editorSelectedTime.TryUnwrap(out DateTime selectedTime)) { TimeSpan timeInEditor = DateTime.Now - selectedTime; - SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); - editorSelectedTime = Option.None(); + if (timeInEditor.TotalSeconds > Timing.TotalTime) + { + DebugConsole.ThrowErrorAndLogToGA( + "SubEditorScreen.DeselectEditorSpecific:InvalidTimeInEditor", + $"Error in sub editor screen. Calculated time in editor {timeInEditor} was larger than the time the game has run ({Timing.TotalTime} s)."); + } + else + { + SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); + editorSelectedTime = Option.None(); + } } #endif @@ -2365,49 +2372,58 @@ namespace Barotrauma //--------------------------------------- - var beaconSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform)) + var extraSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.5f), subTypeDependentSettingFrame.RectTransform)) { CanBeFocused = true, Visible = false, Stretch = true }; - // ------------------- - - var beaconMinDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), isHorizontal: true) + var minDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), extraSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMinDifficultyGroup.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), minDifficultyGroup.RectTransform), TextManager.Get("minleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); - var numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), beaconMinDifficultyGroup.RectTransform), NumberType.Int) + var numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), minDifficultyGroup.RectTransform), NumberType.Int) { - IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MinLevelDifficulty ?? 0), + IntValue = (int)(MainSub?.Info?.GetExtraSubmarineInfo?.MinLevelDifficulty ?? 0), MinValueInt = 0, MaxValueInt = 100, OnValueChanged = (numberInput) => { - MainSub.Info.BeaconStationInfo.MinLevelDifficulty = numberInput.IntValue; + MainSub.Info.GetExtraSubmarineInfo.MinLevelDifficulty = numberInput.IntValue; } }; - beaconMinDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; - var beaconMaxDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), isHorizontal: true) + minDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; + var maxDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), extraSettingsContainer.RectTransform), isHorizontal: true) { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), beaconMaxDifficultyGroup.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), maxDifficultyGroup.RectTransform), TextManager.Get("maxleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true); - numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), beaconMaxDifficultyGroup.RectTransform), NumberType.Int) + numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), maxDifficultyGroup.RectTransform), NumberType.Int) { - IntValue = (int)(MainSub?.Info?.BeaconStationInfo?.MaxLevelDifficulty ?? 100), + IntValue = (int)(MainSub?.Info?.GetExtraSubmarineInfo?.MaxLevelDifficulty ?? 100), MinValueInt = 0, MaxValueInt = 100, OnValueChanged = (numberInput) => { - MainSub.Info.BeaconStationInfo.MaxLevelDifficulty = numberInput.IntValue; + MainSub.Info.GetExtraSubmarineInfo.MaxLevelDifficulty = numberInput.IntValue; } }; - beaconMaxDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; + maxDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize; + + + //--------------------------------------- + + var beaconSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, extraSettingsContainer.RectTransform)) + { + CanBeFocused = true, + Visible = false, + Stretch = true + }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdamagedwalls")) { Selected = MainSub?.Info?.BeaconStationInfo?.AllowDamagedWalls ?? true, @@ -2669,8 +2685,13 @@ namespace Barotrauma { MainSub.Info.BeaconStationInfo ??= new BeaconStationInfo(MainSub.Info); } + else if (type == SubmarineType.Wreck) + { + MainSub.Info.WreckInfo ??= new WreckInfo(MainSub.Info); + } previewImageButtonHolder.Children.ForEach(c => c.Enabled = MainSub.Info.AllowPreviewImage); outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; + extraSettingsContainer.Visible = type == SubmarineType.BeaconStation || type == SubmarineType.Wreck; beaconSettingsContainer.Visible = type == SubmarineType.BeaconStation; subSettingsContainer.Visible = type == SubmarineType.Player; return true; @@ -3918,28 +3939,15 @@ namespace Barotrauma new ContextMenuOption("editor.cut", isEnabled: hasTargets, onSelected: () => MapEntity.Cut(targets)), new ContextMenuOption("editor.copytoclipboard", isEnabled: hasTargets, onSelected: () => MapEntity.Copy(targets)), new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))), - new ContextMenuOption("delete", isEnabled: hasTargets, onSelected: () => RemoveEntitiesWithPossibleWarning(targets)), - new ContextMenuOption(TextManager.Get("editortip.shiftforextraoptions") + '\n' + TextManager.Get("editortip.altforruler"), isEnabled: false, onSelected: null)); - } - } - - public static void RemoveEntitiesWithPossibleWarning(List targets) - { - if (targets.Any(static t => t is Item it && it.GetComponent() is not null)) - { - GUI.AskForConfirmation(CircuitBoxDeletionWarningHeader, CircuitBoxDeletionWarningBody, onConfirm: Delete); - return; - } - - Delete(); - - void Delete() - { - StoreCommand(new AddOrDeleteCommand(targets, true)); - foreach (var me in targets) - { - if (!me.Removed) { me.Remove(); } - } + new ContextMenuOption("delete", isEnabled: hasTargets, onSelected: () => + { + StoreCommand(new AddOrDeleteCommand(targets, true)); + foreach (var me in targets) + { + if (!me.Removed) { me.Remove(); } + } + }), + new ContextMenuOption(TextManager.GetWithVariable("editortip.shiftforextraoptions", "[button]", PlayerInput.SecondaryMouseLabel) + '\n' + TextManager.Get("editortip.altforruler"), isEnabled: false, onSelected: null)); } } @@ -4439,6 +4447,7 @@ namespace Barotrauma MapEntity.SelectEntity(itemContainer); dummyCharacter.SelectedItem = itemContainer; FilterEntities(entityFilterBox.Text); + MapEntity.StopSelection(); } /// @@ -5469,9 +5478,11 @@ namespace Barotrauma { foreach (LightComponent lightComponent in item.GetComponents()) { - lightComponent.Light.Color = item.Container != null || (item.body != null && !item.body.Enabled) ? - Color.Transparent : - lightComponent.LightColor; + lightComponent.Light.Color = + item.body == null || item.body.Enabled || + (item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems) ? + lightComponent.LightColor : + Color.Transparent; lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects; } } @@ -5556,11 +5567,32 @@ namespace Barotrauma dummyCharacter.Submarine = MainSub; } - // Deposit item from our "infinite stack" into inventory slots - var inv = dummyCharacter?.SelectedItem?.OwnInventory; - if (inv?.visualSlots != null && !PlayerInput.IsCtrlDown()) + if (dummyCharacter?.SelectedItem != null) { - var dragginMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20; + // Deposit item from our "infinite stack" into inventory slots + TryDragItemsToItem(dummyCharacter.SelectedItem); + foreach (Item linkedItem in dummyCharacter.SelectedItem.linkedTo.OfType()) + { + TryDragItemsToItem(linkedItem); + } + } + + void TryDragItemsToItem(Item item) + { + foreach (ItemContainer ic in item.GetComponents()) + { + if (ic.Inventory?.visualSlots != null) + { + TryDragItemsToInventory(ic.Inventory); + } + } + } + + void TryDragItemsToInventory(Inventory inv) + { + if (PlayerInput.IsCtrlDown()) { return; } + + var draggingMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20; // So we don't accidentally drag inventory items while doing this if (DraggedItemPrefab != null) { Inventory.DraggingItems.Clear(); } @@ -5568,134 +5600,134 @@ namespace Barotrauma switch (DraggedItemPrefab) { // regular item prefabs - case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || dragginMouse: - { - bool spawnedItem = false; - for (var i = 0; i < inv.Capacity; i++) + case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || draggingMouse: { - var slot = inv.visualSlots[i]; - var itemContainer = inv.GetItemAt(i)?.GetComponent(); - - // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit - if (Inventory.IsMouseOnSlot(slot)) + bool spawnedItem = false; + for (var i = 0; i < inv.Capacity; i++) { - var newItem = new Item(itemPrefab, Vector2.Zero, MainSub); + var slot = inv.visualSlots[i]; + var itemContainer = inv.GetItemAt(i)?.GetComponent(); - if (inv.CanBePutInSlot(itemPrefab, i, condition: null)) + // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit + if (Inventory.IsMouseOnSlot(slot)) { - bool placedItem = inv.TryPutItem(newItem, i, false, true, dummyCharacter); - spawnedItem |= placedItem; + var newItem = new Item(itemPrefab, Vector2.Zero, MainSub); - if (!placedItem) + if (inv.CanBePutInSlot(itemPrefab, i, condition: null)) { - newItem.Remove(); + bool placedItem = inv.TryPutItem(newItem, i, false, true, dummyCharacter); + spawnedItem |= placedItem; + + if (!placedItem) + { + newItem.Remove(); + } } - } - else if (itemContainer != null && itemContainer.Inventory.CanBePut(itemPrefab)) - { - bool placedItem = itemContainer.Inventory.TryPutItem(newItem, dummyCharacter); - spawnedItem |= placedItem; - - // try to place the item into the inventory of the item we are hovering over - if (!placedItem) + else if (itemContainer != null && itemContainer.Inventory.CanBePut(itemPrefab)) { - newItem.Remove(); + bool placedItem = itemContainer.Inventory.TryPutItem(newItem, dummyCharacter); + spawnedItem |= placedItem; + + // try to place the item into the inventory of the item we are hovering over + if (!placedItem) + { + newItem.Remove(); + } + else + { + slot.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); + } } else { - slot.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); + newItem.Remove(); + slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f); + } + + if (!newItem.Removed) + { + BulkItemBufferInUse = ItemAddMutex; + BulkItemBuffer.Add(new AddOrDeleteCommand(new List { newItem }, false)); + } + + if (!draggingMouse) + { + SoundPlayer.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); } } - else - { - newItem.Remove(); - slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f); - } - - if (!newItem.Removed) - { - BulkItemBufferInUse = ItemAddMutex; - BulkItemBuffer.Add(new AddOrDeleteCommand(new List { newItem }, false)); - } - - if (!dragginMouse) - { - SoundPlayer.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); - } } + break; } - break; - } // item assemblies case ItemAssemblyPrefab assemblyPrefab when PlayerInput.PrimaryMouseButtonClicked(): - { - bool spawnedItems = false; - for (var i = 0; i < inv.visualSlots.Length; i++) { - var slot = inv.visualSlots[i]; - var item = inv?.GetItemAt(i); - var itemContainer = item?.GetComponent(); - if (item == null && Inventory.IsMouseOnSlot(slot)) + bool spawnedItems = false; + for (var i = 0; i < inv.visualSlots.Length; i++) { - // load the items - var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); - - // counter for items that failed so we so we known that slot remained empty - var failedCount = 0; - - for (var j = 0; j < itemInstance.Count(); j++) + var slot = inv.visualSlots[i]; + var item = inv?.GetItemAt(i); + var itemContainer = item?.GetComponent(); + if (item == null && Inventory.IsMouseOnSlot(slot)) { - var newItem = itemInstance[j]; - var newSpot = i + j - failedCount; + // load the items + var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); - // try to find a valid slot to put the items - while (inv.visualSlots.Length > newSpot) + // counter for items that failed so we so we known that slot remained empty + var failedCount = 0; + + for (var j = 0; j < itemInstance.Count; j++) { - if (inv.GetItemAt(newSpot) == null) { break; } - newSpot++; - } + var newItem = itemInstance[j]; + var newSpot = i + j - failedCount; - // valid slot found - if (inv.visualSlots.Length > newSpot) - { - var placedItem = inv.TryPutItem(newItem, newSpot, false, true, dummyCharacter); - spawnedItems |= placedItem; - - if (!placedItem) + // try to find a valid slot to put the items + while (inv.visualSlots.Length > newSpot) { - failedCount++; - // delete the included items too so we don't get a popup asking if we want to keep them - newItem?.OwnInventory?.DeleteAllItems(); - newItem.Remove(); + if (inv.GetItemAt(newSpot) == null) { break; } + newSpot++; + } + + // valid slot found + if (inv.visualSlots.Length > newSpot) + { + var placedItem = inv.TryPutItem(newItem, newSpot, false, true, dummyCharacter); + spawnedItems |= placedItem; + + if (!placedItem) + { + failedCount++; + // delete the included items too so we don't get a popup asking if we want to keep them + newItem?.OwnInventory?.DeleteAllItems(); + newItem.Remove(); + } + } + else + { + var placedItem = inv.TryPutItem(newItem, dummyCharacter); + spawnedItems |= placedItem; + + // if our while loop didn't find a valid slot then let the inventory decide where to put it as a last resort + if (!placedItem) + { + // delete the included items too so we don't get a popup asking if we want to keep them + newItem?.OwnInventory?.DeleteAllItems(); + newItem.Remove(); + } } } - else + + List placedEntities = itemInstance.Where(it => !it.Removed).Cast().ToList(); + if (placedEntities.Any()) { - var placedItem = inv.TryPutItem(newItem, dummyCharacter); - spawnedItems |= placedItem; - - // if our while loop didn't find a valid slot then let the inventory decide where to put it as a last resort - if (!placedItem) - { - // delete the included items too so we don't get a popup asking if we want to keep them - newItem?.OwnInventory?.DeleteAllItems(); - newItem.Remove(); - } + BulkItemBufferInUse = ItemAddMutex; + BulkItemBuffer.Add(new AddOrDeleteCommand(placedEntities, false)); } } - - List placedEntities = itemInstance.Where(it => !it.Removed).Cast().ToList(); - if (placedEntities.Any()) - { - BulkItemBufferInUse = ItemAddMutex; - BulkItemBuffer.Add(new AddOrDeleteCommand(placedEntities, false)); - } } - } - SoundPlayer.PlayUISound(spawnedItems ? GUISoundType.PickItem : GUISoundType.PickItemFail); - break; - } + SoundPlayer.PlayUISound(spawnedItems ? GUISoundType.PickItem : GUISoundType.PickItemFail); + break; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 8cd9300f5..38f110c95 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -571,16 +571,37 @@ namespace Barotrauma numberInput.MinValueFloat = editableAttribute.MinValueFloat; numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; - numberInput.valueStep = editableAttribute.ValueStep; + numberInput.ValueStep = editableAttribute.ValueStep; + numberInput.ForceShowPlusMinusButtons = editableAttribute.ForceShowPlusMinusButtons; numberInput.FloatValue = value; - numberInput.OnValueChanged += (numInput) => + numberInput.OnValueChanged += numInput => { if (SetPropertyValue(property, entity, numInput.FloatValue)) { TrySendNetworkUpdate(entity, property); } }; + + // Lots of UI boilerplate to handle all(?) cases where the property's setter may be called + // and modify the input value (e.g. rotation value wrapping) + void HandleSetterModifyingInput(GUINumberInput numInput) + { + var inputFloatValue = numInput.FloatValue; + var resultingFloatValue = property.GetFloatValue(entity); + if (!MathUtils.NearlyEqual(resultingFloatValue, inputFloatValue)) + { + numInput.FloatValue = resultingFloatValue; + } + } + bool HandleSetterModifyingInputOnButtonPressed() { HandleSetterModifyingInput(numberInput); return true; } + bool HandleSetterModifyingInputOnButtonClicked(GUIButton _, object __) { HandleSetterModifyingInput(numberInput); return true; } + + numberInput.OnValueEntered += HandleSetterModifyingInput; + numberInput.PlusButton.OnPressed += HandleSetterModifyingInputOnButtonPressed; + numberInput.PlusButton.OnClicked += HandleSetterModifyingInputOnButtonClicked; + numberInput.MinusButton.OnPressed += HandleSetterModifyingInputOnButtonPressed; + numberInput.MinusButton.OnClicked += HandleSetterModifyingInputOnButtonClicked; refresh += () => { if (!numberInput.TextBox.Selected) { numberInput.FloatValue = (float)property.GetValue(entity); } @@ -859,7 +880,7 @@ namespace Barotrauma numberInput.MinValueFloat = editableAttribute.MinValueFloat; numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; - numberInput.valueStep = editableAttribute.ValueStep; + numberInput.ValueStep = editableAttribute.ValueStep; if (i == 0) numberInput.FloatValue = value.X; @@ -930,7 +951,7 @@ namespace Barotrauma numberInput.MinValueFloat = editableAttribute.MinValueFloat; numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; - numberInput.valueStep = editableAttribute.ValueStep; + numberInput.ValueStep = editableAttribute.ValueStep; if (i == 0) numberInput.FloatValue = value.X; @@ -1006,7 +1027,7 @@ namespace Barotrauma numberInput.MinValueFloat = editableAttribute.MinValueFloat; numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; - numberInput.valueStep = editableAttribute.ValueStep; + numberInput.ValueStep = editableAttribute.ValueStep; if (i == 0) numberInput.FloatValue = value.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 4d443268b..24d40dd72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -727,7 +727,21 @@ namespace Barotrauma Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); Label(layout, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); - + Spacer(layout); + var resetSpamListFilter = + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), layout.RectTransform), + TextManager.Get("clearserverlistfilters"), style: "GUIButtonSmall") + { + OnClicked = static (_, _) => + { + GUI.AskForConfirmation( + header: TextManager.Get("clearserverlistfilters"), + body: TextManager.Get("clearserverlistfiltersconfirmation"), + onConfirm: SpamServerFilters.ClearLocalSpamFilter); + return true; + } + }; + Spacer(layout); #if !OSX Spacer(layout); var statisticsTickBox = new GUITickBox(NewItemRectT(layout), TextManager.Get("statisticsconsenttickbox")) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index f816a71a8..d050e09c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -1,22 +1,23 @@ -using System; +using NVorbis; using OpenAL; -using NVorbis; +using System; using System.Collections.Generic; using System.Threading.Tasks; -using System.Xml.Linq; namespace Barotrauma.Sounds { sealed class OggSound : Sound { - private VorbisReader streamReader; + private readonly VorbisReader streamReader; + + public long MaxStreamSamplePos => streamReader == null ? 0 : streamReader.TotalSamples * streamReader.Channels * 2; private List playbackAmplitude; private const int AMPLITUDE_SAMPLE_COUNT = 4410; //100ms in a 44100hz file private short[] sampleBuffer = Array.Empty(); private short[] muffleBuffer = Array.Empty(); - public OggSound(SoundManager owner, string filename, bool stream, XElement xElement) : base(owner, filename, + public OggSound(SoundManager owner, string filename, bool stream, ContentXElement xElement) : base(owner, filename, stream, true, xElement) { var reader = new VorbisReader(Filename); @@ -101,7 +102,7 @@ namespace Barotrauma.Sounds if (!Stream) { throw new Exception("Called FillStreamBuffer on a non-streamed sound!"); } if (streamReader == null) { throw new Exception("Called FillStreamBuffer when the reader is null!"); } - if (samplePos >= streamReader.TotalSamples * streamReader.Channels * 2) return 0; + if (samplePos >= MaxStreamSamplePos) { return 0; } samplePos /= streamReader.Channels * 2; streamReader.DecodedPosition = samplePos; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index 8d2ea80c6..8c815b9c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Sounds public readonly string Filename; - public readonly XElement XElement; + public readonly ContentXElement XElement; public readonly bool Stream; @@ -60,14 +60,14 @@ namespace Barotrauma.Sounds public float BaseNear; public float BaseFar; - public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, XElement xElement = null, bool getFullPath = true) + public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, ContentXElement xElement = null, bool getFullPath = true) { Owner = owner; Filename = getFullPath ? Path.GetFullPath(filename.CleanUpPath()).CleanUpPath() : filename; Stream = stream; StreamsReliably = streamsReliably; XElement = xElement; - sourcePoolIndex = XElement.GetAttributeEnum("sourcepool", SoundManager.SourcePoolIndex.Default); + sourcePoolIndex = XElement?.GetAttributeEnum("sourcepool", SoundManager.SourcePoolIndex.Default) ?? SoundManager.SourcePoolIndex.Default; BaseGain = 1.0f; BaseNear = 100.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index b08ed5862..08e9811ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -444,6 +444,18 @@ namespace Barotrauma.Sounds } } + public long MaxStreamSeekPos + { + get + { + if (!IsStream || Sound is not OggSound oggSound) + { + return 0; + } + return oggSound.MaxStreamSamplePos; + } + } + private readonly object mutex; public bool IsPlaying @@ -564,7 +576,7 @@ namespace Barotrauma.Sounds throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName); } } - Sound.Owner.InitStreamThread(); + Sound.Owner.InitUpdateChannelThread(); SetProperties(); } } @@ -609,6 +621,7 @@ namespace Barotrauma.Sounds public void FadeOutAndDispose() { FadingOutAndDisposing = true; + Sound.Owner.InitUpdateChannelThread(); } public void Dispose() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index e6cefeb2d..c9a06e685 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -39,7 +39,7 @@ namespace Barotrauma.Sounds public bool Disconnected { get; private set; } - private Thread streamingThread; + private Thread updateChannelsThread; private Vector3 listenerPosition; public Vector3 ListenerPosition @@ -201,7 +201,7 @@ namespace Barotrauma.Sounds public SoundManager() { loadedSounds = new List(); - streamingThread = null; + updateChannelsThread = null; sourcePools = new SoundSourcePool[2]; playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT]; @@ -696,7 +696,7 @@ namespace Barotrauma.Sounds CompressionDynamicRangeGain = 1.0f; } - if (streamingThread == null || streamingThread.ThreadState.HasFlag(ThreadState.Stopped)) + if (updateChannelsThread == null || updateChannelsThread.ThreadState.HasFlag(ThreadState.Stopped)) { bool startedStreamThread = false; for (int i = 0; i < playingChannels.Length; i++) @@ -708,7 +708,7 @@ namespace Barotrauma.Sounds if (playingChannels[i][j] == null) { continue; } if (playingChannels[i][j].IsStream && playingChannels[i][j].IsPlaying) { - InitStreamThread(); + InitUpdateChannelThread(); startedStreamThread = true; } if (startedStreamThread) { break; } @@ -727,37 +727,43 @@ namespace Barotrauma.Sounds SetCategoryGainMultiplier("music", GameSettings.CurrentConfig.Audio.MusicVolume, 0); SetCategoryGainMultiplier("voip", Math.Min(GameSettings.CurrentConfig.Audio.VoiceChatVolume, 1.0f), 0); } - - public void InitStreamThread() + + /// + /// Initializes the thread that handles streaming audio and fading out and disposing channels that are no longer needed. + /// + public void InitUpdateChannelThread() { if (Disabled) { return; } - bool isStreamThreadDying; + bool isUpdateChannelsThreadDying; lock (threadDeathMutex) { - isStreamThreadDying = !areStreamsPlaying; + isUpdateChannelsThreadDying = !needsUpdateChannels; } - if (streamingThread == null || streamingThread.ThreadState.HasFlag(ThreadState.Stopped) || isStreamThreadDying) + if (updateChannelsThread == null || updateChannelsThread.ThreadState.HasFlag(ThreadState.Stopped) || isUpdateChannelsThreadDying) { - if (streamingThread != null && !streamingThread.Join(1000)) + if (updateChannelsThread != null && !updateChannelsThread.Join(1000)) { - DebugConsole.ThrowError("Sound stream thread join timed out!"); + DebugConsole.ThrowError("SoundManager.UpdateChannels thread join timed out!"); } - areStreamsPlaying = true; - streamingThread = new Thread(UpdateStreaming) + needsUpdateChannels = true; + updateChannelsThread = new Thread(UpdateChannels) { - Name = "SoundManager Streaming Thread", + Name = "SoundManager.UpdateChannels Thread", IsBackground = true //this should kill the thread if the game crashes }; - streamingThread.Start(); + updateChannelsThread.Start(); } } - bool areStreamsPlaying = false; - ManualResetEvent streamMre = null; + private bool needsUpdateChannels = false; + private ManualResetEvent updateChannelsMre = null; - void UpdateStreaming() + /// + /// Handles streaming audio and fading out and disposing channels that are no longer needed. + /// + private void UpdateChannels() { - streamMre = new ManualResetEvent(false); + updateChannelsMre = new ManualResetEvent(false); bool killThread = false; while (!killThread) { @@ -784,6 +790,7 @@ namespace Barotrauma.Sounds } else if (playingChannels[i][j].FadingOutAndDisposing) { + killThread = false; playingChannels[i][j].Gain -= 0.1f; if (playingChannels[i][j].Gain <= 0.0f) { @@ -794,18 +801,18 @@ namespace Barotrauma.Sounds } } } - streamMre.WaitOne(10); - streamMre.Reset(); + updateChannelsMre.WaitOne(10); + updateChannelsMre.Reset(); lock (threadDeathMutex) { - areStreamsPlaying = !killThread; + needsUpdateChannels = !killThread; } } } public void ForceStreamUpdate() { - streamMre?.Set(); + updateChannelsMre?.Set(); } private void ReloadSounds() @@ -824,12 +831,12 @@ namespace Barotrauma.Sounds { for (int j = 0; j < playingChannels[i].Length; j++) { - if (playingChannels[i][j] != null) playingChannels[i][j].Dispose(); + playingChannels[i][j]?.Dispose(); } } } - streamingThread?.Join(); + updateChannelsThread?.Join(); for (int i = loadedSounds.Count - 1; i >= 0; i--) { if (keepSounds) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index e04b5a60f..3be4a5571 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -709,6 +709,11 @@ namespace Barotrauma { musicChannel[i].StreamSeekPos = targetMusic[i].PreviousTime; } + else if (targetMusic[i].StartFromRandomTime) + { + musicChannel[i].StreamSeekPos = + (int)(musicChannel[i].MaxStreamSeekPos * Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced)); + } musicChannel[i].Looping = true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs index bb3af5eee..f6c8a2beb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -241,6 +241,7 @@ namespace Barotrauma public readonly bool MuteIntensityTracks; public readonly float? ForceIntensityTrack; + public readonly bool StartFromRandomTime; public readonly bool ContinueFromPreviousTime; public int PreviousTime; @@ -255,6 +256,7 @@ namespace Barotrauma ForceIntensityTrack = element.GetAttributeFloat(nameof(ForceIntensityTrack), 0.0f); } Volume = element.GetAttributeFloat(nameof(Volume), 1.0f); + StartFromRandomTime = element.GetAttributeBool(nameof(StartFromRandomTime), false); ContinueFromPreviousTime = element.GetAttributeBool(nameof(ContinueFromPreviousTime), false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs new file mode 100644 index 000000000..07da313e3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs @@ -0,0 +1,330 @@ +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Net; +using System.Net.Cache; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Barotrauma.IO; +using Barotrauma.Networking; +using RestSharp; +using XmlWriter = Barotrauma.IO.XmlWriter; + +namespace Barotrauma +{ + public enum SpamServerFilterType + { + Invalid, + NameEquals, + NameContains, + MessageEquals, + MessageContains, + PlayerCountLarger, + PlayerCountExact, + MaxPlayersLarger, + MaxPlayersExact, + GameModeEquals, + PlayStyleEquals, + Endpoint, + LanguageEquals + } + + internal readonly record struct SpamFilter(ImmutableHashSet<(SpamServerFilterType Type, string Value)> Filters) + { + public bool IsFiltered(ServerInfo info) + { + if (!Filters.Any()) { return false; } + + foreach (var (type, value) in Filters) + { + if (!IsFiltered(info, type, value)) { return false; } + } + + return true; + } + + private static bool IsFiltered(ServerInfo info, SpamServerFilterType type, string value) + { + string desc = info.ServerMessage, + name = info.ServerName; + + int.TryParse(value, out int parsedInt); + + return type switch + { + SpamServerFilterType.NameEquals => CompareEquals(name, value), + SpamServerFilterType.NameContains => CompareContains(name, value), + + SpamServerFilterType.MessageEquals => CompareEquals(desc, value), + SpamServerFilterType.MessageContains => CompareContains(desc, value), + + SpamServerFilterType.Endpoint => info.Endpoint.StringRepresentation.Equals(value, StringComparison.OrdinalIgnoreCase), + + SpamServerFilterType.PlayerCountLarger => info.PlayerCount > parsedInt, + SpamServerFilterType.PlayerCountExact => info.PlayerCount == parsedInt, + + SpamServerFilterType.MaxPlayersLarger => info.MaxPlayers > parsedInt, + SpamServerFilterType.MaxPlayersExact => info.MaxPlayers == parsedInt, + + SpamServerFilterType.GameModeEquals => info.GameMode == value, + SpamServerFilterType.PlayStyleEquals => info.PlayStyle.ToIdentifier() == value, + + SpamServerFilterType.LanguageEquals => info.Language.Value == value, + _ => false + }; + + static bool CompareEquals(string a, string b) + => a.Equals(b, StringComparison.OrdinalIgnoreCase) || Homoglyphs.Compare(a, b); + + static bool CompareContains(string a, string b) + => a.Contains(b, StringComparison.OrdinalIgnoreCase); + } + + public XElement Serialize() + { + var element = new XElement("Filter"); + + foreach (var (type, value) in Filters) + { + element.Add(new XAttribute(type.ToString().ToLowerInvariant(), value)); + } + + return element; + } + + public static bool TryParse(XElement element, out SpamFilter filter) + { + var builder = ImmutableHashSet.CreateBuilder<(SpamServerFilterType Type, string Value)>(); + foreach (var attribute in element.Attributes()) + { + if (!Enum.TryParse(attribute.Name.ToString(), ignoreCase: true, out SpamServerFilterType e)) + { + DebugConsole.ThrowError($"Failed to parse spam filter attribute \"{attribute.Name}\""); + continue; + } + if (e is SpamServerFilterType.Invalid) { continue; } + builder.Add((e, attribute.Value)); + } + + if (builder.Any()) + { + filter = new SpamFilter(builder.ToImmutable()); + return true; + } + + filter = default; + return false; + } + + public override string ToString() + { + return !Filters.Any() ? "Invalid Filter" : string.Join(", ", Filters.Select(static f => $"{f.Type}: {f.Value}")); + } + } + + internal sealed class SpamServerFilter + { + public readonly ImmutableArray Filters; + + public bool IsFiltered(ServerInfo info) + { + foreach (var f in Filters) + { + if (f.IsFiltered(info)) { return true; } + } + + return false; + } + + public SpamServerFilter(XElement element) + { + var builder = ImmutableArray.CreateBuilder(); + foreach (var subElement in element.Elements()) + { + if (SpamFilter.TryParse(subElement, out var filter)) + { + builder.Add(filter); + } + } + Filters = builder.ToImmutable(); + } + + public SpamServerFilter(ImmutableArray filters) + => Filters = filters; + + public readonly static string SavePath = Path.Combine("Data", "serverblacklist.xml"); + + public void Save(string path) + { + var comment = new XComment(SpamServerFilters.LocalFilterComment); + var doc = new XDocument(comment, new XElement("Filters")); + foreach (var filter in Filters) + { + doc.Root?.Add(filter.Serialize()); + } + + try + { + using var writer = XmlWriter.Create(path, new XmlWriterSettings { Indent = true }); + doc.SaveSafe(writer); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving spam filter failed.", e); + } + } + } + + internal static class SpamServerFilters + { + public static Option LocalSpamFilter; + public static Option GlobalSpamFilter; + + public const string LocalFilterComment = @" +This file contains a list of filters that can be used to hide servers from the server list. +You can add filters by right-clicking a server in the server list and selecting ""Hide server"" or by reporting the server and choosing ""Report and hide server"". +The filters are saved in this file, which you can edit manually if you want to. + +The available filter types are: +- NameEquals: The server name must equal the specified value. Homoglyphs are also checked. +- NameContains: The server name must contain the specified value. +- MessageEquals: The server description must equal the specified value. Homoglyphs are also checked. +- MessageContains: The server description must contain the specified value. +- PlayerCountLarger: The player count must be larger than the specified value. +- PlayerCountExact: The player count must match the specified value exactly. +- MaxPlayersLarger: The max player count must be larger than the specified value. +- MaxPlayersExact: The max player count must match the specified value exactly. +- GameModeEquals: The game mode identifier must match the specified value exactly. +- PlayStyleEquals: The play style must match the specified value exactly. +- Endpoint: The server endpoint, which is a Steam ID or an IP address, must match the specified value exactly. Steam ID is in the format of STEAM_X:Y:Z. +- LanguageEquals: The server language must match the specified value exactly. + +The filter values are case-insensitive and adding multiple conditions on one filter will require all of them to be met. +Homoglyph comparison is used for NameEquals and MessageEquals filters, which means that it checks whether the words look the same, meaning you can't abuse identical-looking but different symbols to work around the filter. For example ""lmaobox"" and ""lmаobox"" (with a cyrillic a) are considered equal. + +Examples: + + + + + +These will hide all servers that have a discord.gg link in their name or description and servers with the name ""get good get lmaobox"" that have 999 max players. +"; + static SpamServerFilters() + { + XDocument? doc; + if (!File.Exists(SpamServerFilter.SavePath)) + { + var comment = new XComment(LocalFilterComment); + + doc = new XDocument(comment, new XElement("Filters")); + + try + { + using var writer = XmlWriter.Create(SpamServerFilter.SavePath, new XmlWriterSettings { Indent = true }); + doc.SaveSafe(writer); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving spam filter failed.", e); + } + } + else + { + doc = XMLExtensions.TryLoadXml(SpamServerFilter.SavePath); + } + + if (doc?.Root is { } root) + { + LocalSpamFilter = Option.Some(new SpamServerFilter(root)); + } + } + + public static bool IsFiltered(ServerInfo info) + { + if (LocalSpamFilter.TryUnwrap(out var localFilter) && localFilter.IsFiltered(info)) { return true; } + if (GlobalSpamFilter.TryUnwrap(out var globalFilter) && globalFilter.IsFiltered(info)) { return true; } + return false; + } + + public static void AddServerToLocalSpamList(ServerInfo info) + { + if (!LocalSpamFilter.TryUnwrap(out var localFilter)) { return; } + if (localFilter.IsFiltered(info)) { return; } + + var filters = localFilter.Filters.Add(new SpamFilter(ImmutableHashSet.Create((NameExact: SpamServerFilterType.NameEquals, info.ServerName)))); + var newFilter = new SpamServerFilter(filters); + newFilter.Save(SpamServerFilter.SavePath); + LocalSpamFilter = Option.Some(newFilter); + } + + public static void ClearLocalSpamFilter() + { + var newFilter = new SpamServerFilter(ImmutableArray.Empty); + newFilter.Save(SpamServerFilter.SavePath); + LocalSpamFilter = Option.Some(newFilter); + } + + public static void RequestGlobalSpamFilter() + { + if (GameSettings.CurrentConfig.DisableGlobalSpamList) { return; } + + string remoteContentUrl = GameSettings.CurrentConfig.RemoteMainMenuContentUrl; + if (string.IsNullOrEmpty(remoteContentUrl)) { return; } + + try + { + var client = new RestClient($"{remoteContentUrl}spamfilter") + { + CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore) + }; + client.AddDefaultHeader("Cache-Control", "no-cache"); + client.AddDefaultHeader("Pragma", "no-cache"); + var request = new RestRequest("serve_spamlist.php", Method.GET); + TaskPool.Add("RequestGlobalSpamFilter", client.ExecuteAsync(request), RemoteContentReceived); + } + catch (Exception e) + { +#if DEBUG + DebugConsole.ThrowError("Fetching global spam list failed.", e); +#endif + GameAnalyticsManager.AddErrorEventOnce("SpamServerFilters.RequestGlobalSpamFilter:Exception", GameAnalyticsManager.ErrorSeverity.Error, + "Fetching global spam list failed. " + e.Message); + } + + static void RemoteContentReceived(Task t) + { + try + { + if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } + if (remoteContentResponse.StatusCode != HttpStatusCode.OK) + { + DebugConsole.AddWarning( + "Failed to receive global spam filter." + + "There may be an issue with your internet connection, or the master server might be temporarily unavailable " + + $"(error code: {remoteContentResponse.StatusCode})"); + return; + } + string data = remoteContentResponse.Content; + if (string.IsNullOrWhiteSpace(data)) { return; } + + if (XDocument.Parse(data).Root is { } root) + { + GlobalSpamFilter = Option.Some(new SpamServerFilter(root)); + } + } + catch (Exception e) + { +#if DEBUG + DebugConsole.ThrowError("Reading received global spam filter failed.", e); +#endif + GameAnalyticsManager.AddErrorEventOnce("SpamServerFilters.RemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error, + "Reading received global spam filter failed. " + e.Message); + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index e599e9c41..5314247ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -111,7 +111,7 @@ namespace Barotrauma partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn) { - texture = LoadTexture(FilePath.Value, Compress); + texture = LoadTexture(FilePath.Value, Compress, contentPackage: SourceElement?.ContentPackage); if (texture == null) { @@ -175,7 +175,7 @@ namespace Barotrauma return; } texture.Dispose(); - texture = TextureLoader.FromFile(FilePath.Value, Compress); + texture = TextureLoader.FromFile(FilePath.Value, Compress, contentPackage: SourceElement?.ContentPackage); Identifier pathKey = FullPath.ToIdentifier(); if (textureRefCounts.ContainsKey(pathKey)) { @@ -195,7 +195,7 @@ namespace Barotrauma sourceRect = new Rectangle(0, 0, texture.Width, texture.Height); } - public static Texture2D LoadTexture(string file, bool compress = true) + public static Texture2D LoadTexture(string file, bool compress = true, ContentPackage contentPackage = null) { if (string.IsNullOrWhiteSpace(file)) { @@ -221,11 +221,11 @@ namespace Barotrauma if (!ToolBox.IsProperFilenameCase(file)) { #if DEBUG - DebugConsole.ThrowError("Texture file \"" + file + "\" has incorrect case!"); + DebugConsole.ThrowError("Texture file \"" + file + "\" has incorrect case!", contentPackage: contentPackage); #endif } - Texture2D newTexture = TextureLoader.FromFile(file, compress); + Texture2D newTexture = TextureLoader.FromFile(file, compress, contentPackage: contentPackage); lock (list) { if (!textureRefCounts.TryAdd(fullPath, @@ -284,17 +284,44 @@ namespace Barotrauma } } - public void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, - Color? color = null, Vector2? startOffset = null, Vector2? textureScale = null, float? depth = null) + public void DrawTiled(ISpriteBatch spriteBatch, + Vector2 position, + Vector2 targetSize, + float rotation = 0f, + Vector2? origin = null, + Color? color = null, + Vector2? startOffset = null, + Vector2? textureScale = null, + float? depth = null, + SpriteEffects? spriteEffects = null) { if (Texture == null) { return; } + + spriteEffects ??= effects; + bool flipHorizontal = (spriteEffects.Value & SpriteEffects.FlipHorizontally) != 0; + bool flipVertical = (spriteEffects.Value & SpriteEffects.FlipVertically) != 0; + + float addedRotation = rotation + this.rotation; + if (flipHorizontal != flipVertical) { addedRotation = -addedRotation; } + + Vector2 advanceX = addedRotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(addedRotation), (float)Math.Sin(addedRotation)); + Vector2 advanceY = new Vector2(-advanceX.Y, advanceX.X); + //Init optional values Vector2 drawOffset = startOffset ?? Vector2.Zero; Vector2 scale = textureScale ?? Vector2.One; Color drawColor = color ?? Color.White; + Vector2 transformedOrigin = origin ?? Vector2.Zero; - bool flipHorizontal = (effects & SpriteEffects.FlipHorizontally) != 0; - bool flipVertical = (effects & SpriteEffects.FlipVertically) != 0; + transformedOrigin = advanceX * transformedOrigin.X + advanceY * transformedOrigin.Y; + + void drawSection(Vector2 slicePos, Rectangle sliceRect) + { + Vector2 transformedPos = slicePos - position; + transformedPos = advanceX * transformedPos.X + advanceY * transformedPos.Y; + transformedPos += position - transformedOrigin; + spriteBatch.Draw(texture, transformedPos, sliceRect, drawColor, addedRotation, Vector2.Zero, scale, spriteEffects.Value, depth ?? this.depth); + } //wrap the drawOffset inside the sourceRect drawOffset.X = (drawOffset.X / scale.X) % sourceRect.Width; @@ -368,8 +395,8 @@ namespace Barotrauma { slicePos.Y += flippedDrawOffset.Y; } - - spriteBatch.Draw(texture, slicePos, sliceRect, drawColor, rotation, Vector2.Zero, scale, effects, depth ?? this.depth); + + drawSection(slicePos, sliceRect); currDrawPosition.X = slicePos.X + sliceWidth; } } @@ -416,7 +443,7 @@ namespace Barotrauma sliceRect.Y = SourceRect.Y; sliceRect.Height = (int)(sliceHeight / scale.Y); - spriteBatch.Draw(texture, slicePos, sliceRect, drawColor, rotation, Vector2.Zero, scale, effects, depth ?? this.depth); + drawSection(slicePos, sliceRect); currDrawPosition.Y = slicePos.Y + sliceHeight; } @@ -433,8 +460,7 @@ namespace Barotrauma } } - spriteBatch.Draw(texture, currDrawPosition, - texPerspective, drawColor, rotation, Vector2.Zero, scale, effects, depth ?? this.depth); + drawSection(currDrawPosition, texPerspective); currDrawPosition.Y += texPerspective.Height * scale.Y; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 41be59e49..3eb48d01a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -210,7 +210,7 @@ namespace Barotrauma statusEffect.soundChannel.FadeOutAndDispose(); statusEffect.soundChannel = null; } - else + else if (statusEffect.soundEmitter is { Removed: false }) { statusEffect.soundChannel.Position = new Vector3(statusEffect.soundEmitter.WorldPosition, 0.0f); if (doMuffleCheck && !statusEffect.ignoreMuffling) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs index 264cf243f..e42bf4dd3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -101,13 +101,36 @@ namespace Barotrauma.Steam { Color = GUIStyle.Green }; + var textShadow = new GUITextBlock(new RectTransform(Vector2.One, itemDownloadProgress.RectTransform) { AbsoluteOffset = new Point(GUI.IntScale(3)) }, "", + textColor: Color.Black, textAlignment: Alignment.Center); + var text = new GUITextBlock(new RectTransform(Vector2.One, itemDownloadProgress.RectTransform), "", + textAlignment: Alignment.Center); var itemDownloadProgressUpdater = new GUICustomComponent( new RectTransform(Vector2.Zero, msgBox.Content.RectTransform), onUpdate: (f, component) => { float progress = 0.0f; - if (item.IsDownloading) { progress = item.DownloadAmount; } - else if (itemDownloadProgress.BarSize > 0.0f) { progress = 1.0f; } + if (item.IsDownloading) + { + progress = item.DownloadAmount; + text.Text = textShadow.Text = TextManager.GetWithVariable( + "PublishPopupDownload", + "[percentage]", + ((int)MathF.Round(item.DownloadAmount * 100)).ToString()); + } + else if (itemDownloadProgress.BarSize > 0.0f) + { + if (!item.IsInstalled && !SteamManager.Workshop.CanBeInstalled(item.Id)) + { + itemDownloadProgress.Color = GUIStyle.Red; + text.Text = textShadow.Text = TextManager.Get("workshopiteminstallfailed"); + } + else + { + text.Text = textShadow.Text = TextManager.Get(item.IsInstalled ? "workshopiteminstalled" : "PublishPopupInstall"); + } + progress = 1.0f; + } itemDownloadProgress.BarSize = Math.Max(itemDownloadProgress.BarSize, MathHelper.Lerp(itemDownloadProgress.BarSize, progress, 0.1f)); @@ -134,9 +157,16 @@ namespace Barotrauma.Steam { foreach (var item in itemsToDownload) { + DebugConsole.Log($"Reinstalling {item.Title}..."); await SteamManager.Workshop.Reinstall(item); - if (!GUIMessageBox.MessageBoxes.Contains(msgBox)) { break; } + DebugConsole.Log($"Finished installing {item.Title}..."); + if (!GUIMessageBox.MessageBoxes.Contains(msgBox)) + { + DebugConsole.Log($"Download prompt closed, interrupting {nameof(DownloadItems)}."); + break; + } } + DebugConsole.Log($"{nameof(DownloadItems)} finished."); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 2748b691c..90c86e709 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -120,6 +120,10 @@ namespace Barotrauma.Steam currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? ""); currentLobby?.SetData("language", serverSettings.Language.ToString()); + if (GameMain.NetLobbyScreen?.SelectedSub != null) + { + currentLobby?.SetData("submarine", GameMain.NetLobbyScreen.SelectedSub.Name); + } DebugConsole.Log("Lobby updated!"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 0159decbd..dd21a0e2a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -298,7 +298,7 @@ namespace Barotrauma.Steam public static void OnItemDownloadComplete(ulong id, bool forceInstall = false) { - if (!(Screen.Selected is MainMenuScreen) && !forceInstall) + if (Screen.Selected is not MainMenuScreen && !forceInstall) { if (!MainMenuScreen.WorkshopItemsToUpdate.Contains(id)) { @@ -306,13 +306,26 @@ namespace Barotrauma.Steam } return; } - else if (CanBeInstalled(id) - && !ContentPackageManager.WorkshopPackages.Any(p => + else if (!CanBeInstalled(id)) + { + DebugConsole.Log($"Cannot install {id}"); + InstallWaiter.StopWaiting(id); + } + else if (ContentPackageManager.WorkshopPackages.Any(p => p.UgcId.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId workshopId - && workshopId.Value == id) - && !InstallTaskCounter.IsInstalling(id)) + && workshopId.Value == id)) { + DebugConsole.Log($"Already installed {id}."); + InstallWaiter.StopWaiting(id); + } + else if (InstallTaskCounter.IsInstalling(id)) + { + DebugConsole.Log($"Already installing {id}."); + } + else + { + DebugConsole.Log($"Finished downloading {id}, installing..."); TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs index 39f998cef..50a3f5f90 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Xml.Linq; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; @@ -116,6 +117,9 @@ namespace Barotrauma private readonly bool WasDeleted; private readonly List ContainedItemsCommand = new List(); + // We need to 'snapshot' the state of the circuit box and the best way to do that is to save it to XML. + private readonly List CircuitBoxData = new List(); + /// /// Creates a command where all entities share the same state. /// @@ -143,13 +147,17 @@ namespace Barotrauma List itemsToDelete = new List(); foreach (MapEntity receiver in Receivers) { - if (receiver is Item it) + if (receiver is not Item it) { continue; } + + foreach (var cb in it.GetComponents()) { - foreach (ItemContainer component in it.GetComponents()) - { - if (component.Inventory == null) { continue; } - itemsToDelete.AddRange(component.Inventory.AllItems.Where(item => !item.Removed)); - } + CircuitBoxData.Add(cb.Save(new XElement("root"))); + } + + foreach (ItemContainer component in it.GetComponents()) + { + if (component.Inventory == null) { continue; } + itemsToDelete.AddRange(component.Inventory.AllItems.Where(static item => !item.Removed)); } } @@ -192,34 +200,50 @@ namespace Barotrauma } public override void Execute() - { - var items = DeleteUndelete(true); - ContainedItemsCommand?.ForEach(static cmd => cmd.Execute()); - CircuitBoxWorkaround(items); - } + => Process(true); public override void UnExecute() + => Process(false); + + private void Process(bool redo) { - var items = DeleteUndelete(false); - ContainedItemsCommand?.ForEach(static cmd => cmd.UnExecute()); - CircuitBoxWorkaround(items); + var items = DeleteUndelete(redo); + foreach (var cmd in ContainedItemsCommand) + { + cmd.Process(redo); + } + ApplyCircuitBoxDataIfAny(items); } - // FIXME Temporary workaround for circuit boxes throwing console errors and breaking completely when undoing a deletion - private static void CircuitBoxWorkaround(Option> entitiesOption) + /// + /// We need to manually copy over the circuit box data because of how the undo handles inventory items. + /// The undo system recursively deletes inventory items and creates a separate command for each one. + /// This causes the circuit box to lose its internal inventory when it's cloned and then restored and make it + /// unable to load the state from XML. + /// + /// The workaround to this is to ignore the XML that is being loaded when the item is created and instead + /// save the XML into the command and then load it back after the undo system has restored the items which + /// is what this function does. + /// + private void ApplyCircuitBoxDataIfAny(ImmutableArray items) { - if (!entitiesOption.TryUnwrap(out var entities)) { return; } - - foreach (var entity in entities) + int cbIndex = 0; + foreach (var newItem in items) { - if (entity is not Item it) { continue; } - - if (it.GetComponent() is not null) + foreach (ItemComponent component in newItem.Components) { - foreach (var container in it.GetComponents()) + if (component is not CircuitBox cb) { continue; } + + if (cbIndex < 0 || cbIndex >= CircuitBoxData.Count) { - container.Inventory.DeleteAllItems(); + DebugConsole.ThrowError("Unable to restore wiring in circuit box, index out of range."); + continue; } + + var cbData = CircuitBoxData[cbIndex]; + cbIndex++; + + cb.LoadFromXML(new ContentXElement(null, cbData)); } } } @@ -238,15 +262,19 @@ namespace Barotrauma Receivers.Clear(); PreviousInventories?.Clear(); ContainedItemsCommand?.ForEach(static cmd => cmd.Cleanup()); + CircuitBoxData.Clear(); } - private Option> DeleteUndelete(bool redo) + private ImmutableArray DeleteUndelete(bool redo) { bool wasDeleted = WasDeleted; // We are redoing instead of undoing, flip the behavior if (redo) { wasDeleted = !wasDeleted; } + // collect newly created items so we can update their circuit boxes if any + var builder = ImmutableArray.CreateBuilder(); + if (wasDeleted) { Debug.Assert(Receivers.All(static entity => entity.GetReplacementOrThis().Removed), "Tried to redo a deletion but some items were not deleted"); @@ -260,6 +288,7 @@ namespace Barotrauma if (receiver.GetReplacementOrThis() is Item item && clone is Item cloneItem) { + builder.Add(cloneItem); foreach (ItemComponent ic in item.Components) { int index = item.GetComponentIndex(ic); @@ -303,7 +332,7 @@ namespace Barotrauma clone.Submarine = Submarine.MainSub; } - return Option.Some(clones.ToImmutableArray()); + return builder.ToImmutable(); } else { @@ -316,7 +345,7 @@ namespace Barotrauma } } - return Option.None; + return builder.ToImmutable(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/GraphicsQuad.cs similarity index 98% rename from Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs rename to Barotrauma/BarotraumaClient/ClientSource/Utils/GraphicsQuad.cs index d14a9a885..5f2461316 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/GraphicsQuad.cs @@ -6,7 +6,7 @@ using System.Text; namespace Barotrauma { - static class Quad + static class GraphicsQuad { private static VertexBuffer vertexBuffer = null; private static IndexBuffer indexBuffer = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs index f7c0b35fc..063a2e634 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -35,7 +35,7 @@ namespace Barotrauma Vector2 pos, Rectangle srcRect, Color color, - float rotation, + float rotationRad, Vector2 origin, Vector2 scale, SpriteEffects effects, @@ -55,9 +55,8 @@ namespace Barotrauma (srcRectBottom, srcRectTop) = (srcRectTop, srcRectBottom); } - rotation = MathHelper.ToRadians(rotation); - float sin = (float)Math.Sin(rotation); - float cos = (float)Math.Cos(rotation); + float sin = (float)Math.Sin(rotationRad); + float cos = (float)Math.Cos(rotationRad); var size = srcRect.Size.ToVector2() * scale; @@ -183,11 +182,11 @@ namespace Barotrauma commandList.Add(command); } - public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) + public void Draw(Texture2D texture, Vector2 pos, Rectangle? srcRect, Color color, float rotationRad, Vector2 origin, Vector2 scale, SpriteEffects effects, float depth) { if (isDisposed) { return; } - var command = Command.FromTransform(texture, pos, srcRect ?? texture.Bounds, color, rotation, origin, scale, effects, depth, commandList.Count); + var command = Command.FromTransform(texture, pos, srcRect ?? texture.Bounds, color, rotationRad, origin, scale, effects, depth, commandList.Count); AppendCommand(command); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs index 1fcbab5fb..3ab6a1a90 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs @@ -151,13 +151,13 @@ namespace Barotrauma output[outputOffset + 10] = (byte)((g2_565 << 5) | b2_565); } - public static Texture2D FromFile(string path, bool compress = true, bool mipmap = false) + public static Texture2D FromFile(string path, bool compress = true, bool mipmap = false, ContentPackage contentPackage = null) { using FileStream fileStream = File.OpenRead(path); - return FromStream(fileStream, path, compress, mipmap); + return FromStream(fileStream, path, compress, mipmap, contentPackage); } - public static Texture2D FromStream(System.IO.Stream stream, string path = null, bool compress = true, bool mipmap = false) + public static Texture2D FromStream(System.IO.Stream stream, string path = null, bool compress = true, bool mipmap = false, ContentPackage contentPackage = null) { try { @@ -176,7 +176,8 @@ namespace Barotrauma } else { - DebugConsole.AddWarning($"Could not compress a texture because the dimensions aren't a multiple of 4 (path: {path ?? "null"}, size: {width}x{height})"); + DebugConsole.AddWarning($"Could not compress a texture because the dimensions aren't a multiple of 4 (path: {path ?? "null"}, size: {width}x{height})", + contentPackage); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index c89d0cbfb..7b13a797d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -76,7 +76,7 @@ namespace Barotrauma float zoom = (float)texWidth / (float)boundingBox.Width; int texHeight = (int)(zoom * boundingBox.Height); - using Camera cam = new Camera(); + Camera cam = new Camera(); cam.SetResolution(new Point(texWidth, texHeight)); cam.MaxZoom = zoom; cam.MinZoom = zoom * 0.5f; diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index a5b67175d..cba9e2ea5 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 12fa0df4c..8694f9367 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index a469892ba..0f7aacf16 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 7b072552b..e983630ac 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 16781acf4..56fca5e74 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 0f0014017..fc368dace 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -80,7 +80,9 @@ namespace Barotrauma { msg.WriteUInt32(Job.Prefab.UintIdentifier); msg.WriteByte((byte)Job.Variant); - foreach (SkillPrefab skillPrefab in Job.Prefab.Skills.OrderBy(s => s.Identifier)) + var skills = Job.Prefab.Skills.OrderBy(s => s.Identifier); + msg.WriteByte((byte)skills.Count()); + foreach (SkillPrefab skillPrefab in skills) { msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier)?.Level ?? 0.0f); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index fd59111ec..aebcc26a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -34,8 +34,8 @@ namespace Barotrauma { if (!CheatsEnabled && IsCheat) { - NewMessage("Client \"" + client.Name + "\" attempted to use the command \"" + names[0] + "\". Cheats must be enabled using \"enablecheats\" before the command can be used.", Color.Red); - GameMain.Server.SendConsoleMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", client, Color.Red); + NewMessage("Client \"" + client.Name + "\" attempted to use the command \"" + Names[0] + "\". Cheats must be enabled using \"enablecheats\" before the command can be used.", Color.Red); + GameMain.Server.SendConsoleMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + Names[0] + "\".", client, Color.Red); #if USE_STEAM NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red); @@ -317,7 +317,7 @@ namespace Barotrauma private static void AssignOnClientRequestExecute(string names, Action onClientRequestExecute) { - var matchingCommand = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0); + var matchingCommand = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any()); if (matchingCommand == null) { throw new Exception("AssignOnClientRequestExecute failed. Command matching the name(s) \"" + names + "\" not found."); @@ -654,8 +654,10 @@ namespace Barotrauma ShowQuestionPrompt("Console command permissions to grant to \"" + client.Name + "\"? You may enter multiple commands separated with a space, or \"all\" to allow using any console command.", (commandsStr) => { - string[] splitCommands = commandsStr.Split(' '); - bool giveAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + Identifier[] splitCommands = commandsStr.Split(' ') + .Select(s => s.Trim()) + .ToIdentifiers().ToArray(); + bool giveAll = splitCommands.Length > 0 && splitCommands[0] == "all"; List grantedCommands = new List(); if (giveAll) @@ -664,13 +666,12 @@ namespace Barotrauma } else { - for (int i = 0; i < splitCommands.Length; i++) + foreach (Identifier command in splitCommands) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + Command matchingCommand = commands.Find(c => c.Names.Contains(command)); if (matchingCommand == null) { - ThrowError("Could not find the command \"" + splitCommands[i] + "\"!"); + ThrowError("Could not find the command \"" + command + "\"!"); } else { @@ -688,7 +689,7 @@ namespace Barotrauma } else if (grantedCommands.Count > 0) { - NewMessage("Gave the client \"" + client.Name + "\" the permission to use console commands " + string.Join(", ", grantedCommands.Select(c => c.names[0])) + ".", Color.White); + NewMessage("Gave the client \"" + client.Name + "\" the permission to use console commands " + string.Join(", ", grantedCommands.Select(c => c.Names[0])) + ".", Color.White); } }, args, 1); @@ -717,22 +718,23 @@ namespace Barotrauma ShowQuestionPrompt("Console command permissions to revoke from \"" + client.Name + "\"? You may enter multiple commands separated with a space.", (commandsStr) => { - string[] splitCommands = commandsStr.Split(' '); + Identifier[] splitCommands = commandsStr.Split(' ') + .Select(s => s.Trim()) + .ToIdentifiers().ToArray(); List revokedCommands = new List(); - bool revokeAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + bool revokeAll = splitCommands.Length > 0 && splitCommands[0] == "all"; if (revokeAll) { revokedCommands.AddRange(commands); } else { - for (int i = 0; i < splitCommands.Length; i++) + foreach (Identifier command in splitCommands) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + Command matchingCommand = commands.Find(c => c.Names.Contains(command)); if (matchingCommand == null) { - ThrowError("Could not find the command \"" + splitCommands[i] + "\"!"); + ThrowError("Could not find the command \"" + command + "\"!"); } else { @@ -749,7 +751,7 @@ namespace Barotrauma } else if (revokedCommands.Any()) { - NewMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", Color.White); + NewMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.Names[0])) + ".", Color.White); } }, args, 1); }); @@ -793,7 +795,7 @@ namespace Barotrauma NewMessage("Permitted console commands:", Color.White); foreach (Command permittedCommand in client.PermittedConsoleCommands) { - NewMessage(" - " + permittedCommand.names[0], Color.White); + NewMessage(" - " + permittedCommand.Names[0], Color.White); } } } @@ -1132,7 +1134,12 @@ namespace Barotrauma createMessage("Traitors:"); foreach (var ev in traitorManager.ActiveEvents) { - createMessage($" - {ev.Traitor.Name}: {ev.TraitorEvent.Prefab.Identifier} ({ev.TraitorEvent.CurrentState})"); + string msg = $" - {ev.TraitorEvent.Prefab.Identifier} ({ev.TraitorEvent.CurrentState}): {ev.Traitor.Name}"; + if (ev.TraitorEvent.SecondaryTraitors.Any()) + { + msg += $" secondary traitors: {string.Join(", ", ev.TraitorEvent.SecondaryTraitors.Select(t => t.Name))}"; + } + createMessage(msg); } } @@ -1156,6 +1163,23 @@ namespace Barotrauma } ); + commands.Add(new Command("debugjobassignment", "debugjobassignment: Shows information about how jobs were assigned for the most recent round.", (string[] args) => + { + if (GameMain.Server == null) { return; } + foreach (var debugMsg in GameMain.Server.JobAssignmentDebugLog) + { + NewMessage(debugMsg, Color.Cyan); + } + })); + AssignOnClientRequestExecute("debugjobassignment", (Client client, Vector2 cursorWorldPos, string[] args) => + { + if (GameMain.Server == null) { return; } + foreach (var debugMsg in GameMain.Server.JobAssignmentDebugLog) + { + GameMain.Server.SendConsoleMessage(debugMsg, client); + } + }); + commands.Add(new Command("setpassword|setserverpassword|password", "setpassword [password]: Changes the password of the server that's being hosted.", (string[] args) => { if (GameMain.Server == null) { return; } @@ -1397,7 +1421,7 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath, client: null); } else { @@ -1432,7 +1456,6 @@ namespace Barotrauma GameMain.Server.PrintSenderTransters(); })); - commands.Add(new Command("forcelocationtypechange", "", (string[] args) => { if (GameMain.Server == null || GameMain.GameSession?.Campaign == null) { return; } @@ -1443,7 +1466,7 @@ namespace Barotrauma return; } - var location = GameMain.GameSession.Campaign.Map.Locations.FirstOrDefault(l => l.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + var location = GameMain.GameSession.Campaign.Map.Locations.FirstOrDefault(l => l.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (location == null) { ThrowError($"Could not find a location with the name {args[0]}."); @@ -1466,7 +1489,7 @@ namespace Barotrauma return new string[][] { - GameMain.GameSession.Campaign.Map.Locations.Select(l => l.Name).ToArray(), + GameMain.GameSession.Campaign.Map.Locations.Select(l => l.DisplayName.Value).ToArray(), LocationType.Prefabs.Select(lt => lt.Name.Value).ToArray() }; })); @@ -1568,6 +1591,19 @@ namespace Barotrauma GameMain.Server.SendChatMessage(ToolBox.RandomSeed(msgLength), ChatMessageType.Default); } })); + + commands.Add(new Command("multiclienttestmode", "Makes the server assign campaign characters based on the name of the client and the character, as opposed to just checking the account ID or address. Useful for testing the campaign with multiple clients running locally.", (string[] args) => + { + CharacterCampaignData.RequireClientNameMatch = !CharacterCampaignData.RequireClientNameMatch; + if (CharacterCampaignData.RequireClientNameMatch) + { + NewMessage("Enabled RequireClientNameMatch (clients' names must match their campaign character)"); + } + else + { + NewMessage("Disabled RequireClientNameMatch"); + } + })); #endif AssignOnClientRequestExecute( @@ -1751,17 +1787,32 @@ namespace Barotrauma { Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); } - else + else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase)) { Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); } + else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase)) + { + Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); + var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub); + if (Level.Loaded?.EndOutpost == null) + { + NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red); + return; + } + var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost); + if (submarineDockingPort != null && outpostDockingPort != null) + { + submarineDockingPort.Dock(outpostDockingPort); + } + } } ); AssignOnClientRequestExecute("togglecampaignteleport", (Client client, Vector2 cursorWorldPos, string[] args) => { - if (!(GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)) + if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign mpCampaign) { GameMain.Server.SendConsoleMessage("No campaign active.", client, Color.Red); return; @@ -2171,21 +2222,21 @@ namespace Barotrauma } List grantedCommands = new List(); - string[] splitCommands = args.Skip(1).ToArray(); - bool giveAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + Identifier[] splitCommands = args.Skip(1) + .Select(s => s.Trim()).ToIdentifiers().ToArray(); + bool giveAll = splitCommands.Length > 0 && splitCommands[0] == "all"; if (giveAll) { grantedCommands.AddRange(commands); } else { - for (int i = 0; i < splitCommands.Length; i++) + foreach (Identifier command in splitCommands) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + Command matchingCommand = commands.Find(c => c.Names.Contains(command)); if (matchingCommand == null) { - GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient, Color.Red); + GameMain.Server.SendConsoleMessage("Could not find the command \"" + command + "\"!", senderClient, Color.Red); } else { @@ -2204,7 +2255,7 @@ namespace Barotrauma } else if (grantedCommands.Count > 0) { - GameMain.Server.SendConsoleMessage("Gave the client \"" + client.Name + "\" the permission to use console commands " + string.Join(", ", grantedCommands.Select(c => c.names[0])) + ".", senderClient); + GameMain.Server.SendConsoleMessage("Gave the client \"" + client.Name + "\" the permission to use console commands " + string.Join(", ", grantedCommands.Select(c => c.Names[0])) + ".", senderClient); } } ); @@ -2227,21 +2278,21 @@ namespace Barotrauma return; } List revokedCommands = new List(); - string[] splitCommands = args.Skip(1).ToArray(); - bool revokeAll = splitCommands.Length > 0 && splitCommands[0].Equals("all", StringComparison.OrdinalIgnoreCase); + Identifier[] splitCommands = args.Skip(1) + .Select(s => s.Trim()).ToIdentifiers().ToArray(); + bool revokeAll = splitCommands.Length > 0 && splitCommands[0] == "all"; if (revokeAll) { revokedCommands.AddRange(commands); } else { - for (int i = 0; i < splitCommands.Length; i++) + foreach (Identifier command in splitCommands) { - splitCommands[i] = splitCommands[i].Trim().ToLowerInvariant(); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommands[i])); + Command matchingCommand = commands.Find(c => c.Names.Contains(command)); if (matchingCommand == null) { - GameMain.Server.SendConsoleMessage("Could not find the command \"" + splitCommands[i] + "\"!", senderClient, Color.Red); + GameMain.Server.SendConsoleMessage("Could not find the command \"" + command + "\"!", senderClient, Color.Red); } else { @@ -2256,14 +2307,14 @@ namespace Barotrauma client.RemovePermission(ClientPermissions.ConsoleCommands); } GameMain.Server.UpdateClientPermissions(client); - GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", senderClient); + GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.Names[0])) + ".", senderClient); if (revokeAll) { GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use console commands.", senderClient); } else if (revokedCommands.Count > 0) { - GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.names[0])) + ".", senderClient); + GameMain.Server.SendConsoleMessage("Revoked \"" + client.Name + "\"'s permission to use the console commands " + string.Join(", ", revokedCommands.Select(c => c.Names[0])) + ".", senderClient); } } ); @@ -2308,7 +2359,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("Permitted console commands:", senderClient); foreach (Command permittedCommand in client.PermittedConsoleCommands) { - GameMain.Server.SendConsoleMessage(" - " + permittedCommand.names[0], senderClient); + GameMain.Server.SendConsoleMessage(" - " + permittedCommand.Names[0], senderClient); } } } @@ -2411,7 +2462,7 @@ namespace Barotrauma } Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation); campaign.Map.SelectLocation(location); - GameMain.Server.SendConsoleMessage(location.Name + " selected.", senderClient); + GameMain.Server.SendConsoleMessage($"{location.DisplayName.Value} selected.", senderClient); } ); @@ -2585,10 +2636,10 @@ namespace Barotrauma } string[] splitCommand = ToolBox.SplitCommand(command); - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0].ToLowerInvariant())); + Command matchingCommand = commands.Find(c => c.Names.Contains(splitCommand[0].ToIdentifier())); if (matchingCommand != null && !client.PermittedConsoleCommands.Contains(matchingCommand) && client.Connection != GameMain.Server.OwnerConnection) { - GameMain.Server.SendConsoleMessage("You are not permitted to use the command\"" + matchingCommand.names[0] + "\"!", client, Color.Red); + GameMain.Server.SendConsoleMessage("You are not permitted to use the command\"" + matchingCommand.Names[0] + "\"!", client, Color.Red); GameServer.Log(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" without a permission to use the command.", ServerLog.MessageType.ConsoleUsage); return; } @@ -2612,14 +2663,14 @@ namespace Barotrauma } catch (Exception e) { - ThrowError("Executing the command \"" + matchingCommand.names[0] + "\" by request from \"" + GameServer.ClientLogName(client) + "\" failed.", e); + ThrowError("Executing the command \"" + matchingCommand.Names[0] + "\" by request from \"" + GameServer.ClientLogName(client) + "\" failed.", e); } } static partial void ShowHelpMessage(Command command) { - NewMessage(command.names[0], Color.Cyan); - NewMessage(command.help, Color.Gray); + NewMessage(command.Names[0].Value, Color.Cyan); + NewMessage(command.Help, Color.Gray); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs index 976984e76..86d0297f6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs @@ -27,10 +27,11 @@ partial class EventLogAction : EventAction } else { - DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character."); + DebugConsole.AddWarning($"{target} is not a valid target for an EventLogAction. The target should be a character.", + ParentEvent.Prefab.ContentPackage); } } - if (eventLog.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog) + if (eventLog!.TryAddEntry(ParentEvent.Prefab.Identifier, Id, displayText, targetClients) && ShowInServerLog) { Log(targetClients); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs new file mode 100644 index 000000000..98c06c3f9 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/HighlightAction.cs @@ -0,0 +1,24 @@ +#nullable enable +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +partial class HighlightAction : EventAction +{ + partial void SetHighlightProjSpecific(Entity entity, IEnumerable? targetCharacters) + { + if (entity is Item item && GameMain.Server != null) + { + IEnumerable? targetClients = null; + if (targetCharacters != null) + { + targetClients = targetCharacters + .Select(c => GameMain.Server.ConnectedClients.FirstOrDefault(client => client.Character == c)) + .Where(c => c != null)!; + } + GameMain.Server?.CreateEntityEvent(item, new Item.SetHighlightEventData(State, highlightColor, targetClients)); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs index 64f8f99b6..456037115 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs @@ -97,7 +97,13 @@ namespace Barotrauma void GiveMissionExperience(CharacterInfo info) { if (info == null) { return; } - var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f, info.Character); + //check if anyone else in the crew has talents that could give a bonus to this one + foreach (var c in crew) + { + if (c == info.Character) { continue; } + c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplierIndividual); + } info.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); int finalExperienceGain = (int)(experienceGain * experienceGainMultiplierIndividual.Value); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index d21e80fbd..7b2abfa96 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -118,26 +118,13 @@ namespace Barotrauma private void CheckContentPackage() { - //TODO: reimplement using only core package? - /*foreach (ContentPackage contentPackage in Config.AllEnabledPackages) + if (Version < VanillaContent.GameVersion) { - var exePaths = contentPackage.GetFilesOfType(ContentType.ServerExecutable); - if (exePaths.Count() > 0 && AppDomain.CurrentDomain.FriendlyName != exePaths.First()) - { - DebugConsole.NewMessage(AppDomain.CurrentDomain.FriendlyName); - DebugConsole.ShowQuestionPrompt(TextManager.GetWithVariables("IncorrectExe", new string[2] { "[selectedpackage]", "[exename]" }, new string[2] { contentPackage.Name, exePaths.First() }), - (option) => - { - if (option.ToLower() == "y" || option.ToLower() == "yes") - { - string fullPath = Path.GetFullPath(exePaths.First()); - ToolBox.OpenFileWithShell(fullPath); - ShouldRun = false; - } - }); - break; - } - }*/ + DebugConsole.ThrowError( + TextManager.GetWithVariables("versionmismatchwarning", + ("[gameversion]", Version.ToString()), + ("[contentversion]", VanillaContent.GameVersion.ToString()))); + } } public void StartServer() diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index 79597042e..f3737955a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -2,27 +2,12 @@ using System.Collections.Generic; using System.Linq; using Barotrauma.Networking; +using System.Text; namespace Barotrauma { partial class CargoManager { - public void SellBackPurchasedItems(Identifier storeIdentifier, List itemsToSell, Client client) - { - // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction - var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToSell.Select(i => i.ItemPrefab)); - var store = Location.GetStore(storeIdentifier); - if (store == null) { return; } - var storeSpecificItems = GetPurchasedItems(storeIdentifier); - foreach (var item in itemsToSell) - { - var itemValue = item.Quantity * buyValues[item.ItemPrefab]; - store.Balance -= itemValue; - campaign.GetWallet(client).Give(itemValue); - storeSpecificItems?.Remove(item); - } - } - public void BuyBackSoldItems(Identifier storeIdentifier, List itemsToBuy, Client client) { var store = Location.GetStore(storeIdentifier); @@ -80,6 +65,21 @@ namespace Barotrauma OnSoldItemsChanged?.Invoke(this); } + public void LogNewItemPurchases(Identifier storeIdentifier, List newItems, Client client) + { + StringBuilder sb = new StringBuilder(); + int price = 0; + Dictionary buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, newItems.Select(i => i.ItemPrefab)); + foreach (PurchasedItem item in newItems) + { + int itemValue = item.Quantity * buyValues[item.ItemPrefab]; + GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); + sb.Append($"\n - {item.ItemPrefab.Name} x{item.Quantity}"); + price += itemValue; + } + GameServer.Log($"{NetworkMember.ClientLogName(client, client?.Name ?? "Unknown")} purchased {newItems.Count} item(s) for {TextManager.FormatCurrency(price)}{sb.ToString()}", ServerLog.MessageType.Money); + } + public void ClearSoldItemsProjSpecific() { SoldItems.Clear(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index f3d53216a..7aa80a84e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -1,5 +1,4 @@ -using Barotrauma.Extensions; -using Barotrauma.Networking; +using Barotrauma.Networking; namespace Barotrauma { @@ -27,6 +26,15 @@ namespace Barotrauma AnyOneAllowedToManageCampaign(permissions); } + public static bool AllowImmediateItemDelivery(Client client) + { + if (client == null || GameMain.Server == null) { return false; } + return + GameMain.Server.ServerSettings.AllowImmediateItemDelivery || + client.HasPermission(ClientPermissions.ManageCampaign) || + client.Connection == GameMain.Server.OwnerConnection; + } + public static bool AllowedToManageWallets(Client client) { return AllowedToManageCampaign(client, ClientPermissions.ManageMoney); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index ddc0ae1d5..5b2daa59b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -6,6 +6,14 @@ namespace Barotrauma { partial class CharacterCampaignData { +#if DEBUG + /// + /// If enabled, client names must match the name of the character. Useful for testing the campaign with multiple clients running locally: + /// without this, the clients would all get assigned the same character due to all of them having the same AccountId or Address. + /// + public static bool RequireClientNameMatch = false; +#endif + public bool HasSpawned; public bool HasItemData @@ -76,7 +84,7 @@ namespace Barotrauma { case "character": case "characterinfo": - CharacterInfo = new CharacterInfo(subElement); + CharacterInfo = new CharacterInfo(new ContentXElement(contentPackage: null, subElement)); break; case "inventory": itemData = subElement; @@ -103,6 +111,12 @@ namespace Barotrauma } else { +#if DEBUG + if (RequireClientNameMatch) + { + return ClientAddress == client.Connection.Endpoint.Address && client.Name == Name; + } +#endif return ClientAddress == client.Connection.Endpoint.Address; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index e1b3de615..0081b16f2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -120,28 +120,41 @@ namespace Barotrauma SaveUtil.SaveGame(GameMain.GameSession.SavePath); DebugConsole.NewMessage("Campaign started!", Color.Cyan); - DebugConsole.NewMessage("Current location: " + GameMain.GameSession.Map.CurrentLocation.Name, Color.Cyan); + DebugConsole.NewMessage("Current location: " + GameMain.GameSession.Map.CurrentLocation.DisplayName, Color.Cyan); ((MultiPlayerCampaign)GameMain.GameSession.GameMode).LoadInitialLevel(); } - public static void LoadCampaign(string selectedSave) + public static void LoadCampaign(string selectedSave, Client client) { GameMain.NetLobbyScreen.ToggleCampaignMode(true); - SaveUtil.LoadGame(selectedSave); - if (GameMain.GameSession.GameMode is MultiPlayerCampaign mpCampaign) + try { - mpCampaign.LastSaveID++; + SaveUtil.LoadGame(selectedSave); + if (GameMain.GameSession.GameMode is MultiPlayerCampaign mpCampaign) + { + mpCampaign.LastSaveID++; + } + else + { + DebugConsole.ThrowError("Failed to load a campaign. Unexpected game mode: " + GameMain.GameSession.GameMode ?? "none"); + return; + } } - else + catch (Exception e) { - DebugConsole.ThrowError("Unexpected game mode: " + GameMain.GameSession.GameMode); + string errorMsg = $"Error while loading the save {selectedSave}"; + if (client != null) + { + GameMain.Server?.SendDirectChatMessage($"{errorMsg}: {e.Message}\n{e.StackTrace}", client, ChatMessageType.Error); + } + DebugConsole.ThrowError(errorMsg, e); return; } DebugConsole.NewMessage("Campaign loaded!", Color.Cyan); DebugConsole.NewMessage( GameMain.GameSession.Map.SelectedLocation == null ? - GameMain.GameSession.Map.CurrentLocation.Name : - GameMain.GameSession.Map.CurrentLocation.Name + " -> " + GameMain.GameSession.Map.SelectedLocation.Name, Color.Cyan); + GameMain.GameSession.Map.CurrentLocation.DisplayName : + GameMain.GameSession.Map.CurrentLocation.DisplayName + " -> " + GameMain.GameSession.Map.SelectedLocation.DisplayName, Color.Cyan); } protected override void LoadInitialLevel() @@ -188,7 +201,14 @@ namespace Barotrauma } else { - LoadCampaign(saveFiles[saveIndex].FilePath); + try + { + LoadCampaign(saveFiles[saveIndex].FilePath, client: null); + } + catch (Exception ex) + { + DebugConsole.ThrowError("Failed to load the campaign.", ex); + } } }); } @@ -377,7 +397,7 @@ namespace Barotrauma { PendingSubmarineSwitch = null; GameMain.Server.EndGame(TransitionType.None, wasSaved: false); - LoadCampaign(GameMain.GameSession.SavePath); + LoadCampaign(GameMain.GameSession.SavePath, client: null); LastSaveID++; IncrementAllLastUpdateIds(); yield return CoroutineStatus.Success; @@ -806,7 +826,7 @@ namespace Barotrauma UInt16 itemToRemoveID = msg.ReadUInt16(); Identifier itemToInstallIdentifier = msg.ReadIdentifier(); ItemPrefab itemToInstall = itemToInstallIdentifier.IsEmpty ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); - if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; } + if (Entity.FindEntityByID(itemToRemoveID) is not Item itemToRemove) { continue; } purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } @@ -894,7 +914,7 @@ namespace Barotrauma int availableQuantity = map.CurrentLocation.Stores[store.Key].Stock.Find(s => s.ItemPrefab == item.ItemPrefab)?.Quantity ?? 0; int alreadyPurchasedQuantity = CargoManager.GetBuyCrateItem(store.Key, item.ItemPrefab)?.Quantity ?? 0 + - CargoManager.GetPurchasedItem(store.Key, item.ItemPrefab)?.Quantity ?? 0; + CargoManager.GetPurchasedItemCount(store.Key, item.ItemPrefab); item.Quantity = MathHelper.Clamp(item.Quantity, 0, availableQuantity - alreadyPurchasedQuantity); CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender); } @@ -905,9 +925,41 @@ namespace Barotrauma { prevPurchasedItems.Add(kvp.Key, new List(kvp.Value)); } - foreach (var kvp in prevPurchasedItems) + + foreach (var storeId in purchasedItems.Keys) { - CargoManager.SellBackPurchasedItems(kvp.Key, kvp.Value, sender); + DebugConsole.Log($"Purchased items ({storeId}):\n"); + if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchased)) + { + var delivered = alreadyPurchased.Where(it => it.Delivered); + var notDelivered = alreadyPurchased.Where(it => !it.Delivered); + if (delivered.Any()) + { + DebugConsole.Log($" Already delivered:\n" + string.Concat(delivered.Select(it => $" - {it.ItemPrefab.Name} (x{it.Quantity})"))); + } + if (notDelivered.Any()) + { + DebugConsole.Log($" Already purchased:\n" + string.Concat(notDelivered.Where(it => !it.Delivered).Select(it => $" - {it.ItemPrefab.Name} (x{it.Quantity})"))); + } + } + DebugConsole.Log($" New purchases:"); + foreach (var purchasedItem in purchasedItems[storeId]) + { + if (purchasedItem.Delivered) { continue; } + int quantity = purchasedItem.Quantity; + if (alreadyPurchased != null) + { + quantity -= alreadyPurchased.Where(it => it.DeliverImmediately == purchasedItem.DeliverImmediately && it.ItemPrefab == purchasedItem.ItemPrefab).Sum(it => it.Quantity); + } + if (quantity > 0) + { + DebugConsole.Log($" - {purchasedItem.ItemPrefab.Name} (x{quantity})"); + } + } + } + foreach (var storeId in soldItems.Keys) + { + DebugConsole.Log($"Sold items:\n" + string.Concat(soldItems[storeId].Select(it => $" - {it.ItemPrefab.Name}"))); } foreach (var kvp in purchasedItems) @@ -916,17 +968,23 @@ namespace Barotrauma var purchasedItemList = kvp.Value; foreach (var purchasedItem in purchasedItemList) { + int desiredQuantity = purchasedItem.Quantity; + if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchasedList) && + alreadyPurchasedList.FirstOrDefault(p => p.ItemPrefab == purchasedItem.ItemPrefab) is { } alreadyPurchased) + { + desiredQuantity -= alreadyPurchased.Quantity; + } int availableQuantity = map.CurrentLocation.Stores[storeId].Stock.Find(s => s.ItemPrefab == purchasedItem.ItemPrefab)?.Quantity ?? 0; - purchasedItem.Quantity = Math.Min(purchasedItem.Quantity, availableQuantity); - } - CargoManager.PurchaseItems(storeId, purchasedItemList, false, sender); + purchasedItem.Quantity = Math.Min(desiredQuantity, availableQuantity); + } + CargoManager.PurchaseItems(storeId, purchasedItemList, removeFromCrate: false, client: sender); } foreach (var (storeIdentifier, items) in CargoManager.PurchasedItems) { if (!prevPurchasedItems.ContainsKey(storeIdentifier)) { - CargoManager.OnNewItemsPurchased(storeIdentifier, items, sender); + CargoManager.LogNewItemPurchases(storeIdentifier, items, sender); continue; } @@ -941,7 +999,6 @@ namespace Barotrauma newItems.Add(item); continue; } - if (matching.Quantity < item.Quantity) { newItems.Add(new PurchasedItem(item.ItemPrefab, item.Quantity - matching.Quantity, sender)); @@ -950,7 +1007,7 @@ namespace Barotrauma if (newItems.Any()) { - CargoManager.OnNewItemsPurchased(storeIdentifier, newItems, sender); + CargoManager.LogNewItemPurchases(storeIdentifier, newItems, sender); } } @@ -1015,7 +1072,7 @@ namespace Barotrauma UpgradeManager.PurchaseUpgrade(prefab, category, client: sender); // unstable logging - int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList); + int price = prefab.Price.GetBuyPrice(prefab, 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); } @@ -1194,13 +1251,13 @@ namespace Barotrauma { foreach (CharacterInfo hireInfo in location.HireManager.PendingHires) { - if (TryHireCharacter(location, hireInfo, sender)) + if (TryHireCharacter(location, hireInfo, sender.Character, sender)) { hiredCharacters.Add(hireInfo); - }; + } } } - + if (updatePending) { List pendingHireInfos = new List(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs index c961063c5..b0b24e75c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -304,7 +304,8 @@ namespace Barotrauma.Items.Components private void ThrowError(string message, Client c) { - DebugConsole.ThrowError(message); + DebugConsole.ThrowError(message, + contentPackage: item.Prefab.ContentPackage); SendToClient(CircuitBoxOpcode.Error, new CircuitBoxErrorEvent(message), c); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index a063286d5..0ce5b7ab5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -66,7 +66,7 @@ namespace Barotrauma { foreach (Item item in slots[i].Items.ToList()) { - if (!receivedItemIdsFromClient[i].Contains(item.ID)) + if (!receivedItemIdsFromClient[i].Contains(item.ID) && item.IsInteractable(c.Character)) { Item droppedItem = item; Entity prevOwner = Owner; @@ -107,10 +107,10 @@ namespace Barotrauma if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; } if (item.GetComponent() is not Pickable pickable || - (pickable.IsAttached && !pickable.PickingDone) || - item.AllowedSlots.None()) + (pickable.IsAttached && !pickable.PickingDone) || item.AllowedSlots.None() || !item.IsInteractable(c.Character)) { - DebugConsole.AddWarning($"Client {c.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})"); + DebugConsole.AddWarning($"Client {c.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})", + item.Prefab.ContentPackage); continue; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index f59311120..62c2477a1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -161,6 +161,20 @@ namespace Barotrauma msg.WriteUInt16(droppedItem.ID); } break; + case SetHighlightEventData highlightEventData: + bool isTargetedForClient = + highlightEventData.TargetClients.IsEmpty || + highlightEventData.TargetClients.Contains(c); + msg.WriteBoolean(isTargetedForClient); + if (isTargetedForClient) + { + msg.WriteBoolean(highlightEventData.Highlighted); + if (highlightEventData.Highlighted) + { + msg.WriteColorR8G8B8A8(highlightEventData.Color); + } + } + break; default: throw error($"Unsupported event type {itemEventData.GetType().Name}"); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs index b2a6b0dda..a37508ccf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/ItemEventData.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +#nullable enable +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -16,4 +19,20 @@ partial class Item Items = items.Distinct().ToImmutableArray(); } } + + public readonly struct SetHighlightEventData : IEventData + { + public EventType EventType => EventType.SetHighlight; + public readonly bool Highlighted; + public readonly Color Color; + + public readonly ImmutableArray TargetClients; + + public SetHighlightEventData(bool highlighted, Color color, IEnumerable? targetClients) + { + Highlighted = highlighted; + Color = color; + TargetClients = (targetClients ?? Enumerable.Empty()).ToImmutableArray(); + } + } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index bdd772286..814152a26 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -124,7 +124,7 @@ namespace Barotrauma out NetworkFireSource[] newFireSources); if (!c.HasPermission(ClientPermissions.ConsoleCommands) || - !c.PermittedConsoleCommands.Any(command => command.names.Contains("fire") || command.names.Contains("editfire"))) + !c.PermittedConsoleCommands.Any(command => command.Names.Contains("fire".ToIdentifier()) || command.Names.Contains("editfire".ToIdentifier()))) { return; } @@ -138,7 +138,7 @@ namespace Barotrauma var newFire = i < FireSources.Count ? FireSources[i] : - new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true); + new FireSource(Submarine == null ? pos : pos + Submarine.Position, sourceCharacter: null, isNetworkMessage: true); newFire.Position = pos; newFire.Size = new Vector2(size, newFire.Size.Y); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index c08936ba3..cede18f2d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -254,6 +254,7 @@ namespace Barotrauma.Networking private void OnInitializationComplete(NetworkConnection connection, string clientName) { + clientName = Client.SanitizeName(clientName); Client newClient = new Client(clientName, GetNewClientSessionId()); newClient.InitClientSync(); newClient.Connection = connection; @@ -804,7 +805,7 @@ namespace Barotrauma.Networking { using (dosProtection.Pause(connectedClient)) { - MultiPlayerCampaign.LoadCampaign(saveName); + MultiPlayerCampaign.LoadCampaign(saveName, connectedClient); } } } @@ -1230,11 +1231,6 @@ namespace Barotrauma.Networking } c.LastRecvEntityEventID = lastRecvEntityEventID; - #warning TODO: remove this later - /*if (!CoroutineManager.IsCoroutineRunning("RoundRestartLoop")) - { - CoroutineManager.StartCoroutine(RoundRestartLoop(), "RoundRestartLoop"); - }*/ } else if (lastRecvEntityEventID != c.LastRecvEntityEventID && GameSettings.CurrentConfig.VerboseLogging) { @@ -1484,7 +1480,7 @@ namespace Barotrauma.Networking { using (dosProtection.Pause(sender)) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath, sender); } } } @@ -2538,7 +2534,7 @@ namespace Barotrauma.Networking { spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value, buyer: null)); } - CargoManager.CreateItems(spawnList, sub, cargoManager: null); + CargoManager.DeliverItemsToSub(spawnList, sub, cargoManager: null); } } @@ -2585,6 +2581,7 @@ namespace Barotrauma.Networking msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn); msg.WriteBoolean(ServerSettings.AllowDisguises); msg.WriteBoolean(ServerSettings.AllowRewiring); + msg.WriteBoolean(ServerSettings.AllowImmediateItemDelivery); msg.WriteBoolean(ServerSettings.AllowFriendlyFire); msg.WriteBoolean(ServerSettings.LockAllDefaultWires); msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat); @@ -3710,8 +3707,12 @@ namespace Barotrauma.Networking } } + public readonly List JobAssignmentDebugLog = new List(); + public void AssignJobs(List unassigned) { + JobAssignmentDebugLog.Clear(); + var jobList = JobPrefab.Prefabs.ToList(); unassigned = new List(unassigned); unassigned = unassigned.OrderBy(sp => Rand.Int(int.MaxValue)).ToList(); @@ -3733,10 +3734,11 @@ namespace Barotrauma.Networking //remove already assigned clients from unassigned unassigned.RemoveAll(u => campaignAssigned.ContainsKey(u)); //add up to assigned client count - foreach (KeyValuePair clientJob in campaignAssigned) + foreach ((Client client, Job job) in campaignAssigned) { - assignedClientCount[clientJob.Value.Prefab]++; - clientJob.Key.AssignedJob = new JobVariant(clientJob.Value.Prefab, clientJob.Value.Variant); + assignedClientCount[job.Prefab]++; + client.AssignedJob = new JobVariant(job.Prefab, job.Variant); + JobAssignmentDebugLog.Add($"Client {client.Name} has an existing campaign character, keeping the job {job.Name}."); } } @@ -3755,6 +3757,7 @@ namespace Barotrauma.Networking { if (unassigned[i].JobPreferences.Count == 0) { continue; } if (!unassigned[i].JobPreferences.Any() || !unassigned[i].JobPreferences[0].Prefab.AllowAlways) { continue; } + JobAssignmentDebugLog.Add($"Client {unassigned[i].Name} has {unassigned[i].JobPreferences[0].Prefab.Name} as their first preference, assigning it because the job is always allowed."); unassigned[i].AssignedJob = unassigned[i].JobPreferences[0]; unassigned.RemoveAt(i); } @@ -3773,6 +3776,7 @@ namespace Barotrauma.Networking Client client = FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: false); if (client != null) { + JobAssignmentDebugLog.Add($"At least {jobPrefab.MinNumber} {jobPrefab.Name} required. Assigning {client.Name} as a {jobPrefab.Name} (has the job in their preferences)."); AssignJob(client, jobPrefab); } } @@ -3784,7 +3788,11 @@ namespace Barotrauma.Networking { if (unassigned.Count == 0) { break; } if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) { continue; } - AssignJob(FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: true), jobPrefab); + var client = FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: true); + JobAssignmentDebugLog.Add( + $"At least {jobPrefab.MinNumber} {jobPrefab.Name} required. "+ + $"A random client needs to be assigned because no one has the job in their preferences. Assigning {client.Name} as a {jobPrefab.Name}."); + AssignJob(client, jobPrefab); } } @@ -3802,32 +3810,6 @@ namespace Barotrauma.Networking } } - List availableSpawnPoints = WayPoint.WayPointList.FindAll(wp => - wp.SpawnType == SpawnType.Human && - wp.Submarine != null && wp.Submarine.TeamID == teamID); - - /*bool canAssign = false; - do - { - canAssign = false; - foreach (WayPoint spawnPoint in unassignedSpawnPoints) - { - if (unassigned.Count == 0) { break; } - - JobPrefab job = spawnPoint.AssignedJob ?? JobPrefab.List.Values.GetRandom(); - if (assignedClientCount[job] >= job.MaxNumber) { continue; } - - Client assignedClient = FindClientWithJobPreference(unassigned, job, true); - if (assignedClient != null) - { - assignedClient.AssignedJob = job; - assignedClientCount[job]++; - unassigned.Remove(assignedClient); - canAssign = true; - } - } - } while (unassigned.Count > 0 && canAssign);*/ - // Attempt to give the clients a job they have in their job preferences. // First evaluate all the primary preferences, then all the secondary etc. for (int preferenceIndex = 0; preferenceIndex < 3; preferenceIndex++) @@ -3838,12 +3820,17 @@ namespace Barotrauma.Networking if (preferenceIndex >= client.JobPreferences.Count) { continue; } var preferredJob = client.JobPreferences[preferenceIndex]; JobPrefab jobPrefab = preferredJob.Prefab; - if (assignedClientCount[jobPrefab] >= jobPrefab.MaxNumber || client.Karma < jobPrefab.MinKarma) + if (assignedClientCount[jobPrefab] >= jobPrefab.MaxNumber) { - //can't assign this job if maximum number has reached or the clien't karma is too low + JobAssignmentDebugLog.Add($"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Cannot assign, maximum number of the job has been reached."); continue; } - + if (client.Karma < jobPrefab.MinKarma) + { + JobAssignmentDebugLog.Add($"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Cannot assign, karma too low ({client.Karma} < {jobPrefab.MinKarma})."); + continue; + } + JobAssignmentDebugLog.Add($"{client.Name} has {jobPrefab.Name} as their {preferenceIndex + 1}. preference. Assigning {client.Name} as a {jobPrefab.Name}."); client.AssignedJob = preferredJob; assignedClientCount[jobPrefab]++; unassigned.RemoveAt(i); @@ -3859,7 +3846,9 @@ namespace Barotrauma.Networking //all jobs taken, give a random job if (remainingJobs.Count == 0) { - DebugConsole.ThrowError("Failed to assign a suitable job for \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); + string errorMsg = $"Failed to assign a suitable job for \"{c.Name}\" (all jobs already have the maximum numbers of players). Assigning a random job..."; + DebugConsole.ThrowError(errorMsg); + JobAssignmentDebugLog.Add(errorMsg); int jobIndex = Rand.Range(0, jobList.Count); int skips = 0; while (c.Karma < jobList[jobIndex].MinKarma) @@ -3875,19 +3864,20 @@ namespace Barotrauma.Networking assignedClientCount[c.AssignedJob.Prefab]++; } //if one of the client's preferences is still available, give them that job - else if (c.JobPreferences.Any(jp => remainingJobs.Contains(jp.Prefab))) + else if (c.JobPreferences.FirstOrDefault(jp => remainingJobs.Contains(jp.Prefab)) is { } remainingJob) { - foreach (JobVariant preferredJob in c.JobPreferences) - { - c.AssignedJob = preferredJob; - assignedClientCount[preferredJob.Prefab]++; - break; - } + JobAssignmentDebugLog.Add( + $"{c.Name} has {remainingJob.Prefab.Name} as their {c.JobPreferences.IndexOf(remainingJob) + 1}. preference, and it is still available."+ + $" Assigning {c.Name} as a {remainingJob.Prefab.Name}."); + c.AssignedJob = remainingJob; + assignedClientCount[remainingJob.Prefab]++; } else //none of the client's preferred jobs available, choose a random job { c.AssignedJob = new JobVariant(remainingJobs[Rand.Range(0, remainingJobs.Count)], 0); assignedClientCount[c.AssignedJob.Prefab]++; + JobAssignmentDebugLog.Add( + $"No suitable jobs available for {c.Name} (karma {c.Karma}). Assigning a random job: {c.AssignedJob.Prefab.Name}."); } } } @@ -3951,7 +3941,6 @@ namespace Barotrauma.Networking if (remainingJobs.None()) { DebugConsole.ThrowError("Failed to assign a suitable job for bot \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); - #warning TODO: is this randsync correct? c.Job = Job.Random(Rand.RandSync.ServerAndClient); assignedPlayerCount[c.Job.Prefab]++; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index 6bf35ee39..cf6595515 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -426,8 +426,9 @@ namespace Barotrauma.Networking } else { - double midRoundSyncTimeOut = uniqueEvents.Count / 100 * server.UpdateInterval.TotalSeconds; - midRoundSyncTimeOut = Math.Max(10.0f, midRoundSyncTimeOut * 10.0f); + //assume we can get at least 10 events per second through + double midRoundSyncTimeOut = uniqueEvents.Count / 10 * server.UpdateInterval.TotalSeconds; + midRoundSyncTimeOut = Math.Max(midRoundSyncTimeOut, server.ServerSettings.MinimumMidRoundSyncTimeout); client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count; client.NeedsMidRoundSync = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 187f0f1af..0d32a93e8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -537,7 +537,7 @@ namespace Barotrauma.Networking } //add the ID card tags they should've gotten when spawning in the shuttle - character.GiveIdCardTags(shuttleSpawnPoints[i], requireSpawnPointTagsNotGiven: false, createNetworkEvent: true); + character.GiveIdCardTags(shuttleSpawnPoints[i], createNetworkEvent: true); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index ec0e8e101..7f29c7662 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -611,7 +611,7 @@ namespace Barotrauma.Networking { foreach (DebugConsole.Command command in clientPermission.PermittedCommands) { - clientElement.Add(new XElement("command", new XAttribute("name", command.names[0]))); + clientElement.Add(new XElement("command", new XAttribute("name", command.Names[0]))); } } doc.Root.Add(clientElement); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 2a127c1ba..e1d371659 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -2,6 +2,7 @@ using Barotrauma.Steam; using System; +using System.Diagnostics; using Barotrauma.IO; using System.Linq; using System.Text; @@ -109,18 +110,25 @@ namespace Barotrauma } } + Exception unhandledException = args.ExceptionObject as Exception; string reportFilePath = ""; try { reportFilePath = "servercrashreport.log"; - CrashDump(ref reportFilePath, (Exception)args.ExceptionObject); + CrashDump(ref reportFilePath, unhandledException); } - catch + catch (Exception exceptionHandlerError) { - //fuck + Debug.WriteLine(exceptionHandlerError.Message); + string slimCrashReport = "Exception handler failed: " + exceptionHandlerError.Message + "\n" + exceptionHandlerError.StackTrace; + if (unhandledException != null) + { + slimCrashReport += "\n\nInitial exception: " + unhandledException.Message + "\n" + unhandledException.StackTrace; + } + File.WriteAllText("servercrashreportslim.log", slimCrashReport); reportFilePath = ""; } - swallowExceptions(() => NotifyCrash(reportFilePath, (Exception)args.ExceptionObject)); + swallowExceptions(() => NotifyCrash(reportFilePath, unhandledException)); swallowExceptions(() => Game?.Exit()); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index e0967195b..039e30109 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -58,15 +58,20 @@ 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()); + + //a2s seems to break if too much data is added (seems to be related to MTU?) + //let's restrict the number of packages to 10, clients can use packagecount to tell when the list has been truncated + const int MaxPackagesToList = 10; int index = 0; - foreach (var contentPackage in contentPackages) + foreach (var contentPackage in contentPackages.Take(MaxPackagesToList)) { string ugcIdStr = contentPackage.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : string.Empty; Steamworks.SteamServer.SetKey( - $"contentpackage{index}", - contentPackage.Name+","+ contentPackage.Hash.StringRepresentation + "," + ugcIdStr); + $"contentpackage{index}", + contentPackage.Name + "," + contentPackage.Hash.StringRepresentation + "," + ugcIdStr); index++; } + Steamworks.SteamServer.SetKey("packagecount", contentPackages.Count().ToString()); Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); @@ -79,6 +84,10 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier.Value); Steamworks.SteamServer.SetKey("playstyle", server.ServerSettings.PlayStyle.ToString()); Steamworks.SteamServer.SetKey("language", server.ServerSettings.Language.ToString()); + if (GameMain.NetLobbyScreen?.SelectedSub != null) + { + Steamworks.SteamServer.SetKey("submarine", GameMain.NetLobbyScreen.SelectedSub.Name); + } Steamworks.SteamServer.DedicatedServer = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index 696d36f9d..498108b29 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -224,7 +224,8 @@ namespace Barotrauma var selectedTraitor = SelectRandomTraitor(); if (selectedTraitor == null) { - DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{selectedPrefab.Identifier}\"."); + DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{selectedPrefab.Identifier}\".", + contentPackage: selectedPrefab.ContentPackage); return false; } CreateTraitorEvent(eventManager, selectedPrefab, selectedTraitor); @@ -262,8 +263,9 @@ namespace Barotrauma if (amountToChoose > viableTraitors.Count) { DebugConsole.ThrowError( - $"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} secondary traitors."+ - $"Make sure the {nameof(traitorEvent.Prefab.MinPlayerCount)} of the event is high enough to support to desired amount of secondary traitors."); + $"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} secondary traitors. " + + $"Make sure the {nameof(traitorEvent.Prefab.MinPlayerCount)} of the event is high enough to support to desired amount of secondary traitors.", + contentPackage: traitorEvent.Prefab.ContentPackage); amountToChoose = viableTraitors.Count; } @@ -352,7 +354,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Failed to create an instance of the traitor event prefab \"{selectedPrefab.Identifier}\"!"); + DebugConsole.ThrowError($"Failed to create an instance of the traitor event prefab \"{selectedPrefab.Identifier}\"!", + contentPackage: selectedPrefab.ContentPackage); } } @@ -365,7 +368,8 @@ namespace Barotrauma var traitor = SelectRandomTraitor(); if (traitor == null) { - DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{traitorEventPrefab.Identifier}\"."); + DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{traitorEventPrefab.Identifier}\".", + contentPackage: traitorEventPrefab.ContentPackage); return; } CreateTraitorEvent(eventManager, traitorEventPrefab, traitor); @@ -451,6 +455,15 @@ namespace Barotrauma activeEvent.TraitorEvent.Prefab, activeEvent.TraitorEvent.CurrentState, activeEvent.Traitor)); + + if (activeEvent.TraitorEvent.CurrentState == TraitorEvent.State.Completed) + { + SteamAchievementManager.OnTraitorWin(activeEvent.TraitorEvent.Traitor?.Character); + foreach (var secondaryTraitor in activeEvent.TraitorEvent.SecondaryTraitors) + { + SteamAchievementManager.OnTraitorWin(secondaryTraitor?.Character); + } + } } if (previousTraitorEvents.Count > MaxPreviousEventHistory) { diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 6948b9453..0ece0e532 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.19.3 + 1.2.6.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml index ef782ae23..a9be7381b 100644 --- a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -12,7 +12,9 @@ StartingBalanceAmount="High" StartItemSet="easy" MaxMissionCount="3" - Difficulty="Easy"/> + Difficulty="Easy" + MinStolenItemInspectionProbability="0.2" + MaxStolenItemInspectionProbability="0.9"/> + Difficulty="Medium" + MinStolenItemInspectionProbability="0.3" + MaxStolenItemInspectionProbability="0.9"/> + Difficulty="Hard" + MinStolenItemInspectionProbability="0.4" + MaxStolenItemInspectionProbability="1.0"/> \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 2fc63b541..eb469f60a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -91,6 +91,11 @@ namespace Barotrauma private IEnumerable visibleHulls; private float hullVisibilityTimer; const float hullVisibilityInterval = 0.5f; + + /// + /// Returns hulls that are visible to the character, including the current hull. + /// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls. + /// public IEnumerable VisibleHulls { get @@ -353,7 +358,7 @@ namespace Barotrauma public static void UnequipContainedItems(Character character, Item parentItem, Func predicate, bool avoidDroppingInSea = true, int? unequipMax = null) { var inventory = parentItem.OwnInventory; - if (inventory == null) { return; } + if (inventory == null || !inventory.Container.DrawInventory) { return; } int removed = 0; if (predicate == null || inventory.AllItems.Any(predicate)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 9495c3e08..d4188f275 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -262,7 +262,8 @@ namespace Barotrauma if (aiElements.Count == 0) { - DebugConsole.ThrowError("Error in file \"" + c.Params.File + "\" - no AI element found."); + DebugConsole.ThrowError("Error in file \"" + c.Params.File.Path + "\" - no AI element found.", + contentPackage: c.Prefab?.ContentPackage); outsideSteering = new SteeringManager(this); insideSteering = new IndoorsSteeringManager(this, false, false); return; @@ -311,7 +312,7 @@ namespace Barotrauma } ReevaluateAttacks(); outsideSteering = new SteeringManager(this); - insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors); + insideSteering = new IndoorsSteeringManager(this, AIParams.CanOpenDoors, canAttackDoors); steeringManager = outsideSteering; State = AIState.Idle; requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); @@ -321,6 +322,10 @@ namespace Barotrauma } private CharacterParams.AIParams _aiParams; + /// + /// Shorthand for with null checking. + /// + /// or an empty params. Does not return nulls. public CharacterParams.AIParams AIParams { get @@ -330,7 +335,8 @@ namespace Barotrauma _aiParams = Character.Params.AI; if (_aiParams == null) { - DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled."); + DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled.", + contentPackage: Character.Prefab.ContentPackage); Enabled = false; _aiParams = new CharacterParams.AIParams(null, Character.Params); } @@ -563,7 +569,7 @@ namespace Barotrauma } } - if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors) + if (Character.Params.UsePathFinding && AIParams.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); @@ -2503,7 +2509,8 @@ namespace Barotrauma Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head); if (mouthLimb == null) { - DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)"); + DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)", + contentPackage: Character.Prefab.ContentPackage); State = AIState.Idle; return; } @@ -2540,7 +2547,11 @@ namespace Barotrauma item.body.LinearVelocity -= velocity * 0.25f; bool wasBroken = item.Condition <= 0.0f; item.LastEatenTime = (float)Timing.TotalTimeUnpaused; - item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime); + item.AddDamage(Character, + item.WorldPosition, + new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), + impulseDirection: Vector2.Zero, + deltaTime); Character.ApplyStatusEffects(ActionType.OnEating, deltaTime); if (item.Condition <= 0.0f) { @@ -3090,7 +3101,10 @@ namespace Barotrauma break; } - valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist); + valueModifier *= + targetMemory.Priority / + //sqrt = the further the target is, the less the distance matters + MathF.Sqrt(dist); if (valueModifier > targetValue) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 698f938b4..a8dfedc43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -685,8 +685,8 @@ namespace Barotrauma } if (removeDivingSuit) { - var divingSuit = Character.Inventory.FindItemByTag(Tags.HeavyDivingGear); - if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors)) + var divingSuit = Character.Inventory.FindEquippedItemByTag(Tags.HeavyDivingGear); + if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors) && divingSuit.IsInteractable(Character)) { if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { @@ -727,54 +727,51 @@ namespace Barotrauma } } if (takeMaskOff) - { - if (Character.HasEquippedItem(Tags.LightDivingGear)) + { + var mask = Character.Inventory.FindEquippedItemByTag(Tags.LightDivingGear); + if (mask != null) { - var mask = Character.Inventory.FindItemByTag(Tags.LightDivingGear); - if (mask != null) + if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) { - if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) + if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { - if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) + mask.Drop(Character); + HandleRelocation(mask); + ReequipUnequipped(); + } + else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask) + { + findItemState = FindItemState.DivingMask; + if (FindSuitableContainer(mask, out Item targetContainer)) { - mask.Drop(Character); - HandleRelocation(mask); - ReequipUnequipped(); - } - else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask) - { - findItemState = FindItemState.DivingMask; - if (FindSuitableContainer(mask, out Item targetContainer)) + findItemState = FindItemState.None; + itemIndex = 0; + if (targetContainer != null) { - findItemState = FindItemState.None; - itemIndex = 0; - if (targetContainer != null) + var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent()); + decontainObjective.Abandoned += () => { - var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => - { - ReequipUnequipped(); - IgnoredItems.Add(targetContainer); - }; - decontainObjective.Completed += () => ReequipUnequipped(); - ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); - return; - } - else - { - mask.Drop(Character); - HandleRelocation(mask); ReequipUnequipped(); - } + IgnoredItems.Add(targetContainer); + }; + decontainObjective.Completed += () => ReequipUnequipped(); + ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); + return; + } + else + { + mask.Drop(Character); + HandleRelocation(mask); + ReequipUnequipped(); } } } - else - { - ReequipUnequipped(); - } } - } + else + { + ReequipUnequipped(); + } + } } } } @@ -784,13 +781,11 @@ namespace Barotrauma if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem) { - for (int i = 0; i < 2; i++) + foreach (Item item in Character.HeldItems) { - var hand = i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand; - Item item = Character.Inventory.GetItemInLimbSlot(hand); - if (item == null) { continue; } + if (item == null || !item.IsInteractable(Character)) { continue; } - if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID ) + if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID) { if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Bag })) { continue; } findItemState = FindItemState.OtherItem; @@ -1389,7 +1384,10 @@ namespace Barotrauma // Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist. continue; } - bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); + bool isWitnessing = + otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || + otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) || + otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true); if (!isWitnessing) { //if the other character did not witness the attack, and the character is not within report range (or capable of reporting) @@ -1754,11 +1752,11 @@ namespace Barotrauma if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead || otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI || - !otherHumanAI.VisibleHulls.Contains(character.CurrentHull)) + Vector2.DistanceSquared(otherCharacter.WorldPosition, character.WorldPosition) > 1000.0f * 1000.0f) { continue; } - if (!otherCharacter.CanSeeTarget(character)) { continue; } + if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; } if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); } float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character]; @@ -1796,7 +1794,7 @@ namespace Barotrauma if (!TriggerSecurity(otherHumanAI, combatMode)) { // Else call the others - foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition))) + foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition))) { if (!TriggerSecurity(security.AIController as HumanAIController, combatMode)) { @@ -1840,13 +1838,13 @@ namespace Barotrauma foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f || - otherCharacter.Info?.Job == null || !(otherCharacter.AIController is HumanAIController otherHumanAI) || - !otherHumanAI.VisibleHulls.Contains(thief.CurrentHull)) + otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI || + Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f) { continue; } //if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; } - if (!otherCharacter.CanSeeTarget(thief)) { continue; } + if (!otherCharacter.CanSeeTarget(thief, seeThroughWindows: true)) { continue; } // Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding // -> allow them to use the emergency items if (thief.Submarine != null) @@ -1857,16 +1855,11 @@ namespace Barotrauma } if (!someoneSpoke) { - if (!item.StolenDuringRound && - Level.Loaded?.Type == LevelData.LevelType.Outpost && - GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) + if (!item.StolenDuringRound) { - var reputationLoss = MathHelper.Clamp( - (item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice, - Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem); - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss); + ApplyStealingReputationLoss(item); + item.StolenDuringRound = true; } - item.StolenDuringRound = true; otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f); someoneSpoke = true; #if CLIENT @@ -1877,7 +1870,7 @@ namespace Barotrauma if (!TriggerSecurity(otherHumanAI)) { // Else call the others - foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition))) + foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition))) { if (TriggerSecurity(security.AIController as HumanAIController)) { @@ -1898,6 +1891,10 @@ namespace Barotrauma if (humanAI == null) { return false; } if (!humanAI.Character.IsSecurity) { return false; } if (humanAI.ObjectiveManager.IsCurrentObjective()) { return false; } + if (humanAI.ObjectiveManager.GetObjective() is { } findThieves) + { + findThieves.InspectEveryone(); + } humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(), abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null, onAbort: () => @@ -1915,6 +1912,18 @@ namespace Barotrauma } } + public static void ApplyStealingReputationLoss(Item item) + { + if (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); + } + } + // 0.225 - 0.375 private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 884a07263..c9dd03d88 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -197,17 +197,6 @@ namespace Barotrauma } } - /// - /// This method allows multiple subobjectives of same type. Use with caution. - /// - public void AddSubObjectiveInQueue(AIObjective objective) - { - if (!subObjectives.Contains(objective)) - { - subObjectives.Add(objective); - } - } - public void RemoveSubObjective(ref T objective) where T : AIObjective { if (objective != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs new file mode 100644 index 000000000..cd63e7856 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs @@ -0,0 +1,160 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class AIObjectiveCheckStolenItems : AIObjective + { + public override Identifier Identifier { get; set; } = "check stolen items".ToIdentifier(); + public override bool AllowOutsideSubmarine => false; + public override bool AllowInAnySub => false; + + public float FindStolenItemsProbability = 1.0f; + + enum State + { + GotoTarget, + Inspect, + Warn, + Done + } + + private float inspectDelay; + private float warnDelay; + + private State currentState; + + public readonly Character TargetCharacter; + + private AIObjectiveGoTo? goToObjective; + + private readonly List stolenItems = new List(); + + public AIObjectiveCheckStolenItems(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) : + base(character, objectiveManager, priorityModifier) + { + TargetCharacter = targetCharacter; + inspectDelay = 5.0f; + warnDelay = 5.0f; + } + + public override bool IsLoop + { + get => false; + set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); + } + + protected override bool CheckObjectiveSpecific() => false; + + protected override float GetPriority() + { + if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this)) + { + Priority = objectiveManager.GetOrderPriority(this); + } + else + { + Priority = AIObjectiveManager.LowestOrderPriority - 1; + } + return Priority; + } + + public void ForceComplete() + { + IsCompleted = true; + } + + protected override void Act(float deltaTime) + { + switch (currentState) + { + case State.GotoTarget: + TryAddSubObjective(ref goToObjective, + constructor: () => + { + return new AIObjectiveGoTo(TargetCharacter, character, objectiveManager, repeat: false) + { + SpeakIfFails = false + }; + }, + onCompleted: () => + { + RemoveSubObjective(ref goToObjective); + currentState = State.Inspect; + stolenItems.Clear(); + TargetCharacter.Inventory.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true, stolenItems); + character.Speak(TextManager.Get("dialogcheckstolenitems").Value); + }, + onAbandon: () => + { + Abandon = true; + }); + break; + case State.Inspect: + Inspect(deltaTime); + break; + case State.Warn: + Warn(deltaTime); + break; + } + } + + private void Inspect(float deltaTime) + { + if (inspectDelay > 0.0f) + { + character.SelectCharacter(TargetCharacter); + inspectDelay -= deltaTime; + return; + } + + if (stolenItems.Any() && + Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) < FindStolenItemsProbability) + { + character.Speak(TextManager.Get("dialogcheckstolenitems.warn").Value); + currentState = State.Warn; + } + else + { + character.Speak(TextManager.Get("dialogcheckstolenitems.nostolenitems").Value); + currentState = State.Done; + IsCompleted = true; + } + character.DeselectCharacter(); + } + + private void Warn(float deltaTime) + { + if (warnDelay > 0.0f) + { + warnDelay -= deltaTime; + return; + } + var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == TargetCharacter); + if (stolenItemsOnCharacter.Any()) + { + character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value); + HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, TargetCharacter); + foreach (var stolenItem in stolenItemsOnCharacter) + { + HumanAIController.ApplyStealingReputationLoss(stolenItem); + } + } + else + { + character.Speak(TextManager.Get("dialogcheckstolenitems.comply").Value); + } + foreach (var item in stolenItems) + { + HumanAIController.ObjectiveManager.AddObjective(new AIObjectiveGetItem(character, item, objectiveManager, equip: false) + { + BasePriority = 10 + }); + } + currentState = State.Done; + IsCompleted = true; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 612eaa7fe..9bbb3836b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -1070,7 +1070,8 @@ namespace Barotrauma { // Try reload ammunition from inventory static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio); - Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true); + Item ammunition = character.Inventory.FindItem(i => + i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive: true); if (ammunition != null) { var container = Weapon.GetComponent(); @@ -1089,6 +1090,9 @@ namespace Barotrauma } else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null) { + // Inventory not drawn = it's not interactable + // If the weapon is empty and the inventory is inaccessible, it can't be reloaded + if (!Weapon.OwnInventory.Container.DrawInventory) { return false; } SeekAmmunition(ammunitionIdentifiers); } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs index ed5408fb7..0a2a7839d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs @@ -14,7 +14,7 @@ namespace Barotrauma private int escapeProgress; private bool isBeingWatched; - private bool shouldSwitchTeams; + private readonly bool shouldSwitchTeams; const string EscapeTeamChangeIdentifier = "escape"; @@ -88,10 +88,12 @@ namespace Barotrauma escapeProgress += Rand.Range(2, 5); if (escapeProgress > 15) { - Item handcuffs = character.Inventory.FindItemByTag(Tags.HandLockerItem); - if (handcuffs != null) + foreach (var it in character.HeldItems) { - handcuffs.Drop(character); + if (it.HasTag(Tags.HandLockerItem) && it.IsInteractable(character)) + { + it.Drop(character); + } } } escapeTimer = EscapeIntervalTimer * Rand.Range(0.75f, 1.25f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs new file mode 100644 index 000000000..f6ac0bec8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs @@ -0,0 +1,152 @@ +#nullable enable +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class AIObjectiveFindThieves : AIObjectiveLoop + { + public override Identifier Identifier { get; set; } = "find thieves".ToIdentifier(); + protected override float IgnoreListClearInterval => 30; + public override bool IgnoreUnsafeHulls => true; + + protected override float TargetUpdateTimeMultiplier => 1.0f; + + const float DefaultInspectDistance = 200.0f; + + /// + /// How close the NPC must be to the target to the inspect them? You can use high values to make the NPC + /// systematically go through targets no matter where they are, and low values to check targets they happen to come across. + /// + public float InspectDistance = DefaultInspectDistance; + + private float? overrideInspectProbability; + /// + /// Chance of inspecting a valid target. The NPC won't try to inspect that target again for + /// regardless if the target is inspected or not. + /// + public float InspectProbability + { + get + { + if (overrideInspectProbability.HasValue) + { + return overrideInspectProbability.Value; + } + if (GameMain.GameSession?.Campaign is { } campaign) + { + if (campaign.Map?.CurrentLocation?.Reputation is { } reputation) + { + return MathHelper.Lerp( + campaign.Settings.MaxStolenItemInspectionProbability, + campaign.Settings.MinStolenItemInspectionProbability, + reputation.NormalizedValue); + } + } + + return 0.2f; + } + } + + /// + /// When did the character last inspect whether some other character has stolen items on them? + /// + private static readonly Dictionary lastInspectionTimes = new Dictionary(); + + private readonly float inspectionInterval = 120.0f; + + public AIObjectiveFindThieves(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) { } + + protected override bool Filter(Character target) + { + if (!IsValidTarget(target, character)) { return false; } + if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > InspectDistance * InspectDistance) { return false; } + if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime)) + { + if (Timing.TotalTime < lastInspectionTime + inspectionInterval) + { + return false; + } + } + return true; + } + + protected override IEnumerable GetList() => Character.CharacterList; + + protected override float TargetEvaluation() + { + return subObjectives.Any() ? 50 : 0; + } + + public void InspectEveryone() + { + lastInspectionTimes.Clear(); + overrideInspectProbability = 1.0f; + InspectDistance = DefaultInspectDistance * 2; + } + + protected override AIObjective ObjectiveConstructor(Character target) + { + var checkStolenItemsObjective = new AIObjectiveCheckStolenItems(character, target, objectiveManager); + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= InspectProbability) + { + checkStolenItemsObjective.ForceComplete(); + lastInspectionTimes[target] = Timing.TotalTime; + } + return checkStolenItemsObjective; + } + + private float checkVisibleStolenItemsTimer; + private const float CheckVisibleStolenItemsInterval = 5.0f; + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + if (checkVisibleStolenItemsTimer > 0.0f) + { + checkVisibleStolenItemsTimer -= deltaTime; + return; + } + foreach (var target in Character.CharacterList) + { + if (!IsValidTarget(target, character)) { continue; } + //if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items) + if (target.Inventory.AllItems.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing && target.HasEquippedItem(it)) && + character.CanSeeTarget(target, seeThroughWindows: true)) + { + AIObjectiveCheckStolenItems? existingObjective = + objectiveManager.GetActiveObjectives().FirstOrDefault(o => o.TargetCharacter == target); + if (existingObjective == null) + { + objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager)); + lastInspectionTimes[target] = Timing.TotalTime; + } + + } + } + checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval; + } + + private bool IsValidTarget(Character target, Character character) + { + if (target == null || target.Removed) { return false; } + if (target.IsIncapacitated) { return false; } + if (target == character) { return false; } + if (target.Submarine == null) { return false; } + if (character.Submarine == null) { return false; } + if (target.CurrentHull == null) { return false; } + if (target.Submarine != character.Submarine) { return false; } + //only player's crew can steal, ignore other teams + if (!target.IsOnPlayerTeam) { return false; } + if (target.IsArrested) { return false; } + return true; + } + + protected override void OnObjectiveCompleted(AIObjective objective, Character target) + { + lastInspectionTimes[target] = Timing.TotalTime; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index bd01ecebf..1e22487b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -120,7 +120,7 @@ namespace Barotrauma // The intention behind this is to reduce unnecessary path finding calls in cases where the bot can't find a path. timerMargin += 0.5f; timerMargin = Math.Min(timerMargin, newTargetIntervalMin); - newTargetTimer = Math.Min(newTargetTimer, timerMargin); + newTargetTimer = Math.Max(newTargetTimer, timerMargin); } private void SetTargetTimerHigh() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 5419eedf7..2a939ab76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -178,7 +178,7 @@ namespace Barotrauma if (!objectiveManager.IsOrder(this)) { // Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention. - bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater; + bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater || this is AIObjectiveFindThieves; if (!ignore && !ReportedTargets.Contains(target)) { continue; } } if (!Filter(target)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 5d1aa4a61..42c31a65c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -142,6 +142,7 @@ namespace Barotrauma prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t)); } AddObjective(newIdleObjective); + int objectiveCount = Objectives.Count; foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives) { @@ -549,6 +550,9 @@ namespace Barotrauma case "escapehandcuffs": newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier); break; + case "findthieves": + newObjective = new AIObjectiveFindThieves(character, this, priorityModifier: priorityModifier); + break; case "prepareforexpedition": newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 21c5aeb35..04ee9ccad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -441,7 +441,8 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}"); + DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}", + contentPackage: ContentPackage); return null; } } @@ -663,6 +664,13 @@ namespace Barotrauma WallSectionIndex = wallSectionIndex ?? other.WallSectionIndex; UseController = useController ?? other.UseController; + +#if DEBUG + if (UseController && ConnectedController == null) + { + DebugConsole.ThrowError($"AI: Created an Order {Identifier} that's set to use a Controller, but a Controller was not specified.\n{Environment.StackTrace.CleanupStackTrace()}"); + } +#endif } public Order WithOption(Identifier option) @@ -712,7 +720,12 @@ namespace Barotrauma public Order WithItemComponent(Item item, ItemComponent component = null) { - return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item)); + Controller controller = null; + if (UseController) + { + controller = item?.FindController(tags: ControllerTags); + } + return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item), connectedController: controller); } public Order WithWallSection(Structure wall, int? sectionIndex) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 47e3ec73d..ad724f0bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -552,7 +552,8 @@ namespace Barotrauma #if DEBUG if (handlePos[i].LengthSquared() > ArmLength) { - DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)"); + DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)", + item.Prefab.ContentPackage); } #endif HandIK( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 91924e625..831287a4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -10,6 +10,17 @@ namespace Barotrauma { class HumanoidAnimController : AnimController { + private const float SteepestWalkableSlopeAngleDegrees = 50f; + private const float SlowlyWalkableSlopeAngleDegrees = 30f; + + private static readonly float SteepestWalkableSlopeNormalX = + MathF.Sin(MathHelper.ToRadians(SteepestWalkableSlopeAngleDegrees)); + private static readonly float SlowlyWalkableSlopeNormalX = + MathF.Sin(MathHelper.ToRadians(SlowlyWalkableSlopeAngleDegrees)); + + private const float MaxSpeedOnStairs = 1.7f; + private const float SteepSlopePushMagnitude = MaxSpeedOnStairs; + public override RagdollParams RagdollParams { get { return HumanRagdollParams; } @@ -150,7 +161,7 @@ namespace Barotrauma private readonly float movementLerp; - private float cprAnimTimer,cprPump; + private float cprAnimTimer, cprPumpTimer; private float fallingProneAnimTimer; const float FallingProneAnimDuration = 1.0f; @@ -243,14 +254,17 @@ namespace Barotrauma if (MainLimb == null) { return; } levitatingCollider = !IsHanging; - if ((character.SelectedItem?.GetComponent()?.ControlCharacterPose ?? false) || - (character.SelectedSecondaryItem?.GetComponent()?.ControlCharacterPose ?? false) || - character.SelectedSecondaryItem?.GetComponent() != null || - (ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined)) + if (onGround && character.CanMove) { - Crouching = false; + if ((character.SelectedItem?.GetComponent()?.ControlCharacterPose ?? false) || + (character.SelectedSecondaryItem?.GetComponent()?.ControlCharacterPose ?? false) || + character.SelectedSecondaryItem?.GetComponent() != null || + (ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined)) + { + Crouching = false; + } + ColliderIndex = Crouching && !swimming ? 1 : 0; } - ColliderIndex = Crouching && !swimming ? 1 : 0; //stun (= disable the animations) if the ragdoll receives a large enough impact if (strongestImpact > 0.0f) @@ -276,7 +290,7 @@ namespace Barotrauma if (!character.CanMove) { - if (fallingProneAnimTimer < FallingProneAnimDuration) + if (fallingProneAnimTimer < FallingProneAnimDuration && onGround) { fallingProneAnimTimer += deltaTime; UpdateFallingProne(1.0f); @@ -285,7 +299,12 @@ namespace Barotrauma Collider.FarseerBody.FixedRotation = false; if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { - Collider.Enabled = false; + if (Collider.Enabled) + { + //deactivating the collider -> make the main limb inherit the collider's velocity because it'll control the movement now + MainLimb.body.LinearVelocity = Collider.LinearVelocity; + Collider.Enabled = false; + } Collider.LinearVelocity = MainLimb.LinearVelocity; Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation); //reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving @@ -386,6 +405,12 @@ namespace Barotrauma DragCharacter(character.SelectedCharacter, deltaTime); } + if (Anim != Animation.CPR) + { + cprAnimTimer = 0.0f; + cprPumpTimer = 0.0f; + } + switch (Anim) { case Animation.Climbing: @@ -487,10 +512,14 @@ namespace Barotrauma Limb leftLeg = GetLimb(LimbType.LeftLeg); Limb rightLeg = GetLimb(LimbType.RightLeg); + bool onSlopeThatMakesSlow = Math.Abs(floorNormal.X) > SlowlyWalkableSlopeNormalX; + bool slowedDownBySlope = onSlopeThatMakesSlow && Math.Sign(floorNormal.X) == -Math.Sign(TargetMovement.X); + bool onSlopeTooSteepToClimb = Math.Abs(floorNormal.X) > SteepestWalkableSlopeNormalX; + float walkCycleMultiplier = 1.0f; - if (Stairs != null) + if (Stairs != null || slowedDownBySlope) { - TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -1.7f, 1.7f), TargetMovement.Y); + TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -MaxSpeedOnStairs, MaxSpeedOnStairs), TargetMovement.Y); walkCycleMultiplier *= 1.5f; } @@ -572,6 +601,15 @@ namespace Barotrauma bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f); + if (Stairs == null && onSlopeTooSteepToClimb) + { + if (Math.Sign(targetMovement.X) != Math.Sign(floorNormal.X)) + { + targetMovement.X = Math.Sign(floorNormal.X) * SteepSlopePushMagnitude; + movement = targetMovement; + } + } + if (Stairs != null || onSlope) { torso.PullJointWorldAnchorB = new Vector2( @@ -648,14 +686,6 @@ namespace Barotrauma if (!onGround) { - Vector2 move = torso.PullJointWorldAnchorB - torso.SimPosition; - - foreach (Limb limb in Limbs) - { - if (limb.IsSevered) { continue; } - MoveLimb(limb, limb.SimPosition + move, 15.0f, true); - } - return; } @@ -758,7 +788,7 @@ namespace Barotrauma { footPos = new Vector2(colliderPos.X + stepSize.X * i * 0.2f, colliderPos.Y - 0.1f); } - if (Stairs == null) + if (Stairs == null && !onSlopeThatMakesSlow) { footPos.Y = Math.Max(Math.Min(FloorY, footPos.Y + 0.5f), footPos.Y); } @@ -1318,14 +1348,14 @@ namespace Barotrauma } } - void UpdateFallingProne(float strength) + void UpdateFallingProne(float strength, bool moveHands = true, bool moveTorso = true, bool moveLegs = true) { if (strength <= 0.0f) { return; } Limb head = GetLimb(LimbType.Head); Limb torso = GetLimb(LimbType.Torso); - if (head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered) + if (moveHands && head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered) { //if the head is moving, try to protect it with the hands Limb leftHand = GetLimb(LimbType.LeftHand); @@ -1347,7 +1377,7 @@ namespace Barotrauma //make the torso tip over //otherwise it tends to just drop straight down, pinning the characters legs in a weird pose - if (!InWater) + if (moveTorso && !InWater) { //prefer tipping over in the same direction the torso is rotating //or moving @@ -1358,27 +1388,30 @@ namespace Barotrauma } //attempt to make legs stay in a straight line with the torso to prevent the character from doing a split - for (int i = 0; i < 2; i++) + if (moveLegs) { - var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh); - if (thigh == null) { continue; } - if (thigh.IsSevered) { continue; } - float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation)); - float diff = torso.Rotation - thigh.Rotation; - if (MathUtils.IsValid(diff)) + for (int i = 0; i < 2; i++) { - float thighTorque = thighDiff * thigh.Mass * Math.Sign(diff) * 5.0f; - thigh.body.ApplyTorque(thighTorque * strength); - } + var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh); + if (thigh == null) { continue; } + if (thigh.IsSevered) { continue; } + float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation)); + float diff = torso.Rotation - thigh.Rotation; + if (MathUtils.IsValid(diff)) + { + float thighTorque = thighDiff * thigh.Mass * Math.Sign(diff) * 5.0f; + thigh.body.ApplyTorque(thighTorque * strength); + } - var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg); - if (leg == null || leg.IsSevered) { continue; } - float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation)); - diff = torso.Rotation - leg.Rotation; - if (MathUtils.IsValid(diff)) - { - float legTorque = legDiff * leg.Mass * Math.Sign(diff) * 5.0f; - leg.body.ApplyTorque(legTorque * strength); + var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg); + if (leg == null || leg.IsSevered) { continue; } + float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation)); + diff = torso.Rotation - leg.Rotation; + if (MathUtils.IsValid(diff)) + { + float legTorque = legDiff * leg.Mass * Math.Sign(diff) * 5.0f; + leg.body.ApplyTorque(legTorque * strength); + } } } } @@ -1398,7 +1431,8 @@ namespace Barotrauma Crouching = true; - Vector2 diff = target.SimPosition - character.SimPosition; + Vector2 offset = Vector2.UnitX * -Dir * 0.75f; + Vector2 diff = (target.SimPosition + offset) - character.SimPosition; Limb targetHead = target.AnimController.GetLimb(LimbType.Head); Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso); if (targetTorso == null) @@ -1412,7 +1446,23 @@ namespace Barotrauma Vector2 headDiff = targetHead == null ? diff : targetHead.SimPosition - character.SimPosition; targetMovement = new Vector2(diff.X, 0.0f); + const float CloseEnough = 0.1f; + if (Math.Abs(targetMovement.X) < CloseEnough) + { + targetMovement.X = 0.0f; + } + TargetDir = headDiff.X > 0.0f ? Direction.Right : Direction.Left; + //if the target's in some weird pose, we may not be able to flip it so it's facing up, + //so let's only try it once so we don't end up constantly flipping it + if (cprAnimTimer <= 0.0f && target.AnimController.Direction == TargetDir) + { + target.AnimController.Flip(); + } + (target.AnimController as HumanoidAnimController)?.UpdateFallingProne(strength: 1.0f, moveHands: false, moveTorso: false); + + head.Disabled = true; + torso.Disabled = true; UpdateStanding(); @@ -1443,73 +1493,64 @@ namespace Barotrauma } } - //pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17) - if (cprAnimTimer > 15.0f && targetHead != null && head != null) + //Serverside code + if (GameMain.NetworkMember is not { IsClient: true }) { - float yPos = (float)Math.Sin(cprAnimTimer) * 0.2f; - head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.3f + yPos); + if (target.Oxygen < -10.0f) + { + //stabilize the oxygen level but don't allow it to go positive and revive the character yet + float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill; + stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax); + character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required + if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we + } + } + + if (targetHead != null && head != null) + { + head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.8f); head.PullJointEnabled = true; - torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.2f)); - torso.PullJointEnabled = true; - - //Serverside code - if (GameMain.NetworkMember is not { IsClient: true }) - { - if (target.Oxygen < -10.0f) - { - //stabilize the oxygen level but don't allow it to go positive and revive the character yet - float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill; - stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax); - character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required - if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we - } - } } - else + + torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.1f)); + torso.PullJointEnabled = true; + + if (cprPumpTimer >= 1) { - if (targetHead != null && head != null) + torso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + cprPumpTimer = 0; + + if (skill < CPRSettings.Active.DamageSkillThreshold) { - head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.8f); - head.PullJointEnabled = true; + target.LastDamageSource = null; + target.DamageLimb( + targetTorso.WorldPosition, targetTorso, + new[] { CPRSettings.Active.InsufficientSkillAffliction.Instantiate((CPRSettings.Active.DamageSkillThreshold - skill) * CPRSettings.Active.DamageSkillMultiplier, source: character) }, + stun: 0.0f, + playSound: true, + attackImpulse: Vector2.Zero, + attacker: null); } - - torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.1f)); - torso.PullJointEnabled = true; - - if (cprPump >= 1) + //need to CPR for at least a couple of seconds before the target can be revived + //(reviving the target when the CPR has barely started looks strange) + if (cprAnimTimer > 2.0f && GameMain.NetworkMember is not { IsClient: true }) { - torso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - cprPump = 0; + float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill; + reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent); + reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax); + reviveChance *= 1f + cprBoost; - if (skill < CPRSettings.Active.DamageSkillThreshold) + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance) { - target.LastDamageSource = null; - target.DamageLimb( - targetTorso.WorldPosition, targetTorso, - new[] { CPRSettings.Active.InsufficientSkillAffliction.Instantiate((CPRSettings.Active.DamageSkillThreshold - skill) * CPRSettings.Active.DamageSkillMultiplier, source: character) }, - 0.0f, true, 0.0f, attacker: null); - } - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code - { - float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill; - reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent); - reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax); - - reviveChance *= 1f + cprBoost; - - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance) - { - //increase oxygen and clamp it above zero - // -> the character should be revived if there are no major afflictions in addition to lack of oxygen - target.Oxygen = Math.Max(target.Oxygen + 10.0f, 10.0f); - } + //increase oxygen and clamp it above zero + // -> the character should be revived if there are no major afflictions in addition to lack of oxygen + target.Oxygen = Math.Max(target.Oxygen + 10.0f, 10.0f); } } - cprPump += deltaTime; } - - cprAnimTimer = (cprAnimTimer + deltaTime) % 17; + cprPumpTimer += deltaTime; + cprAnimTimer += deltaTime; //got the character back into a non-critical state, increase medical skill //BUT only if it has been more than 10 seconds since the character revived someone @@ -1519,7 +1560,7 @@ namespace Barotrauma target.CharacterHealth.CalculateVitality(); if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f) { - character.Info?.IncreaseSkillLevel("medical".ToIdentifier(), SkillSettings.Current.SkillIncreasePerCprRevive); + character.Info?.ApplySkillGain(Tags.MedicalSkill, SkillSettings.Current.SkillIncreasePerCprRevive); SteamAchievementManager.OnCharacterRevived(target, character); lastReviveTime = (float)Timing.TotalTime; #if SERVER @@ -1724,7 +1765,23 @@ namespace Barotrauma targetAnchor += target.Submarine.SimPosition; } } - pullLimb.PullJointWorldAnchorB = pullLimbAnchor; + if (Vector2.DistanceSquared(pullLimb.PullJointWorldAnchorA, pullLimbAnchor) > 50.0f * 50.0f) + { + //there's a similar error check in the PullJointWorldAnchorB setter, but we seem to be getting quite a lot of + //errors specifically from this method, so let's use a more consistent error message here to prevent clogging GA with + //different error messages that all include a different coordinate + string errorMsg = + $"Attempted to move the anchor B of a limb's pull joint extremely far from the limb in {nameof(DragCharacter)}. " + + $"Character in sub: {character.Submarine != null}, target in sub: {target.Submarine != null}."; + GameAnalyticsManager.AddErrorEventOnce("DragCharacter:PullJointTooFar", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#endif + } + else + { + pullLimb.PullJointWorldAnchorB = pullLimbAnchor; + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index c9ac1f88b..ffebedae2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -437,7 +437,18 @@ namespace Barotrauma foreach (var huskAppendage in mainElement.GetChildElements("huskappendage")) { if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; } - AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty), huskAppendage, ragdoll: this); + + Identifier afflictionIdentifier = huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty); + if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab affliction) || + affliction is not AfflictionPrefabHusk matchingAffliction) + { + DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!", + contentPackage: huskAppendage.ContentPackage); + } + else + { + AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskAppendage, ragdoll: this); + } } } } @@ -561,6 +572,10 @@ namespace Barotrauma public void AddJoint(JointParams jointParams) { + if (!checkLimbIndex(jointParams.Limb2, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2")) + { + return; + } LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this); GameMain.World.Add(joint.Joint); for (int i = 0; i < LimbJoints.Length; i++) @@ -571,6 +586,21 @@ namespace Barotrauma } Array.Resize(ref LimbJoints, LimbJoints.Length + 1); LimbJoints[LimbJoints.Length - 1] = joint; + + bool checkLimbIndex(int index, string debugName) + { + if (index < 0 || index >= limbs.Length) + { + string errorMsg = $"Failed to add a joint to character {character.Name}. {debugName} out of bounds (index: {index}, limbs: {limbs.Length}."; + DebugConsole.ThrowError(errorMsg, contentPackage: jointParams.Element?.ContentPackage); + if (jointParams.Element?.ContentPackage == GameMain.VanillaContent) + { + GameAnalyticsManager.AddErrorEventOnce("Ragdoll.AddJoint:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } + return false; + } + return true; + } } protected void AddLimb(LimbParams limbParams) @@ -657,6 +687,13 @@ namespace Barotrauma } } + private enum LimbStairCollisionResponse + { + DontClimbStairs, + ClimbWithoutLimbCollision, + ClimbWithLimbCollision + } + public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact) { if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; } @@ -699,37 +736,53 @@ namespace Barotrauma } else if (structure.StairDirection != Direction.None) { - Stairs = null; - - //don't collider with stairs if - - //1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards - float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10); - if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return false; } - - //2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards - if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return false; } - - //3. collided with the stairs from below - if (contact.Manifold.LocalNormal.Y < 0.0f) { return false; } - - //4. contact points is above the bottom half of the collider - contact.GetWorldManifold(out Vector2 normal, out FarseerPhysics.Common.FixedArray2 points); - if (points[0].Y > Collider.SimPosition.Y) { return false; } - - //5. in water - if (inWater && targetMovement.Y < 0.5f) { return false; } - - //--------------- - - //set stairs to that of the one dragging us if (character.SelectedBy != null) + { Stairs = character.SelectedBy.AnimController.Stairs; + } else - Stairs = structure; + { + var collisionResponse = handleLimbStairCollision(); + if (collisionResponse == LimbStairCollisionResponse.ClimbWithLimbCollision) + { + Stairs = structure; + } + else + { + if (collisionResponse == LimbStairCollisionResponse.DontClimbStairs) { Stairs = null; } - if (Stairs == null) - return false; + return false; + } + } + + LimbStairCollisionResponse handleLimbStairCollision() + { + //don't collide with stairs if + + //1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards + float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10); + if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; } + + //2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards + if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return LimbStairCollisionResponse.DontClimbStairs; } + + //3. collided with the stairs from below + if (contact.Manifold.LocalNormal.Y < 0.0f) + { + return Stairs != structure + ? LimbStairCollisionResponse.DontClimbStairs + : LimbStairCollisionResponse.ClimbWithoutLimbCollision; + } + + //4. contact points is above the bottom half of the collider + contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2 points); + if (points[0].Y > Collider.SimPosition.Y) { return LimbStairCollisionResponse.DontClimbStairs; } + + //5. in water + if (inWater && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; } + + return LimbStairCollisionResponse.ClimbWithLimbCollision; + } } lock (impactQueue) @@ -1324,17 +1377,28 @@ namespace Barotrauma if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance) { 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) + + const float LevitationSpeedMultiplier = 5f; + + // If the character is walking down a slope, target a position that moves along it + float slopePull = 0f; + if (floorNormal.Y is > 0f and < 1f + && Math.Sign(movement.X) == Math.Sign(floorNormal.X)) { - if (Stairs != null) + slopePull = Math.Abs(movement.X * floorNormal.X / floorNormal.Y) / LevitationSpeedMultiplier; + } + + if (Math.Abs(Collider.SimPosition.Y - targetY - slopePull) > 0.01f) + { + float yVelocity = (targetY - Collider.SimPosition.Y) * LevitationSpeedMultiplier; + if (Stairs != null && targetY < Collider.SimPosition.Y) { - 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); + yVelocity = Math.Sign(yVelocity); } + + yVelocity -= slopePull * LevitationSpeedMultiplier; + + Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, yVelocity); } } else @@ -1582,10 +1646,10 @@ namespace Barotrauma // 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) + private void RefreshFloorY(float deltaTime, bool ignoreStairs = false) { floorYCheckTimer -= deltaTime; - PhysicsBody refBody = refLimb == null ? Collider : refLimb.body; + PhysicsBody refBody = Collider; if (floorYCheckTimer < 0 || lastFloorCheckIgnoreStairs != ignoreStairs || lastFloorCheckIgnorePlatforms != IgnorePlatforms || @@ -1610,7 +1674,7 @@ namespace Barotrauma if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); } if (TorsoPosition.HasValue && MathUtils.IsValid(TorsoPosition.Value)) { height = Math.Max(height, TorsoPosition.Value); } - Vector2 rayEnd = rayStart - new Vector2(0.0f, height); + Vector2 rayEnd = rayStart - new Vector2(0.0f, height * 2f); Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom()); Fixture standOnFloorFixture = null; @@ -1689,7 +1753,7 @@ namespace Barotrauma } } - if (closestFraction == 1) //raycast didn't hit anything + if (closestFraction >= 1) //raycast didn't hit anything { floorNormal = Vector2.UnitY; if (CurrentHull == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index f3397c089..8bb5d51ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -1,11 +1,12 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Items.Components; namespace Barotrauma -{ +{ public enum HitDetection { Distance, @@ -391,7 +392,8 @@ namespace Barotrauma element.GetAttribute("burndamage") != null || element.GetAttribute("bleedingdamage") != null) { - DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. )."); + DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. ).", + contentPackage: element.ContentPackage); } //if level wall damage is not defined, default to the structure damage @@ -414,12 +416,14 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab; if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names."); + DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.", + contentPackage: element.ContentPackage); string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant(); afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, System.StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { - DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found."); + DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.", + contentPackage: element.ContentPackage); continue; } } @@ -428,7 +432,8 @@ namespace Barotrauma Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out afflictionPrefab)) { - DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found."); + DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.", + contentPackage: element.ContentPackage); continue; } } @@ -441,7 +446,7 @@ namespace Barotrauma } partial void InitProjSpecific(ContentXElement element); - public void ReloadAfflictions(XElement element, string parentDebugName) + public void ReloadAfflictions(ContentXElement element, string parentDebugName) { Afflictions.Clear(); foreach (var subElement in element.GetChildElements("affliction")) @@ -450,13 +455,14 @@ namespace Barotrauma Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab afflictionPrefab)) { - DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\"."); + DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\".", + contentPackage: element.ContentPackage); continue; } affliction = afflictionPrefab.Instantiate(0.0f); affliction.Deserialize(subElement); //backwards compatibility - if (subElement.Attribute("amount") != null && subElement.Attribute("strength") == null) + if (subElement.GetAttribute("amount") != null && subElement.GetAttribute("strength") == null) { affliction.Strength = subElement.GetAttributeFloat("amount", 0.0f); } @@ -465,7 +471,7 @@ namespace Barotrauma } } - public void Serialize(XElement element) + public void Serialize(ContentXElement element) { SerializableProperty.SerializeProperties(this, element, true); foreach (var affliction in Afflictions) @@ -477,7 +483,7 @@ namespace Barotrauma } } - public void Deserialize(XElement element, string parentDebugName) + public void Deserialize(ContentXElement element, string parentDebugName) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); ReloadAfflictions(element, parentDebugName); @@ -497,8 +503,9 @@ namespace Barotrauma SetUser(attacker); DamageParticles(deltaTime, worldPosition); - - var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult(); + + Vector2 impulseDirection = GetImpulseDirection(target as ISpatialEntity, worldPosition, SourceItem); + var attackResult = target?.AddDamage(attacker, worldPosition, this, impulseDirection, deltaTime, playSound) ?? new AttackResult(); var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure; var additionalEffectType = ActionType.OnUse; if (targetCharacter != null && targetCharacter.IsDead) @@ -606,7 +613,7 @@ namespace Barotrauma float penetration = Penetration; RangedWeapon weapon = - SourceItem?.GetComponent() ?? + SourceItem?.GetComponent() ?? SourceItem?.GetComponent()?.Launcher?.GetComponent(); float? penetrationValue = weapon?.Penetration; if (penetrationValue.HasValue) @@ -614,7 +621,8 @@ namespace Barotrauma penetration += penetrationValue.Value; } - var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration); + Vector2 impulseDirection = GetImpulseDirection(targetLimb, worldPosition, SourceItem); + var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, impulseDirection, playSound, targetLimb, penetration); var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure; foreach (StatusEffect effect in statusEffects) @@ -666,6 +674,34 @@ namespace Barotrauma return attackResult; } + private Vector2 GetImpulseDirection(ISpatialEntity target, Vector2 sourceWorldPosition, Item sourceItem) + { + Vector2 impulseDirection = Vector2.Zero; + if (target != null) + { + impulseDirection = target.WorldPosition - sourceWorldPosition; + } + + if (sourceItem?.body != null && sourceItem.body.Enabled && sourceItem.body.LinearVelocity.LengthSquared() > 0.0f) + { + impulseDirection = sourceItem.body.LinearVelocity; + } + else + { + var projectileComponent = sourceItem?.GetComponent(); + if (projectileComponent != null) + { + impulseDirection = new Vector2(MathF.Cos(SourceItem.Rotation), MathF.Sin(SourceItem.Rotation)); + } + } + + if (impulseDirection.LengthSquared() > 0.0001f) + { + impulseDirection = Vector2.Normalize(impulseDirection); + } + return impulseDirection; + } + public float AttackTimer { get; private set; } public float CoolDownTimer { get; set; } public float CurrentRandomCoolDown { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 679e76a1b..21ce51013 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -67,10 +67,18 @@ namespace Barotrauma } foreach (var item in HeldItems) { - if (item.body != null) + if (item.body == null) { continue; } + if (!enabled) { - item.body.Enabled = enabled; + item.body.Enabled = false; } + else if (item.GetComponent() is { IsActive: true }) + { + //held items includes all items in hand slots + //we only want to enable the physics body if it's an actual holdable item, not e.g. a wearable item like handcuffs + item.body.Enabled = true; + } + } AnimController.Collider.Enabled = value; } @@ -936,10 +944,16 @@ namespace Barotrauma { var prevSelectedItem = _selectedItem; _selectedItem = value; + if (value is not null) + { + CheckTalents(AbilityEffectType.OnItemSelected, new AbilityItemSelected(value)); + } #if CLIENT HintManager.OnSetSelectedItem(this, prevSelectedItem, _selectedItem); if (Controlled == this) { + _selectedItem?.GetComponent()?.RefreshSelectedItem(); + if (_selectedItem == null) { GameMain.GameSession?.CrewManager?.ResetCrewList(); @@ -1098,6 +1112,15 @@ namespace Barotrauma set { CharacterHealth.Unkillable = value; } } + /// + /// Is the health interface available on this character? Can be used by status effects + /// + public bool UseHealthWindow + { + get { return CharacterHealth.UseHealthWindow; } + set { CharacterHealth.UseHealthWindow = value; } + } + public CampaignMode.InteractionType CampaignInteractionType; public Identifier MerchantIdentifier; @@ -1278,7 +1301,8 @@ namespace Barotrauma { if (!VariantOf.IsEmpty) { - DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!"); + DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!", + contentPackage: Prefab.ContentPackage); } if (characterInfo == null) { @@ -1402,7 +1426,8 @@ namespace Barotrauma if (matchingAffliction == null || nonHuskedSpeciesName.IsEmpty) { DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition!\n" - + "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\""); + + "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\"", + contentPackage: Prefab.ContentPackage); // Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg. nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier(); speciesName = nonHuskedSpeciesName; @@ -1683,31 +1708,21 @@ namespace Barotrauma info.Job?.GiveJobItems(this, spawnPoint); } - - public void GiveIdCardTags(WayPoint spawnPoint, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false) + public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false) { - GiveIdCardTags(spawnPoint.ToEnumerable(), requireSpawnPointTagsNotGiven, createNetworkEvent); - } - - public void GiveIdCardTags(IEnumerable spawnPoints, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false) - { - if (info?.Job == null || spawnPoints == null) { return; } + if (info?.Job == null || spawnPoint == null) { return; } foreach (Item item in Inventory.AllItems) { - if (item?.GetComponent() is not IdCard idCard) { continue; } - if (requireSpawnPointTagsNotGiven) + var idCard = item?.GetComponent(); + if (idCard == null) { continue; } + //if the card belongs to someone else, don't add any tags. + //otherwise you can gain access to places you shouldn't by temporarily giving the card to someone (e.g. a captain bot) at the end of the round + if (idCard.OwnerName != info.Name) { continue; } + foreach (string s in spawnPoint.IdCardTags) { - if (idCard.SpawnPointTagsGiven) { continue; } + item.AddTag(s); } - foreach (var spawnPoint in spawnPoints) - { - foreach (string s in spawnPoint.IdCardTags) - { - item.AddTag(s); - } - } - idCard.SpawnPointTagsGiven = true; if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true }) { GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item)); @@ -2292,40 +2307,40 @@ namespace Barotrauma return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb; } - public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool checkFacing = false) + public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool seeThroughWindows = false, bool checkFacing = false) { seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb(); if (target is Character targetCharacter) { - return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing); + return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing); } else { - return CheckVisibility(target, seeingEntity, checkFacing); + return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing); } } - public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false) + public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false) { if (seeingEntity is Character seeingCharacter) { - return seeingCharacter.CanSeeTarget(target, checkFacing: checkFacing); + return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing); } if (target is Character targetCharacter) { - return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing); + return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing); } else { - return CheckVisibility(target, seeingEntity, checkFacing); + return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing); } } - private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool checkFacing = false) + private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false) { System.Diagnostics.Debug.Assert(target != null); if (target == null || target.Removed) { return false; } - if (CheckVisibility(target, seeingEntity, checkFacing)) { return true; } + if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; } if (!target.AnimController.SimplePhysicsEnabled) { //find the limbs that are furthest from the target's position (from the viewer's point of view) @@ -2354,13 +2369,13 @@ namespace Barotrauma continue; } } - if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, checkFacing)) { return true; } - if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, checkFacing)) { return true; } + if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; } + if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; } } return false; } - private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false) + private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false) { System.Diagnostics.Debug.Assert(target != null); if (target == null) { return false; } @@ -2371,38 +2386,41 @@ namespace Barotrauma { if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; } } - Body closestBody; //both inside the same sub (or both outside) //OR the we're inside, the other character outside if (target.Submarine == seeingEntity.Submarine || target.Submarine == null) { - closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); + return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null; } //we're outside, the other character inside else if (seeingEntity.Submarine == null) { - closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff); + return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null; } //both inside different subs else { - closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); - if (!IsBlocking(closestBody)) - { - closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff); - } + return + Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null && + Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null; } - return !IsBlocking(closestBody); - bool IsBlocking(Body body) + bool IsBlocking(Fixture f) { + var body = f.Body; if (body == null) { return false; } - if (body.UserData is Structure wall && wall.CastShadow) + if (body.UserData is Structure wall) { + if (!wall.CastShadow && seeThroughWindows) { return false; } return wall != target; } else if (body.UserData is Item item) { + if (item.GetComponent() is { HasWindow: true } door && seeThroughWindows) + { + if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; } + } + return item != target; } return true; @@ -2497,9 +2515,21 @@ namespace Barotrauma if (inventory.Owner is Item item) { - if (!CanInteractWith(item) && !item.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; } - ItemContainer container = item.GetComponents().FirstOrDefault(ic => ic.Inventory == inventory); - if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; } + if (!CanInteractWith(item)) + { + //could be simplified with LINQ, but that'd require capturing variables which we shouldn't do in a method that's called as frequently as this + foreach (var linkedEntity in item.linkedTo) + { + if (linkedEntity is Item linkedItem && linkedItem.DisplaySideBySideWhenLinked && CanInteractWith(linkedItem)) { return true; } + } + return false; + } + ItemContainer container = (inventory as ItemInventory)?.Container; + if (container != null) + { + if (!container.HasRequiredItems(this, addMessage: false)) { return false; } + if (!container.DrawInventory) { return false; } + } } return true; } @@ -2764,9 +2794,17 @@ namespace Barotrauma if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger) { var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true); - if (body != null && body.UserData as Item != item && (body.UserData as ItemComponent)?.Item != item && Submarine.LastPickedFixture?.UserData as Item != item) - { - return false; + if (body != null) + { + var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item; + if (otherItem != item && + (body.UserData as ItemComponent)?.Item != item && + /*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/ + otherItem?.GetComponent() is not { IsOpen: true } && + Submarine.LastPickedFixture?.UserData as Item != item) + { + return false; + } } } @@ -2793,7 +2831,12 @@ namespace Barotrauma public void DeselectCharacter() { if (SelectedCharacter == null) { return; } - SelectedCharacter.AnimController?.ResetPullJoints(); + if (!SelectedCharacter.AllowInput) + { + //we cannot reset the pull joints if the target is conscious (moving on its own), + //that'd interfere with its animations + SelectedCharacter.AnimController?.ResetPullJoints(); + } SelectedCharacter = null; } @@ -3297,10 +3340,7 @@ namespace Barotrauma IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; } } - if (IsRagdolled) - { - SetInput(InputType.Ragdoll, false, true); - } + SetInput(InputType.Ragdoll, false, IsRagdolled); } if (!wasRagdolled && IsRagdolled) { @@ -3558,6 +3598,8 @@ namespace Barotrauma private void Despawn(bool createNetworkEvents = true) { + if (!EnableDespawn) { return; } + Identifier despawnContainerId = IsHuman ? "despawncontainer".ToIdentifier() : @@ -3639,10 +3681,12 @@ namespace Barotrauma float massFactor = (float)Math.Sqrt(Mass / 20); float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange); float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed); + newRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier); if (!float.IsNaN(newRange)) { aiTarget.SightRange = newRange; } + } private void UpdateSoundRange(float deltaTime) @@ -3657,6 +3701,7 @@ namespace Barotrauma float massFactor = (float)Math.Sqrt(Mass / 10); float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange); float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed); + newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier); if (!float.IsNaN(newRange)) { aiTarget.SoundRange = newRange; @@ -3976,15 +4021,15 @@ namespace Barotrauma CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount); } - public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true) { - return ApplyAttack(attacker, worldPosition, attack, deltaTime, playSound, null); + return ApplyAttack(attacker, worldPosition, attack, deltaTime, impulseDirection, playSound); } /// /// Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to worldPosition will receive the damage. /// - public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null, float penetration = 0f) + public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, Vector2 impulseDirection, bool playSound = false, Limb targetLimb = null, float penetration = 0f) { if (Removed) { @@ -3996,7 +4041,16 @@ namespace Barotrauma Limb limbHit = targetLimb; - float attackImpulse = attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier * deltaTime; + float impulseMagnitude = (attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier) * deltaTime; + + Vector2 attackImpulse = Vector2.Zero; + if (Math.Abs(impulseMagnitude) > 0.0f) + { + impulseDirection = impulseDirection.LengthSquared() > 0.0001f ? + Vector2.Normalize(impulseDirection) : + Vector2.UnitX; + attackImpulse = impulseDirection * impulseMagnitude; + } AbilityAttackData attackData = new AbilityAttackData(attack, this, attacker); IEnumerable attackAfflictions; @@ -4125,12 +4179,12 @@ namespace Barotrauma } } - public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null, float damageMultiplier = 1f) + public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, Vector2? attackImpulse = null, Character attacker = null, float damageMultiplier = 1f) { - return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker, damageMultiplier: damageMultiplier); + return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse ?? Vector2.Zero, out _, attacker, damageMultiplier: damageMultiplier); } - public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1) + public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, Vector2 attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1) { hitLimb = null; @@ -4163,7 +4217,7 @@ namespace Barotrauma 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) + public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false) { if (Removed) { return new AttackResult(); } @@ -4196,18 +4250,17 @@ namespace Barotrauma } Vector2 dir = hitLimb.WorldPosition - worldPosition; - if (Math.Abs(attackImpulse) > 0.0f) + if (attackImpulse.LengthSquared() > 0.0f) { Vector2 diff = dir; if (diff == Vector2.Zero) { diff = Rand.Vector(1.0f); } - Vector2 impulse = Vector2.Normalize(diff) * attackImpulse; Vector2 hitPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(diff); - hitLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); + hitLimb.body.ApplyLinearImpulse(attackImpulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); var mainLimb = hitLimb.character.AnimController.MainLimb; if (hitLimb != mainLimb) { // Always add force to mainlimb - mainLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + mainLimb.body.ApplyLinearImpulse(attackImpulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } } bool wasDead = IsDead; @@ -4306,19 +4359,16 @@ namespace Barotrauma } if (medicalDamage > 0) { - IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage); + IncreaseSkillLevel(Tags.MedicalSkill, medicalDamage); } if (weaponDamage > 0) { - IncreaseSkillLevel("weapons".ToIdentifier(), weaponDamage); + IncreaseSkillLevel(Tags.WeaponsSkill, weaponDamage); } void IncreaseSkillLevel(Identifier skill, float damage) { - float attackerSkillLevel = attacker.GetSkillLevel(skill); - // The formula is too generous on low skill levels, hence the minimum divider. - float minSkillDivider = 15f; - attacker.Info?.IncreaseSkillLevel(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, minSkillDivider)); + attacker.Info?.ApplySkillGain(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage, false, 1f); } } @@ -4332,12 +4382,10 @@ namespace Barotrauma { medicalGain += affliction.Strength * affliction.Prefab.MedicalSkillGain; } - if (medicalGain <= 0) { return; } - Identifier skill = new Identifier("medical"); - float attackerSkillLevel = healer.GetSkillLevel(skill); - // The formula is too generous on low skill levels, hence the minimum divider. - float minSkillDivider = 15f; - healer.Info?.IncreaseSkillLevel(skill, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, minSkillDivider)); + if (medicalGain > 0) + { + healer.Info?.ApplySkillGain(Tags.MedicalItem, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed); + } } /// @@ -4971,8 +5019,10 @@ namespace Barotrauma private readonly List visibleHulls = new List(); private readonly HashSet tempList = new HashSet(); + /// - /// Returns hulls that are visible to the player, including the current hull. + /// Returns hulls that are visible to the character, including the current hull. + /// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls. /// Can be heavy if used every frame. /// public List GetVisibleHulls() @@ -4986,7 +5036,9 @@ namespace Barotrauma float maxDistance = 1000f; foreach (var hull in adjacentHulls) { - if (hull.ConnectedGaps.Any(g => g.Open > 0.9f && g.linkedTo.Contains(CurrentHull) && + if (hull.ConnectedGaps.Any(g => + g.Open > 0.9f && + g.linkedTo.Contains(CurrentHull) && Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2))) { if (Vector2.DistanceSquared(hull.WorldPosition, WorldPosition) < Math.Pow(maxDistance, 2)) @@ -5027,7 +5079,7 @@ namespace Barotrauma public bool IsEngineer => HasJob("engineer"); public bool IsMechanic => HasJob("mechanic"); public bool IsMedic => HasJob("medicaldoctor"); - public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer"); + public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer") || HasJob("outpostsecurityofficer"); public bool IsAssistant => HasJob("assistant"); public bool IsWatchman => HasJob("watchman"); public bool IsVip => HasJob("prisoner"); @@ -5546,4 +5598,12 @@ namespace Barotrauma public Character Character { get; set; } } + class AbilityItemSelected : AbilityObject, IAbilityItem + { + public AbilityItemSelected(Item item) + { + Item = item; + } + public Item Item { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 61f4df9c7..2b8466406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -767,7 +767,7 @@ namespace Barotrauma } // Used for loading the data - public CharacterInfo(XElement infoElement, Identifier npcIdentifier = default) + public CharacterInfo(ContentXElement infoElement, Identifier npcIdentifier = default) { ID = idCounter; idCounter++; @@ -1214,6 +1214,21 @@ namespace Barotrauma return (int)(salary * Job.Prefab.PriceMultiplier); } + /// + /// Increases the characters skill at a rate proportional to their current skill. + /// If you want to increase the skill level by a specific amount instead, use + /// + public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f) + { + float skillLevel = Job.GetSkillLevel(skillIdentifier); + // The formula is too generous on low skill levels, hence the minimum divider. + float skillDivider = MathF.Pow(Math.Max(skillLevel, 15f), SkillSettings.Current.SkillIncreaseExponent); + IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility); + } + + /// + /// Increase the skill by a specific amount. Talents may affect the actual, final skill increase. + /// public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false) { if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } @@ -1222,9 +1237,7 @@ namespace Barotrauma { increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier; } - increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed); - increase = GetSkillSpecificGain(increase, skillIdentifier); float prevLevel = Job.GetSkillLevel(skillIdentifier); @@ -1311,12 +1324,12 @@ namespace Barotrauma OnExperienceChanged(prevAmount, ExperiencePoints); } - const int BaseExperienceRequired = -50; + const int BaseExperienceRequired = 450; const int AddedExperienceRequiredPerLevel = 500; public int GetTotalTalentPoints() { - return GetCurrentLevel() + AdditionalTalentPoints - 1; + return GetCurrentLevel() + AdditionalTalentPoints; } public int GetAvailableTalentPoints() @@ -1342,16 +1355,19 @@ namespace Barotrauma return experienceRequired + ExperienceRequiredPerLevel(level); } + /// + /// How much more experience does the character need to reach the specified level? + /// public int GetExperienceRequiredForLevel(int level) { - int currentLevel = GetCurrentLevel(out int experienceRequired); + int currentLevel = GetCurrentLevel(); if (currentLevel >= level) { return 0; } - int required = experienceRequired; - for (int i = currentLevel + 1; i <= level; i++) + int required = 0; + for (int i = 0; i < level; i++) { required += ExperienceRequiredPerLevel(i); } - return required; + return required - ExperiencePoints; } public int GetCurrentLevel() @@ -1361,7 +1377,7 @@ namespace Barotrauma private int GetCurrentLevel(out int experienceRequired) { - int level = 1; + int level = 0; experienceRequired = 0; while (experienceRequired + ExperienceRequiredPerLevel(level) <= ExperiencePoints) { @@ -1899,13 +1915,30 @@ namespace Barotrauma } } + /// + /// Get the combined stat value of the identifier "all" and the specified identifier. + /// + /// + /// The "all" identifier works like the "any" identifier in outpost modules where it doesn't literally mean everything but + /// is an unique identifier that indicates that it should target everything. For example if we wanted to make a talent + /// that increases the fabrication quality of every single item we could use something like: + /// + /// (Granted IncreaseFabricationQuality doesn't support the "all" identifier so if we need this in vanilla it needs to be implemented in code) + /// + public float GetSavedStatValueWithAll(StatTypes statType, Identifier statIdentifier) + => GetSavedStatValue(statType, Tags.StatIdentifierTargetAll) + + GetSavedStatValue(statType, statIdentifier); + public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier) + => GetSavedStatValueWithBotsInMp(statType, statIdentifier, GameSession.GetSessionCrewCharacters(CharacterType.Bot)); + + public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier, IReadOnlyCollection bots) { float statValue = GetSavedStatValue(statType, statIdentifier); if (GameMain.NetworkMember is null) { return statValue; } - foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot)) + foreach (Character bot in bots) { int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier); statValue = Math.Max(statValue, botStatValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index 2dfb6ecf5..acae26e4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -95,7 +95,8 @@ namespace Barotrauma name = ParseName(mainElement, file); if (name == Identifier.Empty) { - DebugConsole.ThrowError($"No species name defined for: {file.Path}"); + DebugConsole.ThrowError($"No species name defined for: {file.Path}", + contentPackage: file.ContentPackage); return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 20a19d64f..4136b6381 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -75,7 +75,8 @@ namespace Barotrauma HuskPrefab = prefab as AfflictionPrefabHusk; if (HuskPrefab == null) { - DebugConsole.ThrowError("Error in husk affliction definition: the prefab is of wrong type!"); + DebugConsole.ThrowError("Error in husk affliction definition: the prefab is of wrong type!", + contentPackage: prefab.ContentPackage); } } @@ -197,7 +198,7 @@ namespace Barotrauma huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount)); character.LastDamageSource = null; float force = applyForce ? random * 0.5f * limb.Mass : 0; - character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force); + character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, Rand.Vector(force)); } } @@ -205,7 +206,7 @@ namespace Barotrauma { if (huskAppendage == null && character.Params.UseHuskAppendage) { - huskAppendage = AttachHuskAppendage(character, Prefab.Identifier); + huskAppendage = AttachHuskAppendage(character, Prefab as AfflictionPrefabHusk); } if (Prefab is AfflictionPrefabHusk { NeedsAir: false }) @@ -285,13 +286,14 @@ namespace Barotrauma if (prefab == null) { - DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found."); + DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found.", + contentPackage: Prefab.ContentPackage); yield return CoroutineStatus.Success; } XElement parentElement = new XElement("CharacterInfo"); XElement infoElement = character.Info?.Save(parentElement); - CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(infoElement); + CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(new ContentXElement(Prefab.ContentPackage, infoElement)); if (huskCharacterInfo != null) { @@ -371,31 +373,28 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public static List AttachHuskAppendage(Character character, Identifier afflictionIdentifier, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null) + public static List AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null) { var appendage = new List(); - if (!(AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier) is AfflictionPrefabHusk matchingAffliction)) - { - DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!"); - return appendage; - } Identifier nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction); Identifier huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction); CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName); if (huskPrefab?.ConfigElement == null) { - DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!"); + DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!", + contentPackage: matchingAffliction.ContentPackage); return appendage; } var mainElement = huskPrefab.ConfigElement; var element = appendageDefinition; if (element == null) { - element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == afflictionIdentifier); + element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == matchingAffliction.Identifier); } if (element == null) { - DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{afflictionIdentifier}'!"); + DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{matchingAffliction.Identifier}'!", + contentPackage: matchingAffliction.ContentPackage); return appendage; } ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 5d46b8207..035a8c9ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -170,11 +170,13 @@ namespace Barotrauma if (DormantThreshold > ActiveThreshold) { - DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})"); + DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})", + contentPackage: element.ContentPackage); } if (ActiveThreshold > TransitionThreshold) { - DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})"); + DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})", + contentPackage: element.ContentPackage); } TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold); @@ -440,13 +442,15 @@ namespace Barotrauma 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", "")}\"."); + DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\".", + contentPackage: element.ContentPackage); continue; } AfflictionAbilityFlags |= flagType; break; case "affliction": - DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects."); + DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects.", + contentPackage: element.ContentPackage); break; } } @@ -537,14 +541,16 @@ namespace Barotrauma } else if (TextTag.IsEmpty) { - DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions."); + DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions.", + contentPackage: element.ContentPackage); } MinStrength = element.GetAttributeFloat(nameof(MinStrength), 0.0f); MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f); if (MinStrength >= MaxStrength) { - DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min."); + DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min.", + contentPackage: element.ContentPackage); } Target = element.GetAttributeEnum(nameof(Target), TargetType.Any); } @@ -953,7 +959,8 @@ namespace Barotrauma AfflictionOverlay = new Sprite(subElement); break; case "statvalue": - DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects."); + DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects.", + contentPackage: element.ContentPackage); break; case "effect": case "periodiceffect": @@ -962,7 +969,8 @@ namespace Barotrauma descriptions.Add(new Description(subElement, this)); break; default: - DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})"); + DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})", + contentPackage: element.ContentPackage); break; } } @@ -1018,6 +1026,10 @@ namespace Barotrauma } } + /// + /// Removes all the effects of the prefab (including the sounds and other assets defined in them). + /// Note that you need to call LoadAllEffectsAndTreatmentSuitabilities before trying to use the affliction again! + /// public static void ClearAllEffects() { Prefabs.ForEach(p => p.ClearEffects()); @@ -1046,7 +1058,8 @@ namespace Barotrauma var b = effects[j]; if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength) { - DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work."); + DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work.", + ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index d1092d476..7e813b74a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -49,7 +49,8 @@ namespace Barotrauma case "vitalitymultiplier": if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names."); + DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.", + contentPackage: element.ContentPackage); continue; } var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null); @@ -61,7 +62,8 @@ namespace Barotrauma VitalityMultipliers.Add(vitalityMultiplier, multiplier); if (AfflictionPrefab.Prefabs.None(p => p.Identifier == vitalityMultiplier)) { - DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions with the identifier \"{vitalityMultiplier}\". Did you mean to define the afflictions by type instead?"); + DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions with the identifier \"{vitalityMultiplier}\". Did you mean to define the afflictions by type instead?", + contentPackage: element.ContentPackage); } } } @@ -74,13 +76,15 @@ namespace Barotrauma VitalityTypeMultipliers.Add(vitalityTypeMultiplier, multiplier); if (AfflictionPrefab.Prefabs.None(p => p.AfflictionType == vitalityTypeMultiplier)) { - DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions of the type \"{vitalityTypeMultiplier}\". Did you mean to define the afflictions by identifier instead?"); + DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions of the type \"{vitalityTypeMultiplier}\". Did you mean to define the afflictions by identifier instead?", + contentPackage: element.ContentPackage); } } } if (vitalityMultipliers == null && VitalityTypeMultipliers == null) { - DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!"); + DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!", + contentPackage: element.ContentPackage); } break; } @@ -1309,6 +1313,8 @@ namespace Barotrauma public void Remove() { RemoveProjSpecific(); + afflictionsToRemove.Clear(); + afflictionsToUpdate.Clear(); } partial void RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index 03fb3ad7d..dcdd8ddb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -79,12 +79,13 @@ namespace Barotrauma public ref readonly ImmutableArray ParsedAfflictionTypes => ref parsedAfflictionTypes; - public DamageModifier(XElement element, string parentDebugName, bool checkErrors = true) + public DamageModifier(ContentXElement element, string parentDebugName, bool checkErrors = true) { Deserialize(element); - if (element.Attribute("afflictionnames") != null) + if (element.GetAttribute("afflictionnames") != null) { - DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); + DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.", + contentPackage: element.ContentPackage); } if (checkErrors) { @@ -108,12 +109,12 @@ namespace Barotrauma } } - static void createWarningOrError(string msg) + void createWarningOrError(string msg) { #if DEBUG - DebugConsole.ThrowError(msg); + DebugConsole.ThrowError(msg, contentPackage: element.ContentPackage); #else - DebugConsole.AddWarning(msg); + DebugConsole.AddWarning(msg, contentPackage: element.ContentPackage); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index d5c10cbef..630e18265 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -117,8 +117,8 @@ namespace Barotrauma public XElement Element { get; protected set; } - public readonly List<(XElement element, float commonness)> ItemSets = new List<(XElement element, float commonness)>(); - public readonly List<(XElement element, float commonness)> CustomCharacterInfos = new List<(XElement element, float commonness)>(); + public readonly List<(ContentXElement element, float commonness)> ItemSets = new List<(ContentXElement element, float commonness)>(); + public readonly List<(ContentXElement element, float commonness)> CustomCharacterInfos = new List<(ContentXElement element, float commonness)>(); public readonly Identifier NpcSetIdentifier; @@ -196,7 +196,7 @@ namespace Barotrauma var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element; if (spawnItems != null) { - foreach (XElement itemElement in spawnItems.GetChildElements("item")) + foreach (ContentXElement itemElement in spawnItems.GetChildElements("item")) { int amount = itemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) @@ -239,14 +239,15 @@ namespace Barotrauma return characterInfo; } - public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true) + public static void InitializeItem(Character character, ContentXElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true) { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; if (itemPrefab == null) { - DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); + DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.", + contentPackage: itemElement?.ContentPackage); return; } Item item = new Item(itemPrefab, character.Position, null); @@ -301,7 +302,7 @@ namespace Barotrauma wifiComponent.TeamID = character.TeamID; } parentItem?.Combine(item, user: null); - foreach (XElement childItemElement in itemElement.Elements()) + foreach (ContentXElement childItemElement in itemElement.Elements()) { int amount = childItemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 887743c78..411ff78b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -47,13 +47,14 @@ namespace Barotrauma } } - public Job(XElement element) + public Job(ContentXElement element) { Identifier identifier = element.GetAttributeIdentifier("identifier", ""); JobPrefab p; if (!JobPrefab.Prefabs.ContainsKey(identifier)) { - DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job."); + DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.", + contentPackage: element.ContentPackage); p = JobPrefab.Random(Rand.RandSync.Unsynced); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index e311800bc..e93e38566 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -43,7 +43,8 @@ namespace Barotrauma Priority = element.GetAttributeFloat("priority", -1f); if (Priority < 0) { - DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {element} of {file.Path}."); + DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {element} of {file.Path}.", + ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index f2859aaa2..d88356d60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -266,7 +266,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}"); + DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}", + contentPackage: characterPrefab.ContentPackage); } return a; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index e902ba353..6a5c7aab6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -63,7 +63,8 @@ namespace Barotrauma { if (!character.AnimController.CanWalk) { - DebugConsole.ThrowError($"{character.SpeciesName} cannot use run animations!"); + DebugConsole.ThrowError($"{character.SpeciesName} cannot use run animations!", + contentPackage: character.Prefab.ContentPackage); return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index c3042739a..7da09bc6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -136,6 +136,12 @@ namespace Barotrauma public readonly List DamageEmitters = new List(); public readonly List Inventories = new List(); public HealthParams Health { get; private set; } + /// + /// Parameters for EnemyAIController. Not used by HumanAIController. + /// + /// + /// AIParams or null. Use , if you don't expect nulls. + /// public AIParams AI { get; private set; } public CharacterParams(CharacterFile file) @@ -559,7 +565,8 @@ namespace Barotrauma 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}."); + $"Changing the crush depth from {CrushDepth} to {newCrushDepth}.", + element.ContentPackage); CrushDepth = newCrushDepth; } } @@ -602,7 +609,8 @@ namespace Barotrauma public void AddItem(string identifier = null) { - identifier = identifier ?? ""; + if (Element == null) { return; } + identifier ??= ""; var element = CreateElement("item", new XAttribute("identifier", identifier)); Element.Add(element); var item = new InventoryItem(element, Character); @@ -711,7 +719,8 @@ namespace Barotrauma if (HasTag(tag)) { target = null; - DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!"); + DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!", + targetElement.ContentPackage); return false; } else @@ -730,6 +739,11 @@ namespace Barotrauma public bool TryAddNewTarget(Identifier tag, AIState state, float priority, out TargetParams targetParams) { + if (Element == null) + { + targetParams = null; + return false; + } var element = TargetParams.CreateNewElement(Character, tag, state, priority); if (TryAddTarget(element, out targetParams)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs index 03f56b285..72ad781a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs @@ -84,12 +84,14 @@ namespace Barotrauma doc = XMLExtensions.TryLoadXml(Path); if (doc == null) { - DebugConsole.ThrowError("[EditableParams] The document is null! Failed to load the parameters."); + DebugConsole.ThrowError("[EditableParams] The document is null! Failed to load the parameters.", + contentPackage: file.ContentPackage); return false; } if (MainElement == null) { - DebugConsole.ThrowError("[EditableParams] The main element is null! Failed to load the parameters."); + DebugConsole.ThrowError("[EditableParams] The main element is null! Failed to load the parameters.", + contentPackage: file.ContentPackage); return false; } IsLoaded = Deserialize(MainElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 9187430ae..26e8ccfec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -106,7 +106,8 @@ namespace Barotrauma CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage)); if (prefab?.ConfigElement == null) { - DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})"); + DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", + contentPackage: contentPackage); return string.Empty; } return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value); @@ -183,7 +184,8 @@ namespace Barotrauma } if (error != null) { - DebugConsole.ThrowError(error); + DebugConsole.ThrowError(error, + contentPackage: prefab?.ContentPackage); } } if (selectedFile == null) @@ -444,7 +446,8 @@ namespace Barotrauma { if (source.MainElement == null) { - DebugConsole.ThrowError("[RagdollParams] The source XML Element of the given RagdollParams is null!"); + DebugConsole.ThrowError("[RagdollParams] The source XML Element of the given RagdollParams is null!", + contentPackage: source.MainElement?.ContentPackage); return; } Deserialize(source.MainElement, alsoChildren: false); @@ -453,7 +456,8 @@ namespace Barotrauma // TODO: cannot currently undo joint/limb deletion. if (sourceSubParams.Count != subParams.Count) { - DebugConsole.ThrowError("[RagdollParams] The count of the sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes."); + DebugConsole.ThrowError("[RagdollParams] The count of the sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.", + contentPackage: source.MainElement?.ContentPackage); return; } for (int i = 0; i < subParams.Count; i++) @@ -461,7 +465,8 @@ namespace Barotrauma var subSubParams = subParams[i].SubParams; if (subSubParams.Count != sourceSubParams[i].SubParams.Count) { - DebugConsole.ThrowError("[RagdollParams] The count of the sub sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes."); + DebugConsole.ThrowError("[RagdollParams] The count of the sub sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.", + contentPackage: source.MainElement?.ContentPackage); return; } subParams[i].Deserialize(sourceSubParams[i].Element, recursive: false); @@ -890,14 +895,14 @@ namespace Barotrauma #if CLIENT public DecorativeSprite DecorativeSprite { get; private set; } - public override bool Deserialize(XElement element = null, bool recursive = true) + public override bool Deserialize(ContentXElement element = null, bool recursive = true) { base.Deserialize(element, recursive); DecorativeSprite.SerializableProperties = SerializableProperty.DeserializeProperties(DecorativeSprite, element ?? Element); return SerializableProperties != null; } - public override bool Serialize(XElement element = null, bool recursive = true) + public override bool Serialize(ContentXElement element = null, bool recursive = true) { base.Serialize(element, recursive); SerializableProperty.SerializeProperties(DecorativeSprite, element ?? Element); @@ -985,7 +990,8 @@ namespace Barotrauma deformation = new PositionalDeformationParams(deformationElement); break; default: - DebugConsole.ThrowError($"SpriteDeformationParams not implemented: '{typeName}'"); + DebugConsole.ThrowError($"SpriteDeformationParams not implemented: '{typeName}'", + contentPackage: element.ContentPackage); break; } if (deformation != null) @@ -1000,14 +1006,14 @@ namespace Barotrauma #if CLIENT public Dictionary Deformations { get; private set; } - public override bool Deserialize(XElement element = null, bool recursive = true) + public override bool Deserialize(ContentXElement element = null, bool recursive = true) { base.Deserialize(element, recursive); Deformations.ForEach(d => d.Key.SerializableProperties = SerializableProperty.DeserializeProperties(d.Key, d.Value)); return SerializableProperties != null; } - public override bool Serialize(XElement element = null, bool recursive = true) + public override bool Serialize(ContentXElement element = null, bool recursive = true) { base.Serialize(element, recursive); Deformations.ForEach(d => SerializableProperty.SerializeProperties(d.Key, d.Value)); @@ -1098,14 +1104,14 @@ namespace Barotrauma } #if CLIENT - public override bool Deserialize(XElement element = null, bool recursive = true) + public override bool Deserialize(ContentXElement element = null, bool recursive = true) { base.Deserialize(element, recursive); LightSource.Deserialize(element ?? Element); return SerializableProperties != null; } - public override bool Serialize(XElement element = null, bool recursive = true) + public override bool Serialize(ContentXElement element = null, bool recursive = true) { base.Serialize(element, recursive); LightSource.Serialize(element ?? Element); @@ -1130,14 +1136,14 @@ namespace Barotrauma Attack = new Attack(element, ragdoll.SpeciesName.Value); } - public override bool Deserialize(XElement element = null, bool recursive = true) + public override bool Deserialize(ContentXElement element = null, bool recursive = true) { base.Deserialize(element, recursive); Attack.Deserialize(element ?? Element, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null"); return SerializableProperties != null; } - public override bool Serialize(XElement element = null, bool recursive = true) + public override bool Serialize(ContentXElement element = null, bool recursive = true) { base.Serialize(element, recursive); Attack.Serialize(element ?? Element); @@ -1182,14 +1188,14 @@ namespace Barotrauma DamageModifier = new DamageModifier(element, ragdoll.SpeciesName.Value); } - public override bool Deserialize(XElement element = null, bool recursive = true) + public override bool Deserialize(ContentXElement element = null, bool recursive = true) { base.Deserialize(element, recursive); DamageModifier.Deserialize(element ?? Element); return SerializableProperties != null; } - public override bool Serialize(XElement element = null, bool recursive = true) + public override bool Serialize(ContentXElement element = null, bool recursive = true) { base.Serialize(element, recursive); DamageModifier.Serialize(element ?? Element); @@ -1218,7 +1224,7 @@ namespace Barotrauma public virtual string Name { get; set; } public Dictionary SerializableProperties { get; private set; } public ContentXElement Element { get; set; } - public XElement OriginalElement { get; protected set; } + public ContentXElement OriginalElement { get; protected set; } public List SubParams { get; set; } = new List(); public RagdollParams Ragdoll { get; private set; } @@ -1230,14 +1236,14 @@ namespace Barotrauma public SubParam(ContentXElement element, RagdollParams ragdoll) { Element = element; - OriginalElement = new XElement(element); + OriginalElement = new ContentXElement(element.ContentPackage, element); Ragdoll = ragdoll; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } - public virtual bool Deserialize(XElement element = null, bool recursive = true) + public virtual bool Deserialize(ContentXElement element = null, bool recursive = true) { - element = element ?? Element; + element ??= Element; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); if (recursive) { @@ -1246,9 +1252,9 @@ namespace Barotrauma return SerializableProperties != null; } - public virtual bool Serialize(XElement element = null, bool recursive = true) + public virtual bool Serialize(ContentXElement element = null, bool recursive = true) { - element = element ?? Element; + element ??= Element; SerializableProperty.SerializeProperties(this, element, true); if (recursive) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs index 579ef14a8..a28080a0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs @@ -100,6 +100,13 @@ namespace Barotrauma set; } + [Serialize(1.5f, IsPropertySaveable.Yes)] + public float SkillIncreaseExponent + { + get; + set; + } + public SkillSettings(XElement element, SkillSettingsFile file) : base(file, "SkillSettings".ToIdentifier()) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs index ad4b4df25..5b74ede1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Abilities public AbilityCondition(CharacterTalent characterTalent, ContentXElement conditionElement) { - this.characterTalent = characterTalent; + this.characterTalent = characterTalent ?? throw new ArgumentNullException(nameof(characterTalent)); character = characterTalent.Character; invert = conditionElement.GetAttributeBool("invert", false); } @@ -40,7 +40,8 @@ namespace Barotrauma.Abilities { if (!Enum.TryParse(targetTypeString, true, out TargetType targetType)) { - DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")", + contentPackage: characterTalent.Prefab.ContentPackage); } targetTypes.Add(targetType); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs index ebd077561..961cfcf71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs @@ -1,7 +1,4 @@ -using Barotrauma.Items.Components; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using System.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index 77cc53bad..905adea68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -34,7 +34,8 @@ namespace Barotrauma.Abilities string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any"); if (!Enum.TryParse(weaponTypeStr, ignoreCase: true, out weapontype)) { - DebugConsole.ThrowError($"Error in talent \"{characterTalent.DebugIdentifier}\": \"{weaponTypeStr}\" is not a valid weapon type."); + DebugConsole.ThrowError($"Error in talent \"{characterTalent.DebugIdentifier}\": \"{weaponTypeStr}\" is not a valid weapon type.", + contentPackage: conditionElement.ContentPackage); } } 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 26af153b2..e887d7b99 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -11,13 +10,19 @@ namespace Barotrauma.Abilities private readonly List conditionals = new List(); + /// + /// If enabled, the conditional is checked on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter). + /// Defaults to true, except in the case of , which by default targets the character who has the talent. + /// + private readonly bool targetAbilityTarget = false; + public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { targetTypes = ParseTargetTypes( conditionElement.GetAttributeStringArray("targettypes", conditionElement.GetAttributeStringArray("targettype", Array.Empty()))); - foreach (XElement subElement in conditionElement.Elements()) + foreach (ContentXElement subElement in conditionElement.Elements()) { if (subElement.NameAsIdentifier() == "conditional") { @@ -25,29 +30,47 @@ namespace Barotrauma.Abilities } } - if (!targetTypes.Any() && !conditionals.Any()) + //don't log this error if this is a subclass of AbilityConditionCharacter + //(in that case not having any conditionals here is ok) + if (!targetTypes.Any() && !conditionals.Any() && GetType() == typeof(AbilityConditionCharacter)) { - DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character."); + DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.", + contentPackage: conditionElement.ContentPackage); } + + targetAbilityTarget = conditionElement.GetAttributeBool(nameof(targetAbilityTarget), this is not AbilityConditionHasPermanentStat); } - protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + public sealed override bool MatchesCondition() { - if (abilityObject is IAbilityCharacter abilityCharacter) + //by default data-reliant conditions don't accept null, but in this case it's ok, + //because we can assume it's the character who has the talent + return MatchesCondition(abilityObject: null); + } + + public sealed override bool MatchesCondition(AbilityObject abilityObject) + { + return invert ? !MatchesConditionSpecific(abilityObject) : MatchesConditionSpecific(abilityObject); + } + + protected sealed override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + Character targetCharacter = + targetAbilityTarget ? + (abilityObject as IAbilityCharacter)?.Character ?? character : + character; + if (targetCharacter is null) { return false; } + if (!IsViableTarget(targetTypes, targetCharacter)) { return false; } + foreach (var conditional in conditionals) { - if (abilityCharacter.Character is not Character character) { return false; } - if (!IsViableTarget(targetTypes, character)) { return false; } - foreach (var conditional in conditionals) - { - if (!conditional.Matches(character)) { return false; } - } - return true; - } - else - { - LogAbilityConditionError(abilityObject, typeof(IAbilityCharacter)); - return false; + if (!conditional.Matches(targetCharacter)) { return false; } } + return MatchesCharacter(targetCharacter); + } + + protected virtual bool MatchesCharacter(Character character) + { + return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs index a79830e5b..3dfbe7808 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs @@ -1,6 +1,6 @@ namespace Barotrauma.Abilities { - internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData + internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionCharacter { private readonly Identifier identifier; @@ -9,11 +9,9 @@ namespace Barotrauma.Abilities identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty); } - protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + protected override bool MatchesCharacter(Character character) { - if (abilityObject is not IAbilityCharacter ability) { return false; } - - return !ability.Character.MarkedAsLooted.Contains(identifier); + return character != null &&!character.MarkedAsLooted.Contains(identifier); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs index 2809f3546..5646497c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs @@ -2,15 +2,13 @@ namespace Barotrauma.Abilities { - internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData + internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionCharacter { public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } - protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + protected override bool MatchesCharacter(Character character) { - if (abilityObject is not IAbilityCharacter targetCharacter) { return false; } - - return targetCharacter.Character.IsUnconscious; + return character is { IsUnconscious: true }; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs index 426156bec..c8d3495de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs @@ -17,13 +17,15 @@ namespace Barotrauma.Abilities protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData) { - DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}"); + DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}", + contentPackage: characterTalent.Prefab.ContentPackage); } protected abstract bool MatchesConditionSpecific(AbilityObject abilityObject); public override bool MatchesCondition() { - DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed."); + DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed.", + contentPackage: characterTalent.Prefab.ContentPackage); return false; } public override bool MatchesCondition(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs index e4580fadd..309c60d80 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -19,7 +19,8 @@ namespace Barotrauma.Abilities if (identifiers.None() && tags.None() && category == MapEntityCategory.None) { - DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers, tags or category defined."); + DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers, tags or category defined.", + contentPackage: conditionElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemIsStatic.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemIsStatic.cs new file mode 100644 index 000000000..f14f65fbb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemIsStatic.cs @@ -0,0 +1,19 @@ +using Barotrauma.Items.Components; + +namespace Barotrauma.Abilities +{ + class AbilityConditionItemIsStatic : AbilityConditionData + { + public AbilityConditionItemIsStatic(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if (abilityObject is IAbilityItem { Item: var item }) + { + return item.GetComponent() is null && item.GetComponent() is null; + } + + return false; + } + } +} 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 f92523e10..71ae4f7dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -22,7 +22,8 @@ namespace Barotrauma.Abilities { if (!isAffiliated) { - DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type."); + DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.", + contentPackage: conditionElement.ContentPackage); } continue; } 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 237e15b5f..e9e68472f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -1,6 +1,8 @@ -namespace Barotrauma.Abilities +using System; + +namespace Barotrauma.Abilities { - class AbilityConditionHasPermanentStat : AbilityConditionDataless + class AbilityConditionHasPermanentStat : AbilityConditionCharacter { private readonly Identifier statIdentifier; private readonly StatTypes statType; @@ -12,7 +14,8 @@ statIdentifier = conditionElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); if (statIdentifier.IsEmpty) { - DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!"); + DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!", + contentPackage: conditionElement.ContentPackage); } string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty); statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier); @@ -20,8 +23,13 @@ placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None); } - protected override bool MatchesConditionSpecific() + protected override bool MatchesCharacter(Character character) { + if (character?.Info == null) + { + DebugConsole.AddWarning($"Error in {nameof(AbilityConditionHasPermanentStat.MatchesCharacter)}: character {character} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}"); + return false; + } Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier); return character.Info.GetSavedStatValue(statType, identifier) >= min; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs index 2ee0a66ee..c9a48c4eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs @@ -13,7 +13,8 @@ namespace Barotrauma.Abilities tag = conditionElement.GetAttributeIdentifier("tag", Identifier.Empty); if (tag.IsEmpty) { - DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag."); + DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag.", + characterTalent.Prefab.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs index 3cf37c2b9..fb6749419 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs @@ -2,21 +2,18 @@ namespace Barotrauma.Abilities { - internal sealed class AbilityConditionLowestLevel : AbilityConditionDataless + internal sealed class AbilityConditionLowestLevel : AbilityConditionCharacter { public AbilityConditionLowestLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } - protected override bool MatchesConditionSpecific() + protected override bool MatchesCharacter(Character character) { int ownLevel = character.Info.GetCurrentLevel(); - - foreach (Character crew in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + foreach (Character otherCharacter in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - if (crew == character) { continue; } - - if (crew.Info.GetCurrentLevel() < ownLevel) { return false; } + if (otherCharacter == character) { continue; } + if (otherCharacter.Info.GetCurrentLevel() < ownLevel) { return false; } } - return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 5a7d22598..7ccc4e036 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -26,7 +26,7 @@ namespace Barotrauma.Abilities public CharacterAbility(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) { - CharacterAbilityGroup = characterAbilityGroup; + CharacterAbilityGroup = characterAbilityGroup ?? throw new ArgumentNullException(nameof(characterAbilityGroup)); CharacterTalent = characterAbilityGroup.CharacterTalent; Character = CharacterTalent.Character; RequiresAlive = abilityElement.GetAttributeBool("requiresalive", true); @@ -59,7 +59,8 @@ namespace Barotrauma.Abilities protected virtual void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups.", + contentPackage: CharacterTalent.Prefab.ContentPackage); } public void ApplyAbilityEffect(AbilityObject abilityObject) @@ -76,17 +77,20 @@ namespace Barotrauma.Abilities protected virtual void ApplyEffect() { - DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}"); + DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}", + CharacterTalent.Prefab.ContentPackage); } protected virtual void ApplyEffect(AbilityObject abilityObject) { - DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}"); + DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}", + CharacterTalent.Prefab.ContentPackage); } protected void LogAbilityObjectMismatch() { - DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}"); + DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}", + contentPackage: CharacterTalent.Prefab.ContentPackage); } // XML @@ -99,13 +103,18 @@ namespace Barotrauma.Abilities abilityType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); if (abilityType == null) { - if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")"); + if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", + contentPackage: abilityElement.ContentPackage); return null; } } catch (Exception e) { - if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", e); + if (errorMessages) + { + DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", e, + contentPackage: abilityElement.ContentPackage); + } return null; } @@ -118,7 +127,8 @@ namespace Barotrauma.Abilities } catch (TargetInvocationException e) { - DebugConsole.ThrowError("Error while creating an instance of a CharacterAbility of the type " + abilityType + ".", e.InnerException); + DebugConsole.ThrowError("Error while creating an instance of a CharacterAbility of the type " + abilityType + ".", e.InnerException, + contentPackage: abilityElement.ContentPackage); return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs index 0b350c514..0ca9c74d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs @@ -30,7 +30,8 @@ namespace Barotrauma.Abilities } else { - DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - \"{limbTypeStr}\" is not a valid limb type."); + DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - \"{limbTypeStr}\" is not a valid limb type.", + contentPackage: abilityElement.ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs index 27d4afe94..453ad0d09 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs @@ -21,7 +21,8 @@ namespace Barotrauma.Abilities JobPrefab? apprenticeJob = GetApprenticeJob(Character, jobPrefabList); if (apprenticeJob is null) { - DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}"); + DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs index 0cd2b4857..2b98992fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs @@ -14,7 +14,8 @@ targetAllies = abilityElement.GetAttributeBool("targetallies", false); if (skillIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs index 13bae5e96..3b9653393 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs @@ -16,7 +16,8 @@ if (afflictionId.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set.", + contentPackage: abilityElement.ContentPackage); } } @@ -27,7 +28,8 @@ var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == afflictionId); if (afflictionPrefab == null) { - DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\"."); + DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } float strength = this.strength; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs index 5686f777a..3aa2719ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs @@ -14,29 +14,35 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility if (amount == 0 && level == 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}.", + contentPackage: abilityElement.ContentPackage); } if (amount > 0 && level > 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level.", + contentPackage: abilityElement.ContentPackage); } } private void ApplyEffectSpecific(Character targetCharacter) { - if (amount != 0) - { - targetCharacter.Info?.GiveExperience(amount); - } if (level > 0) { - targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level)); + targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level) + amount); + } + else if (amount != 0) + { + targetCharacter.Info?.GiveExperience(amount); } } protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter) + if (abilityObject is AbilityCharacterKill { Killer: { } killer }) + { + ApplyEffectSpecific(killer); + } + else if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter) { ApplyEffectSpecific(targetCharacter); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs index c78b2c66b..7e3d5ef4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs @@ -7,12 +7,14 @@ namespace Barotrauma.Abilities private readonly ItemTalentStats stat; private readonly float value; private readonly bool stackable; + private readonly bool save; public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); value = abilityElement.GetAttributeFloat("value", 0f); stackable = abilityElement.GetAttributeBool("stackable", true); + save = abilityElement.GetAttributeBool("save", false); } protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) @@ -27,7 +29,7 @@ namespace Barotrauma.Abilities { if (abilityObject is not IAbilityItem ability) { return; } - ability.Item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent); + ability.Item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs index 147725f90..ac3d4f94c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -10,6 +10,7 @@ namespace Barotrauma.Abilities private readonly float value; private readonly ImmutableHashSet tags; private readonly bool stackable; + private readonly bool save; public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { @@ -17,6 +18,7 @@ namespace Barotrauma.Abilities value = abilityElement.GetAttributeFloat("value", 0f); tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); stackable = abilityElement.GetAttributeBool("stackable", true); + save = abilityElement.GetAttributeBool("save", false); } public override void InitializeAbility(bool addingFirstTime) @@ -44,7 +46,7 @@ namespace Barotrauma.Abilities if (item.Submarine?.TeamID != Character.TeamID) { continue; } if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier)) { - item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent); + item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs index a45e92b1d..2b5f00421 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -14,7 +14,8 @@ if (amount == 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index a2a94cf37..31c58117b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -1,4 +1,6 @@ -namespace Barotrauma.Abilities +using System; + +namespace Barotrauma.Abilities { public enum PermanentStatPlaceholder { @@ -19,7 +21,12 @@ private readonly bool setValue; private readonly PermanentStatPlaceholder placeholder; - //private readonly float maximumValue; + /// + /// If enabled, the effect is applied on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter). + /// Defaults to false (= targets the character who has the talent). + /// + private readonly bool targetAbilityTarget = false; + public override bool AllowClientSimulation => true; public override bool AppliesEffectOnIntervalUpdate => true; @@ -28,7 +35,8 @@ statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); if (statIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined."); + DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined.", + contentPackage: abilityElement.ContentPackage); } string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty); statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier); @@ -39,27 +47,28 @@ giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None); setValue = abilityElement.GetAttributeBool("setvalue", false); placeholder = abilityElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None); + targetAbilityTarget = abilityElement.GetAttributeBool(nameof(targetAbilityTarget), false); } public override void InitializeAbility(bool addingFirstTime) { if (giveOnAddingFirstTime && addingFirstTime) { - ApplyEffectSpecific(); + ApplyEffectSpecific(abilityObject: null); } } protected override void ApplyEffect(AbilityObject abilityObject) { - ApplyEffectSpecific(); + ApplyEffectSpecific(abilityObject); } protected override void ApplyEffect() { - ApplyEffectSpecific(); + ApplyEffectSpecific(abilityObject: null); } - private void ApplyEffectSpecific() + private void ApplyEffectSpecific(AbilityObject abilityObject) { Identifier identifier = HandlePlaceholders(placeholder, statIdentifier); if (targetAllies) @@ -71,7 +80,21 @@ } else { - Character?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + Character targetCharacter = + targetAbilityTarget ? + (abilityObject as IAbilityCharacter)?.Character ?? Character : + Character; + if (targetCharacter == null) + { + DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character was null.\n{Environment.StackTrace.CleanupStackTrace()}"); + return; + } + if (targetCharacter?.Info == null) + { + DebugConsole.AddWarning($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character {targetCharacter} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}"); + return; + } + targetCharacter.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); } } @@ -82,7 +105,7 @@ switch (placeholder) { case PermanentStatPlaceholder.LocationName when map.CurrentLocation is { } location: - return original.Replace("[placeholder]", location.Name); + return original.Replace("[placeholder]", location.NameIdentifier.Value); case PermanentStatPlaceholder.LocationIndex: return original.Replace("[placeholder]", map.CurrentLocationIndex.ToString()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs index 6d3777d53..a0bfb0f34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs @@ -13,11 +13,13 @@ namespace Barotrauma.Abilities amount = abilityElement.GetAttributeFloat("amount", 0f); if (factionIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined.", + contentPackage: abilityElement.ContentPackage); } if (amount == 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs index 9df7fc87b..062032ca7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs @@ -12,11 +12,13 @@ if (resistanceId.IsEmpty) { - DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set."); + DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.", + contentPackage: abilityElement.ContentPackage); } if (MathUtils.NearlyEqual(multiplier, 1)) { - DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing."); + DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs index e61e3981b..ea40eb130 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs @@ -9,7 +9,8 @@ amount = abilityElement.GetAttributeInt("amount", 0); if (amount == 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs index 2b4dd4cac..f3f2d91cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs @@ -11,7 +11,8 @@ namespace Barotrauma.Abilities amount = abilityElement.GetAttributeInt("amount", 0); if (amount == 0) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs index 2e1816bd4..4618bcbeb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs @@ -16,11 +16,13 @@ namespace Barotrauma.Abilities if (skillIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill."); + DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill.", + contentPackage: abilityElement.ContentPackage); } if (MathUtils.NearlyEqual(skillIncrease, 0)) { - DebugConsole.AddWarning($"Possible error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill increase set to 0."); + DebugConsole.AddWarning($"Possible error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill increase set to 0.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs index 45ddb19fb..703b07c48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs @@ -8,7 +8,8 @@ namespace Barotrauma.Abilities identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); if (identifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs index 3c1ec2272..4cee5bb66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs @@ -15,11 +15,13 @@ if (resistanceId.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}.", + contentPackage: abilityElement.ContentPackage); } if (MathUtils.NearlyEqual(multiplier, 1.0f)) { - DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing."); + DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs index 57ed31b3b..4168ec6a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs @@ -11,7 +11,8 @@ multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); if (MathUtils.NearlyEqual(addedValue, 0.0f) && MathUtils.NearlyEqual(multiplyValue, 1.0f)) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs index ba7ef06eb..346c075b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs @@ -11,7 +11,8 @@ amount = abilityElement.GetAttributeInt("amount", 1); if (itemIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined."); + DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined.", + contentPackage: abilityElement.ContentPackage); } } @@ -19,14 +20,16 @@ { if (itemIdentifier.IsEmpty) { - DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined."); + DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined.", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } ItemPrefab itemPrefab = ItemPrefab.Find(null, itemIdentifier); if (itemPrefab == null) { - DebugConsole.ThrowError("Cannot put item in inventory - item prefab " + itemIdentifier + " not found."); + DebugConsole.ThrowError("Cannot put item in inventory - item prefab " + itemIdentifier + " not found.", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } for (int i = 0; i < amount; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs index 14a84d337..4f64d8bfd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs @@ -14,7 +14,8 @@ namespace Barotrauma.Abilities if (afflictionId.IsEmpty) { - DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set."); + DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs index 3e85a16dd..e1dac6415 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs @@ -12,7 +12,8 @@ namespace Barotrauma.Abilities statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); if (statIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty.", + contentPackage: abilityElement.ContentPackage); } } protected override void ApplyEffect(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs index 3953cce9f..daa34f959 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs @@ -13,7 +13,8 @@ namespace Barotrauma.Abilities value = abilityElement.GetAttributeInt("value", 0); if (identifier.IsEmpty) { - DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty.", + contentPackage: abilityElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs index 7a4ceeb07..7ce696992 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -24,7 +24,8 @@ namespace Barotrauma.Abilities JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet()); if (apprentice is null) { - DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}"); + DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 6b59a5825..4d177dd69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Abilities public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) { AbilityEffectType = abilityEffectType; - CharacterTalent = characterTalent; + CharacterTalent = characterTalent ?? throw new ArgumentNullException(nameof(characterTalent)); Character = CharacterTalent.Character; maxTriggerCount = abilityElementGroup.GetAttributeInt("maxtriggercount", int.MaxValue); foreach (var subElement in abilityElementGroup.Elements()) @@ -44,9 +44,13 @@ namespace Barotrauma.Abilities case "fallbackabilities": LoadFallbackAbilities(subElement); break; + case "condition": case "conditions": LoadConditions(subElement); break; + default: + DebugConsole.ThrowError($"Error in talent {characterTalent.Prefab.Identifier}: unrecognized xml element \"{subElement.Name}\"."); + break; } } @@ -55,7 +59,8 @@ namespace Barotrauma.Abilities case AbilityEffectType.OnDieToCharacter: if (characterAbilities.Any(a => a.RequiresAlive)) { - DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute."); + DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute.", + characterTalent.Prefab.ContentPackage); } break; } @@ -90,7 +95,8 @@ namespace Barotrauma.Abilities if (newCondition == null) { - DebugConsole.ThrowError($"AbilityCondition was not found in talent {CharacterTalent.DebugIdentifier}!"); + DebugConsole.ThrowError($"AbilityCondition was not found in talent {CharacterTalent.DebugIdentifier}!", + contentPackage: conditionElement.ContentPackage); return; } @@ -107,7 +113,8 @@ namespace Barotrauma.Abilities { if (characterAbility == null) { - DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!"); + DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } @@ -118,7 +125,8 @@ namespace Barotrauma.Abilities { if (characterAbility == null) { - DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!"); + DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!", + contentPackage: CharacterTalent.Prefab.ContentPackage); return; } @@ -135,13 +143,21 @@ namespace Barotrauma.Abilities conditionType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); if (conditionType == null) { - if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")"); + if (errorMessages) + { + DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", + contentPackage: characterTalent.Prefab.ContentPackage); + } return null; } } catch (Exception e) { - if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", e); + if (errorMessages) + { + DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", e, + contentPackage: characterTalent.Prefab.ContentPackage); + } return null; } @@ -154,13 +170,15 @@ namespace Barotrauma.Abilities } catch (TargetInvocationException e) { - DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ".", e.InnerException); + DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ".", e.InnerException, + contentPackage: characterTalent.Prefab.ContentPackage); return null; } if (newCondition == null) { - DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ", instance was null"); + DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ", instance was null", + contentPackage: characterTalent.Prefab.ContentPackage); return null; } @@ -189,7 +207,8 @@ namespace Barotrauma.Abilities if (newAbility == null) { - DebugConsole.ThrowError($"Unable to create an ability for {characterTalent.DebugIdentifier}!"); + DebugConsole.ThrowError($"Unable to create an ability for {characterTalent.DebugIdentifier}!", + contentPackage: characterTalent.Prefab.ContentPackage); return null; } @@ -200,7 +219,8 @@ namespace Barotrauma.Abilities { if (statusEffectElements == null) { - DebugConsole.ThrowError("StatusEffect list was not found in talent " + characterTalent.DebugIdentifier); + DebugConsole.ThrowError("StatusEffect list was not found in talent " + characterTalent.DebugIdentifier, + contentPackage: characterTalent.Prefab.ContentPackage); return null; } @@ -233,7 +253,8 @@ namespace Barotrauma.Abilities { if (afflictionElements == null) { - DebugConsole.ThrowError("Affliction list was not found in talent " + characterTalent.DebugIdentifier); + DebugConsole.ThrowError("Affliction list was not found in talent " + characterTalent.DebugIdentifier, + contentPackage: characterTalent.Prefab.ContentPackage); return null; } @@ -248,7 +269,8 @@ namespace Barotrauma.Abilities AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier); if (afflictionPrefab == null) { - DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found."); + DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found.", + contentPackage: characterTalent.Prefab.ContentPackage); continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index 0cb53857e..ae7f18849 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -23,9 +23,8 @@ namespace Barotrauma public CharacterTalent(TalentPrefab talentPrefab, Character character) { - Character = character; - - Prefab = talentPrefab; + Character = character ?? throw new ArgumentNullException(nameof(character)); + Prefab = talentPrefab ?? throw new ArgumentNullException(nameof(talentPrefab)); var element = talentPrefab.ConfigElement; DebugIdentifier = talentPrefab.OriginalName; @@ -46,7 +45,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}"); + DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}", + contentPackage: element.ContentPackage); } break; case "addedstoreitem": @@ -56,7 +56,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}"); + DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}", + contentPackage: element.ContentPackage); } break; } @@ -146,11 +147,13 @@ namespace Barotrauma { if (!Enum.TryParse(abilityEffectTypeString, true, out AbilityEffectType abilityEffectType)) { - DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")", + contentPackage: characterTalent?.Prefab?.ContentPackage); } if (abilityEffectType == AbilityEffectType.Undefined) { - DebugConsole.ThrowError("Ability effect type not defined in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + DebugConsole.ThrowError("Ability effect type not defined in CharacterTalent (" + characterTalent.DebugIdentifier + ")", + contentPackage: characterTalent?.Prefab?.ContentPackage); } return abilityEffectType; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index bd105e729..f640dee46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -84,7 +84,8 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e); + DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e, + element?.ContentPackage); } } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 2c1867589..04ac83453 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -37,7 +37,8 @@ namespace Barotrauma if (Identifier.IsEmpty) { - DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!"); + DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!", + contentPackage: element.ContentPackage); return; } @@ -304,7 +305,8 @@ namespace Barotrauma if (RequiredTalents > MaxChosenTalents) { - DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents."); + DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.", + contentPackage: talentOptionsElement.ContentPackage); } HashSet identifiers = new HashSet(); @@ -333,11 +335,13 @@ namespace Barotrauma if (RequiredTalents > talentIdentifiers.Count) { - DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage."); + DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage.", + contentPackage: talentOptionsElement.ContentPackage); } if (MaxChosenTalents > talentIdentifiers.Count) { - DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents."); + DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents.", + contentPackage: talentOptionsElement.ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs index c2ccdb8bb..e9137e756 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/ItemSlotIndexPair.cs @@ -39,6 +39,23 @@ namespace Barotrauma } public Item? FindItemInContainer(ItemContainer? container) - => container?.Inventory.GetItemsAt(Slot).ElementAt(StackIndex); + { + var items = container?.Inventory.GetItemsAt(Slot); + if (items != null && StackIndex >= 0 && StackIndex < items.Count()) + { + return items.ElementAt(StackIndex); + } + else + { + string errorMsg = + $"Circuit box error: failed to find an item in the container {container?.Item.Name ?? "null"}."; + DebugConsole.ThrowError( + errorMsg + + $" Items: {items?.Count().ToString() ?? "null"}, " + + $" Slot: {Slot}, StackIndex: {StackIndex}"); + GameAnalyticsManager.AddErrorEventOnce("ItemSlotIndexPair.FindItemInContainer", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + return null; + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs index e3cfc518c..2069fe68f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs @@ -50,7 +50,8 @@ namespace Barotrauma if (identifier.IsEmpty) { DebugConsole.ThrowError( - $"No identifier defined for the affliction '{elementName}' in file '{Path}'"); + $"No identifier defined for the affliction '{elementName}' in file '{Path}'", + contentPackage: element?.ContentPackage); return; } @@ -60,12 +61,13 @@ namespace Barotrauma { DebugConsole.NewMessage( $"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{Path}'", - Color.Yellow); + Color.MediumPurple); } else { DebugConsole.ThrowError( - $"Duplicate affliction: '{identifier}' defined in {elementName} of '{Path}'"); + $"Duplicate affliction: '{identifier}' defined in {elementName} of '{Path}'", + contentPackage: element?.ContentPackage); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs index e3412c1be..d8269dbd0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -17,12 +17,12 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(Path); if (doc == null) { - DebugConsole.ThrowError($"Loading character file failed: {Path}"); + DebugConsole.ThrowError($"Loading character file failed: {Path}", contentPackage: ContentPackage); return; } if (CharacterPrefab.Prefabs.AllPrefabs.Any(kvp => kvp.Value.Any(cf => cf?.ContentFile == this))) { - DebugConsole.ThrowError($"Duplicate path: {Path}"); + DebugConsole.ThrowError($"Duplicate path: {Path}", contentPackage: ContentPackage); return; } var mainElement = doc.Root.FromPackage(ContentPackage); @@ -69,7 +69,8 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e); + DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e, + contentPackage: characterPrefab.ContentPackage); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index f2470ed6a..b7d896fe6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -76,7 +76,8 @@ namespace Barotrauma { DebugConsole.AddWarning( $"The content type \"TraitorMission\" in content package \"{package.Name}\" is no longer supported." + - $" Traitor missions should be implemented using the scripted event system and the content type TraitorEvents."); + $" Traitor missions should be implemented using the scripted event system and the content type TraitorEvents.", + package); } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs index b57133514..ef041dbef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs @@ -55,7 +55,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"GenericPrefabFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"GenericPrefabFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}", contentPackage: ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs index f26a17cd7..95ee2d5c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs @@ -2,6 +2,7 @@ using System.Xml.Linq; namespace Barotrauma { + [NotSyncedInMultiplayer] sealed class NPCConversationsFile : ContentFile { public NPCConversationsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs index 57273ced6..cb7ca8d87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs @@ -42,7 +42,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"OrdersFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"OrdersFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}", + contentPackage: parentElement?.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs index 4c25ad989..51518e428 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs @@ -65,7 +65,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"RandomEventsFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + DebugConsole.ThrowError($"RandomEventsFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}", + contentPackage: parentElement.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs index 9f21480f9..ff4493c9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs @@ -4,6 +4,7 @@ using System.Xml.Linq; namespace Barotrauma { + [NotSyncedInMultiplayer] public sealed class TextFile : ContentFile { public TextFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } @@ -18,13 +19,12 @@ namespace Barotrauma LanguageIdentifier language = languageName.ToLanguageIdentifier(); if (!TextManager.TextPacks.ContainsKey(language)) { - TextManager.TextPacks.TryAdd(language, ImmutableHashSet.Empty); + TextManager.TextPacks.TryAdd(language, ImmutableList.Empty); } - var newPack = new TextPack(this, mainElement, language); - var newHashSet = TextManager.TextPacks[language].Add(newPack); + var newList = TextManager.TextPacks[language].Add(newPack); TextManager.TextPacks.TryRemove(language, out _); - TextManager.TextPacks.TryAdd(language, newHashSet); + TextManager.TextPacks.TryAdd(language, newList); TextManager.IncrementLanguageVersion(); } @@ -32,16 +32,27 @@ namespace Barotrauma { foreach (var kvp in TextManager.TextPacks.ToArray()) { - var newHashSet = kvp.Value.Where(p => p.ContentFile != this).ToImmutableHashSet(); + var newList = kvp.Value.Where(p => p.ContentFile != this).ToImmutableList(); TextManager.TextPacks.TryRemove(kvp.Key, out _); - if (newHashSet.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newHashSet); } + if (newList.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newList); } } TextManager.IncrementLanguageVersion(); + if (!TextManager.TextPacks.ContainsKey(GameSettings.CurrentConfig.Language)) + { + DebugConsole.AddWarning($"The language {GameSettings.CurrentConfig.Language} is no longer available. Switching to {TextManager.DefaultLanguage}..."); + var config = GameSettings.CurrentConfig; + config.Language = TextManager.DefaultLanguage; + GameSettings.SetCurrentConfig(config); + } } public override void Sort() { - //Overrides for text packs don't exist! Should we change this? + foreach (var language in TextManager.TextPacks.Keys.ToList()) + { + TextManager.TextPacks[language] = + TextManager.TextPacks[language].Sort((t1, t2) => (t1.ContentFile.ContentPackage?.Index ?? int.MaxValue) - (t2.ContentFile.ContentPackage?.Index ?? int.MaxValue)); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index 3d78d1da8..c932b4470 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -283,7 +283,7 @@ namespace Barotrauma catch (Exception e) { var innermost = e.GetInnermost(); - DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}"); + DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}", contentPackage: this); exception = e; } if (exception != null) @@ -391,7 +391,8 @@ namespace Barotrauma DebugConsole.AddWarning( $"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" + - string.Join('\n', FatalLoadErrors.Select(errorToStr))); + string.Join('\n', FatalLoadErrors.Select(errorToStr)), + this); static string errorToStr(LoadError error) => error.ToString(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index f113e04f1..dc6a465be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -155,6 +155,7 @@ namespace Barotrauma .Distinct(new TypeComparer()) .ForEach(f => f.Sort()); MergedHash = Md5Hash.MergeHashes(All.Select(cp => cp.Hash)); + TextManager.IncrementLanguageVersion(); } public static int IndexOf(ContentPackage contentPackage) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index c97b534ce..26afb242f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -70,7 +70,7 @@ namespace Barotrauma 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(parameterName: "def")] 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); @@ -90,6 +90,7 @@ namespace Barotrauma public Color? GetAttributeColor(string key) => Element.GetAttributeColor(key); public Color[]? GetAttributeColorArray(string key, Color[]? def) => Element.GetAttributeColorArray(key, def); public Rectangle GetAttributeRect(string key, in Rectangle def) => Element.GetAttributeRect(key, def); + public Version GetAttributeVersion(string key, Version def) => Element.GetAttributeVersion(key, def); public T GetAttributeEnum(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def); public (T1, T2) GetAttributeTuple(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def); public (T1, T2)[] GetAttributeTupleArray(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 552fe6a3f..ae30a5412 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Globalization; using Barotrauma.IO; @@ -40,8 +41,8 @@ namespace Barotrauma { public partial class Command { - public readonly string[] names; - public readonly string help; + public readonly ImmutableArray Names; + public readonly string Help; public Action OnExecute; @@ -57,8 +58,8 @@ namespace Barotrauma /// public Command(string name, string help, Action onExecute, Func getValidArgs = null, bool isCheat = false) { - names = name.Split('|'); - this.help = help; + Names = name.Split('|').ToIdentifiers().ToImmutableArray(); + this.Help = help; this.OnExecute = onExecute; @@ -76,7 +77,8 @@ namespace Barotrauma #endif if (!allowCheats && !CheatsEnabled && IsCheat) { - NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red); + NewMessage( + $"You need to enable cheats using the command \"enablecheats\" before you can use the command \"{Names.First()}\".", Color.Red); #if USE_STEAM NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red); #endif @@ -88,7 +90,7 @@ namespace Barotrauma public override int GetHashCode() { - return names[0].GetHashCode(); + return Names.First().GetHashCode(); } } @@ -164,7 +166,7 @@ namespace Barotrauma private static void AssignOnExecute(string names, Action onExecute) { - var matchingCommand = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0); + var matchingCommand = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any()); if (matchingCommand == null) { throw new Exception("AssignOnExecute failed. Command matching the name(s) \"" + names + "\" not found."); @@ -187,13 +189,13 @@ namespace Barotrauma { foreach (Command c in commands) { - if (string.IsNullOrEmpty(c.help)) continue; + if (string.IsNullOrEmpty(c.Help)) continue; ShowHelpMessage(c); } } else { - var matchingCommand = commands.Find(c => c.names.Any(name => name == args[0])); + var matchingCommand = commands.Find(c => c.Names.Any(name => name == args[0])); if (matchingCommand == null) { NewMessage("Command " + args[0] + " not found.", Color.Red); @@ -208,7 +210,7 @@ namespace Barotrauma { return new string[][] { - commands.SelectMany(c => c.names).ToArray(), + commands.SelectMany(c => c.Names).Select(n => n.Value).ToArray(), Array.Empty() }; })); @@ -403,7 +405,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - commands.Select(c => c.names[0]).Union(new string[]{ "All" }).ToArray() + commands.Select(c => c.Names.First().Value).Union(new []{ "All" }).ToArray() }; })); @@ -415,7 +417,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - commands.Select(c => c.names[0]).Union(new string[]{ "All" }).ToArray() + commands.Select(c => c.Names.First().Value).Union(new []{ "All" }).ToArray() }; })); @@ -856,7 +858,13 @@ namespace Barotrauma commands.Add(new Command("debugevent", "debugevent [identifier]: outputs debug info about a specific event that's currently active. Mainly intended for debugging events in multiplayer: in single player, the same information is available by enabling debugdraw.", (string[] args) => { - if (GameMain.GameSession?.EventManager is EventManager eventManager && args.Length > 0) + if (args.Length == 0) + { + ThrowError($"Please specify the identifier of the event you want to debug."); + return; + } + + if (GameMain.GameSession?.EventManager is EventManager eventManager) { var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]); if (ev == null) @@ -875,9 +883,18 @@ namespace Barotrauma } }, isCheat: true, getValidArgs: () => { + IEnumerable eventPrefabs; + if (GameMain.GameSession?.EventManager == null || GameMain.GameSession.EventManager.ActiveEvents.None()) + { + eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty); + } + else + { + eventPrefabs = GameMain.GameSession.EventManager.ActiveEvents.Select(e => e.Prefab); + } return new[] { - GameMain.GameSession?.EventManager?.ActiveEvents.Select(ev => ev.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty() + eventPrefabs.Select(ev => ev.Identifier.ToString()).ToArray() ?? Array.Empty() }; })); @@ -1133,7 +1150,7 @@ namespace Barotrauma } },null)); - 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) => + commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the 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; } @@ -1159,7 +1176,7 @@ namespace Barotrauma } Submarine.MainSub.SetPosition(pos); } - else + else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase)) { if (Level.Loaded == null) { @@ -1172,13 +1189,29 @@ namespace Barotrauma pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2; } Submarine.MainSub.SetPosition(pos); + } + else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase)) + { + Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); + + var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub); + if (Level.Loaded?.EndOutpost == null) + { + NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red); + return; + } + var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost); + if (submarineDockingPort != null && outpostDockingPort != null) + { + submarineDockingPort.Dock(outpostDockingPort); + } } }, () => { return new string[][] { - new string[] { "start", "end", "cursor" } + new string[] { "start", "end", "endoutpost", "cursor" } }; }, isCheat: true)); @@ -1595,7 +1628,7 @@ namespace Barotrauma int i = 0; foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections) { - NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).Name, Color.White); + NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).DisplayName, Color.White); i++; } ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) => @@ -1609,7 +1642,7 @@ namespace Barotrauma } Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation); campaign.Map.SelectLocation(location); - NewMessage(location.Name + " selected.", Color.White); + NewMessage(location.DisplayName + " selected.", Color.White); }); } else @@ -1623,7 +1656,7 @@ namespace Barotrauma } Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation); campaign.Map.SelectLocation(location); - NewMessage(location.Name + " selected.", Color.White); + NewMessage(location.DisplayName + " selected.", Color.White); } })); @@ -1895,7 +1928,7 @@ namespace Barotrauma { if (location.Stores != null) { - var msg = "--- Location: " + location.Name + " ---"; + var msg = "--- Location: " + location.DisplayName + " ---"; foreach (var store in location.Stores) { msg += $"\nStore identifier: {store.Value.Identifier}"; @@ -1959,7 +1992,7 @@ namespace Barotrauma InitProjectSpecific(); - commands.Sort((c1, c2) => c1.names[0].CompareTo(c2.names[0])); + commands.Sort((c1, c2) => c1.Names.First().CompareTo(c2.Names.First())); } public static string AutoComplete(string command, int increment = 1) @@ -1970,14 +2003,14 @@ namespace Barotrauma //if an argument is given or the last character is a space, attempt to autocomplete the argument if (args.Length > 0 || (splitCommand.Length > 0 && command.Last() == ' ')) { - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0])); - if (matchingCommand == null || matchingCommand.GetValidArgs == null) return command; + Command matchingCommand = commands.Find(c => c.Names.Contains(splitCommand[0].ToIdentifier())); + if (matchingCommand?.GetValidArgs == null) { return command; } int autoCompletedArgIndex = args.Length > 0 && command.Last() != ' ' ? args.Length - 1 : args.Length; //get all valid arguments for the given command string[][] allArgs = matchingCommand.GetValidArgs(); - if (allArgs == null || allArgs.GetLength(0) < autoCompletedArgIndex + 1) return command; + if (allArgs == null || allArgs.GetLength(0) < autoCompletedArgIndex + 1) { return command; } if (string.IsNullOrEmpty(currentAutoCompletedCommand)) { @@ -1989,7 +2022,7 @@ namespace Barotrauma currentAutoCompletedCommand.Trim().Length <= arg.Length && arg.Substring(0, currentAutoCompletedCommand.Trim().Length).ToLower() == currentAutoCompletedCommand.Trim().ToLower()).ToArray(); - if (validArgs.Length == 0) return command; + if (validArgs.Length == 0) { return command; } currentAutoCompletedIndex = MathUtils.PositiveModulo(currentAutoCompletedIndex + increment, validArgs.Length); string autoCompletedArg = validArgs[currentAutoCompletedIndex]; @@ -2010,13 +2043,13 @@ namespace Barotrauma currentAutoCompletedCommand = command; } - List matchingCommands = new List(); + List matchingCommands = new List(); foreach (Command c in commands) { - foreach (string name in c.names) + foreach (var name in c.Names) { - if (currentAutoCompletedCommand.Length > name.Length) continue; - if (currentAutoCompletedCommand == name.Substring(0, currentAutoCompletedCommand.Length)) + if (currentAutoCompletedCommand.Length > name.Value.Length) { continue; } + if (name.StartsWith(currentAutoCompletedCommand)) { matchingCommands.Add(name); } @@ -2026,7 +2059,7 @@ namespace Barotrauma if (matchingCommands.Count == 0) return command; currentAutoCompletedIndex = MathUtils.PositiveModulo(currentAutoCompletedIndex + increment, matchingCommands.Count); - return matchingCommands[currentAutoCompletedIndex]; + return matchingCommands[currentAutoCompletedIndex].Value; } } @@ -2064,9 +2097,9 @@ namespace Barotrauma return; } - string firstCommand = splitCommand[0].ToLowerInvariant(); + Identifier firstCommand = splitCommand[0].ToIdentifier(); - if (!firstCommand.Equals("admin", StringComparison.OrdinalIgnoreCase)) + if (firstCommand != "admin") { NewCommand(command); } @@ -2074,7 +2107,7 @@ namespace Barotrauma #if CLIENT if (GameMain.Client != null) { - Command matchingCommand = commands.Find(c => c.names.Contains(firstCommand)); + Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand)); if (matchingCommand == null) { //if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side @@ -2095,12 +2128,12 @@ namespace Barotrauma } return; } - if (!IsCommandPermitted(splitCommand[0].ToLowerInvariant(), GameMain.Client)) + if (!IsCommandPermitted(firstCommand, GameMain.Client)) { #if DEBUG - AddWarning($"You're not permitted to use the command \"{splitCommand[0].ToLowerInvariant()}\". Executing the command anyway because this is a debug build."); + AddWarning($"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build."); #else - ThrowError($"You're not permitted to use the command \"{splitCommand[0].ToLowerInvariant()}\"!"); + ThrowError($"You're not permitted to use the command \"{firstCommand}\"!"); return; #endif } @@ -2110,7 +2143,7 @@ namespace Barotrauma bool commandFound = false; foreach (Command c in commands) { - if (!c.names.Contains(firstCommand)) { continue; } + if (!c.Names.Contains(firstCommand)) { continue; } c.Execute(splitCommand.Skip(1).ToArray()); commandFound = true; break; @@ -2397,8 +2430,9 @@ namespace Barotrauma #endif } - public static void LogError(string msg, Color? color = null) + public static void LogError(string msg, Color? color = null, ContentPackage contentPackage = null) { + msg = AddContentPackageInfoToMessage(msg, contentPackage); color ??= Color.Red; NewMessage(msg, color.Value, isCommand: false, isError: true); } @@ -2515,7 +2549,7 @@ namespace Barotrauma return true; } - public static Command FindCommand(string commandName) => commands.Find(c => c.names.Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase))); + public static Command FindCommand(string commandName) => commands.Find(c => c.Names.Contains(commandName.ToIdentifier())); public static void Log(LocalizedString message) => Log(message?.Value); @@ -2532,8 +2566,9 @@ namespace Barotrauma ThrowError(error.Value, e, createMessageBox, appendStackTrace); } - public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) + public static void ThrowError(string error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false) { + error = AddContentPackageInfoToMessage(error, contentPackage); if (e != null) { error += " {" + e.Message + "}\n"; @@ -2547,7 +2582,7 @@ namespace Barotrauma error += "\n\nInner exception: " + innermost.Message + "\n"; if (innermost.StackTrace != null) { - error += innermost.StackTrace.CleanupStackTrace(); ; + error += innermost.StackTrace.CleanupStackTrace(); } } } @@ -2580,10 +2615,22 @@ namespace Barotrauma errorMsg); } - public static void AddWarning(string warning) + public static void AddWarning(string warning, ContentPackage contentPackage = null) { + warning = AddContentPackageInfoToMessage($"WARNING: {warning}", contentPackage); System.Diagnostics.Debug.WriteLine(warning); - NewMessage($"WARNING: {warning}", Color.Yellow); + NewMessage(warning, Color.Yellow); + } + + private static string AddContentPackageInfoToMessage(string message, ContentPackage contentPackage) + { + if (contentPackage == null) { return message; } +#if CLIENT + string color = XMLExtensions.ToStringHex(Color.MediumPurple); + return $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {message}"; +#else + return $"[{contentPackage.Name}] {message}"; +#endif } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 813524345..9fcaed105 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -159,11 +159,13 @@ namespace Barotrauma OnItemDeconstructedInventory, OnStopTinkering, OnItemPicked, + OnItemSelected, OnGeneticMaterialCombinedOrRefined, OnCrewGeneticMaterialCombinedOrRefined, AfterSubmarineAttacked, OnApplyTreatment, OnStatusEffectIdentifier, + OnRepairedOutsideLeak } /// @@ -550,7 +552,27 @@ namespace Barotrauma /// /// Can be used to prevent certain talents from being unlocked by specifying the talent's identifier via CharacterAbilityGivePermanentStat. /// - LockedTalents + LockedTalents, + + /// + /// Used to reduce or increase the cost of hiring certain jobs by a percentage. + /// + HireCostMultiplier, + + /// + /// Used to increase how much items can stack in the characters inventory. + /// + InventoryExtraStackSize, + + /// + /// Modifies the range of the sounds emitted by the character (can be used to make the character easier or more difficult for monsters to hear) + /// + SoundRangeMultiplier, + + /// + /// Modifies how far the character can be seen from (can be used to make the character easier or more difficult for monsters to see) + /// + SightRangeMultiplier } internal enum ItemTalentStats @@ -565,7 +587,8 @@ namespace Barotrauma ReactorMaxOutput, ReactorFuelConsumption, DeconstructorSpeed, - FabricationSpeed + FabricationSpeed, + ExtraStackSize } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 16622dee8..da3967687 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -34,7 +34,8 @@ namespace Barotrauma { if (prefab.ConfigElement.GetAttribute("itemname") != null) { - DebugConsole.ThrowError("Error in ArtifactEvent - use item identifier instead of the name of the item."); + DebugConsole.ThrowError("Error in ArtifactEvent - use item identifier instead of the name of the item.", + contentPackage: prefab?.ContentPackage); string itemName = prefab.ConfigElement.GetAttributeString("itemname", ""); itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; if (itemPrefab == null) @@ -44,11 +45,12 @@ namespace Barotrauma } else { - string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", ""); - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + Identifier itemIdentifier = prefab.ConfigElement.GetAttributeIdentifier("itemidentifier", Identifier.Empty); + itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier) as ItemPrefab; if (itemPrefab == null) { - DebugConsole.ThrowError("Error in ArtifactEvent - couldn't find an item prefab with the identifier " + itemIdentifier); + DebugConsole.ThrowError("Error in ArtifactEvent - couldn't find an item prefab with the identifier " + itemIdentifier, + contentPackage: prefab?.ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 1df74bcb8..0e4358efb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -39,7 +39,7 @@ namespace Barotrauma public Event(EventPrefab prefab) { - this.prefab = prefab; + this.prefab = prefab ?? throw new ArgumentNullException(nameof(prefab)); } public virtual IEnumerable GetFilesToPreload() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index 5fed3e0ce..8c11d1829 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -1,3 +1,6 @@ +using Barotrauma.Extensions; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -8,21 +11,56 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier TargetTag { get; set; } - private PropertyConditional Conditional { get; } + [Serialize(PropertyConditional.LogicalOperatorType.Or, IsPropertySaveable.Yes)] + public PropertyConditional.LogicalOperatorType LogicalOperator { get; set; } + + private ImmutableArray Conditionals { get; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ApplyTagToLinkedHulls { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside when the item is used.")] + public Identifier ApplyTagToHull { get; set; } public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (TargetTag.IsEmpty) { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed."); + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.", + contentPackage: parentEvent.Prefab.ContentPackage); } - Conditional = PropertyConditional.FromXElement(element, IsNotTargetTagAttribute).FirstOrDefault(); - if (Conditional == null) + var conditionalElements = element.GetChildElements("Conditional"); + if (conditionalElements.None()) { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed."); + //backwards compatibility + Conditionals = PropertyConditional.FromXElement(element, IsConditionalAttribute).ToImmutableArray(); + } + else + { + var conditionalList = new List(); + foreach (ContentXElement subElement in conditionalElements) + { + conditionalList.AddRange(PropertyConditional.FromXElement(subElement)); + break; + } + Conditionals = conditionalList.ToImmutableArray(); } - static bool IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag"; + if (Conditionals.None()) + { + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.", + contentPackage: parentEvent.Prefab.ContentPackage); + } + + static bool IsConditionalAttribute(XAttribute attribute) + { + var nameAsIdentifier = attribute.NameAsIdentifier(); + return + nameAsIdentifier != nameof(TargetTag) && + nameAsIdentifier != nameof(LogicalOperator) && + nameAsIdentifier != nameof(ApplyTagToLinkedHulls) && + nameAsIdentifier != nameof(ApplyTagToHull); + } } private string GetEventName() @@ -32,31 +70,65 @@ namespace Barotrauma protected override bool? DetermineSuccess() { - ISerializableEntity target = null; + IEnumerable targets = null; if (!TargetTag.IsEmpty) { - foreach (var t in ParentEvent.GetTargets(TargetTag)) + targets = ParentEvent.GetTargets(TargetTag).OfType(); + } + + if (targets.None()) + { + DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.", + contentPackage: ParentEvent.Prefab.ContentPackage); + } + + if (targets.None() || Conditionals.None()) + { + foreach (var target in targets) { - if (t is ISerializableEntity e) - { - target = e; - break; - } + ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls); } - } - if (target == null) - { - DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed."); - } - if (target == null || Conditional == null) - { return true; } + else + { + bool success = false; + foreach (var target in targets) + { + if (ConditionalsMatch(target)) + { + success = true; + ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls); + } + } + return success; + } + } + + private bool ConditionalsMatch(ISerializableEntity target) + { + if (LogicalOperator == PropertyConditional.LogicalOperatorType.And) + { + return Conditionals.All(c => ConditionalMatches(target, c)); + } + else + { + return Conditionals.Any(c => ConditionalMatches(target, c)); + } + } + + private static bool ConditionalMatches(ISerializableEntity target, PropertyConditional conditional) + { if (target is Item item) { - return item.ConditionalMatches(Conditional); + if (!conditional.TargetItemComponent.IsNullOrEmpty() && + item.Components.None(ic => ic.Name == conditional.TargetItemComponent)) + { + return false; + } + return item.ConditionalMatches(conditional); } - return Conditional.Matches(target); + return conditional.Matches(target); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index df038e70c..f56d50e29 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -30,7 +30,8 @@ namespace Barotrauma Condition = element.GetAttributeString("value", string.Empty)!; if (string.IsNullOrEmpty(Condition)) { - DebugConsole.ThrowError($"Error in scripted event \"{parentEvent.Prefab.Identifier}\". CheckDataAction with no condition set ({element})."); + DebugConsole.ThrowError($"Error in scripted event \"{parentEvent.Prefab.Identifier}\". CheckDataAction with no condition set ({element}).", + contentPackage: element?.ContentPackage); } } } @@ -42,7 +43,8 @@ namespace Barotrauma Condition = element.GetAttributeString("value", string.Empty)!; if (string.IsNullOrEmpty(Condition)) { - DebugConsole.ThrowError($"Error in scripted event \"{parentDebugString}\". CheckDataAction with no condition set ({element})."); + DebugConsole.ThrowError($"Error in scripted event \"{parentDebugString}\". CheckDataAction with no condition set ({element}).", + contentPackage: element?.ContentPackage); } } } @@ -59,7 +61,8 @@ namespace Barotrauma (Operator, string value) = PropertyConditional.ExtractComparisonOperatorFromConditionString(Condition); if (Operator == PropertyConditional.ComparisonOperatorType.None) { - DebugConsole.ThrowError($"{Condition} is invalid, 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.", + contentPackage: ParentEvent?.Prefab?.ContentPackage); return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index 275203b94..727d627dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -77,7 +77,8 @@ namespace Barotrauma ItemIdentifiers.None() && TargetTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check."); + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check.", + contentPackage: element.ContentPackage); } checkPercentage = element.GetAttribute(nameof(RequiredConditionalMatchPercentage)) is not null; if (checkPercentage && conditionals.None()) @@ -86,7 +87,8 @@ namespace Barotrauma } if (Amount != 1 && checkPercentage) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Cannot define both '{Amount}' and '{RequiredConditionalMatchPercentage}' in {nameof(CheckItemAction)}."); + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Cannot define both '{Amount}' and '{RequiredConditionalMatchPercentage}' in {nameof(CheckItemAction)}.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs index 870a7ee9c..39b427ba4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs @@ -32,7 +32,8 @@ namespace Barotrauma var targetCharacters = ParentEvent.GetTargets(TargetTag); if (targetCharacters.None()) { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.", + contentPackage: ParentEvent.Prefab.ContentPackage); return false; } foreach (var t in targetCharacters) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs index 5f6f19e47..478fb872d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs @@ -1,7 +1,5 @@ #nullable enable -using System; using System.Diagnostics; -using System.Xml.Linq; namespace Barotrauma { @@ -31,7 +29,8 @@ namespace Barotrauma } default: { - DebugConsole.ThrowError("CheckReputationAction requires a \"TargetType\" but none were specified."); + DebugConsole.ThrowError("CheckReputationAction requires a \"TargetType\" but none were specified.", + contentPackage: ParentEvent.Prefab.ContentPackage); break; } } @@ -41,7 +40,8 @@ namespace Barotrauma protected override bool GetBool(CampaignMode campaignMode) { - DebugConsole.ThrowError("Boolean comparison cannot be applied to reputations."); + DebugConsole.ThrowError("Boolean comparison cannot be applied to reputations.", + contentPackage: ParentEvent.Prefab.ContentPackage); return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs index c03e7991e..0e673e047 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs @@ -87,13 +87,13 @@ namespace Barotrauma #if DEBUG void Error(string errorMsg) { - DebugConsole.ThrowError(errorMsg); + DebugConsole.ThrowError(errorMsg, contentPackage: ParentEvent.Prefab.ContentPackage); } #else void Error(string errorMsg) { - DebugConsole.LogError(errorMsg); + DebugConsole.LogError(errorMsg, contentPackage: ParentEvent.Prefab.ContentPackage); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs index f3833fa6d..ab2b9fde6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs @@ -17,7 +17,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Cannot use the action {nameof(CheckTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event."); + DebugConsole.ThrowError($"Cannot use the action {nameof(CheckTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs index 7f8e26526..9ab84cd3e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs @@ -16,7 +16,8 @@ namespace Barotrauma { if (parentEvent is not TraitorEvent) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - {nameof(CheckTraitorVoteAction)} can only be used in traitor events."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - {nameof(CheckTraitorVoteAction)} can only be used in traitor events.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs index 35e4eeb9c..119fa0649 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs @@ -1,5 +1,6 @@ #nullable enable - +using Microsoft.Xna.Framework; +using System.Linq; namespace Barotrauma { @@ -8,12 +9,18 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")] public Identifier EntityTag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Entities that also have this tag are excluded.")] + public Identifier ExcludedEntityTag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check to.")] public Identifier TargetTag { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Does the entity need to be facing the target? Only valid if the entity is a character.")] public bool CheckFacing { get; set; } + [Serialize(1000.0f, IsPropertySaveable.Yes, description: "Maximum distance between the targets.")] + public float MaxDistance { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity who saw the target when the check succeeds.")] public Identifier ApplyTagToEntity { get; set; } @@ -31,11 +38,17 @@ namespace Barotrauma { foreach (var entity in ParentEvent.GetTargets(EntityTag)) { + if (!ExcludedEntityTag.IsEmpty) + { + if (ParentEvent.GetTargets(ExcludedEntityTag).Contains(entity)) { continue; } + } + foreach (var target in ParentEvent.GetTargets(TargetTag)) { if (!AllowSameEntity && entity == target) { continue; } - if (Character.IsTargetVisible(target, entity, CheckFacing)) - { + if (Vector2.DistanceSquared(target.WorldPosition, entity.WorldPosition) > MaxDistance * MaxDistance) { continue; } + if (Character.IsTargetVisible(target, entity, seeThroughWindows: true, CheckFacing)) + { if (!ApplyTagToEntity.IsEmpty) { ParentEvent.AddTarget(ApplyTagToEntity, entity); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 8092bdb91..7817c012c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -116,7 +116,8 @@ namespace Barotrauma { DebugConsole.ThrowError( $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" + - $" - unrecognized child element \"Replace\"."); + $" - unrecognized child element \"Replace\".", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs index c2287af04..70b6f74b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs @@ -61,14 +61,16 @@ namespace Barotrauma } if (MinAmount > MaxAmount && MaxAmount > -1) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {MinAmount} is larger than {MaxAmount} in {nameof(CountTargetsAction)}."); + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {MinAmount} is larger than {MaxAmount} in {nameof(CountTargetsAction)}.", + contentPackage: element.ContentPackage); } } else { if (MinPercentageRelativeToTarget < 0.0f && MaxPercentageRelativeToTarget < 0.0f) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Comparing to another target, but neither {nameof(MinPercentageRelativeToTarget)} or {nameof(MaxPercentageRelativeToTarget)} is set."); + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Comparing to another target, but neither {nameof(MinPercentageRelativeToTarget)} or {nameof(MaxPercentageRelativeToTarget)} is set.", + contentPackage: element.ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index dd086ec74..f9be2652e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -36,7 +36,8 @@ namespace Barotrauma { if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { - DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction."); + DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction.", + contentPackage: elem.ContentPackage); continue; } var action = Instantiate(scriptedEvent, e); @@ -140,14 +141,19 @@ namespace Barotrauma Identifier typeName = element.Name.ToString().ToIdentifier(); if (typeName == "TutorialSegmentAction") { - typeName = "EventObjectiveAction".ToIdentifier(); + typeName = nameof(EventObjectiveAction).ToIdentifier(); + } + else if (typeName == "TutorialHighlightAction") + { + typeName = nameof(HighlightAction).ToIdentifier(); } actionType = Type.GetType("Barotrauma." + typeName, throwOnError: true, ignoreCase: true); if (actionType == null) { throw new NullReferenceException(); } } catch { - DebugConsole.ThrowError($"Could not find an {nameof(EventAction)} class of the type \"{element.Name}\"."); + DebugConsole.ThrowError($"Could not find an {nameof(EventAction)} class of the type \"{element.Name}\".", + contentPackage: element.ContentPackage); return null; } @@ -162,11 +168,36 @@ namespace Barotrauma } catch (Exception ex) { - DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString()); + DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString(), + contentPackage: element.ContentPackage); return null; } } + protected void ApplyTagsToHulls(Entity entity, Identifier hullTag, Identifier linkedHullTag) + { + var currentHull = entity switch + { + Item item => item.CurrentHull, + Character character => character.CurrentHull, + _ => null, + }; + if (currentHull == null) { return; } + + if (!hullTag.IsEmpty) + { + ParentEvent.AddTarget(hullTag, currentHull); + } + if (!linkedHullTag.IsEmpty) + { + ParentEvent.AddTarget(linkedHullTag, currentHull); + foreach (var linkedHull in currentHull.GetLinkedEntities()) + { + ParentEvent.AddTarget(linkedHullTag, linkedHull); + } + } + } + /// /// Rich test to display in debugdraw /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs index 7fc3af8b5..c6f24003d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs @@ -24,7 +24,8 @@ namespace Barotrauma { if (Id == Identifier.Empty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no id."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no id.", + contentPackage: element.ContentPackage); } //append the target tag so logs targeted to different players don't interfere with each other even if they use the same Id Id = (Id.ToString() + TargetTag).ToIdentifier(); @@ -42,7 +43,8 @@ namespace Barotrauma { if (Text.IsNullOrEmpty()) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no text set ({element})."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no text set ({element}).", + contentPackage: element.ContentPackage); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs index 17316466f..2faa398bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs @@ -2,7 +2,7 @@ namespace Barotrauma { partial class EventObjectiveAction : EventAction { - public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove }; + public enum SegmentActionType { Trigger, Add, AddIfNotFound, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove }; [Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)] public SegmentActionType Type { get; set; } @@ -49,13 +49,15 @@ namespace Barotrauma { DebugConsole.ThrowError( $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\""+ - $" - {nameof(TextTag)} will do nothing unless the action triggers a message box or a video."); + $" - {nameof(TextTag)} will do nothing unless the action triggers a message box or a video.", + contentPackage: element.ContentPackage); } if (element.GetChildElement("Replace") != null) { DebugConsole.ThrowError( $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" + - $" - unrecognized child element \"Replace\"."); + $" - unrecognized child element \"Replace\".", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs index 7031d0daf..f2899ff8a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs @@ -14,7 +14,8 @@ namespace Barotrauma { if (TargetTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveExpAction)} without a target tag (the action needs to know whose skill to check)."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveExpAction)} without a target tag (the action needs to know whose skill to check).", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index b051966cd..8f8cc7d0b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs @@ -17,7 +17,8 @@ namespace Barotrauma { if (TargetTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveSkillExpAction)} without a target tag (the action needs to know whose skill to check)."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveSkillExpAction)} without a target tag (the action needs to know whose skill to check).", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs index 95c317203..d2cb4011f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs @@ -5,17 +5,31 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public string Name { get; set; } + [Serialize(-1, IsPropertySaveable.Yes)] + public int MaxTimes { get; set; } + + private int counter; + public GoTo(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override bool IsFinished(ref string goTo) { - goTo = Name; + if (counter < MaxTimes || MaxTimes <= 0) + { + goTo = Name; + counter++; + } return true; } public override string ToDebugString() { - return $"[-] Go to label \"{Name}\""; + string msg = $"[-] Go to label \"{Name}\""; + if (MaxTimes > 0) + { + msg += $" ({counter}/{MaxTimes})"; + } + return msg; } public override void Reset() { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs new file mode 100644 index 000000000..8900299ea --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs @@ -0,0 +1,43 @@ +#nullable enable +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +partial class HighlightAction : EventAction +{ + private static readonly Color highlightColor = Color.Orange; + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "Only the player controlling this character will see the highlight. If empty, all players will see it.")] + public Identifier TargetCharacter { get; set; } + + [Serialize(true, IsPropertySaveable.Yes)] + public bool State { get; set; } + + private bool isFinished; + + public HighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + var targetCharacters = TargetCharacter.IsEmpty ? null : ParentEvent.GetTargets(TargetCharacter).OfType(); + foreach (var target in ParentEvent.GetTargets(TargetTag)) + { + SetHighlightProjSpecific(target, targetCharacters); + } + isFinished = true; + } + + partial void SetHighlightProjSpecific(Entity entity, IEnumerable? targetCharacters); + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() => isFinished = false; +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 3973643a5..4592e6751 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -37,11 +37,13 @@ namespace Barotrauma { if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": neither MissionIdentifier or MissionTag has been configured."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": neither MissionIdentifier or MissionTag has been configured.", + contentPackage: element.ContentPackage); } if (!MissionIdentifier.IsEmpty && !MissionTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored.", + contentPackage: element.ContentPackage); } LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); random = new MTRandom(parentEvent.RandomSeed); @@ -103,11 +105,13 @@ namespace Barotrauma { if (!MissionIdentifier.IsEmpty) { - unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); + unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier, + invokingContentPackage: ParentEvent.Prefab.ContentPackage); } else if (!MissionTag.IsEmpty) { - unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag, random); + unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag, random, + invokingContentPackage: ParentEvent.Prefab.ContentPackage); } if (campaign is MultiPlayerCampaign mpCampaign) { @@ -119,11 +123,11 @@ namespace Barotrauma 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}\"."); + DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.DisplayName}\"."); } else { - DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the connection from \"{unlockedMission.Locations[0].Name}\" to \"{unlockedMission.Locations[1].Name}\"."); + DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the connection from \"{unlockedMission.Locations[0].DisplayName}\" to \"{unlockedMission.Locations[1].DisplayName}\"."); } #if CLIENT new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", unlockedMission.Name), @@ -139,7 +143,8 @@ namespace Barotrauma } else { - DebugConsole.AddWarning($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\" (LocationType: {string.Join(", ", LocationTypes)}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})"); + DebugConsole.AddWarning($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\" (LocationType: {string.Join(", ", LocationTypes)}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})", + ParentEvent.Prefab.ContentPackage); } } isFinished = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs index d6deb9b1c..02b726aa8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs @@ -24,7 +24,8 @@ namespace Barotrauma State = element.GetAttributeInt("value", State); if (MissionIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs index 013b48771..a21925c94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs @@ -12,7 +12,7 @@ namespace Barotrauma public Identifier Type { get; set; } [Serialize("", IsPropertySaveable.Yes)] - public string Name { get; set; } + public Identifier Name { get; set; } private bool isFinished; @@ -43,7 +43,8 @@ namespace Barotrauma 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}\"."); + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{Faction}\".", + contentPackage: ParentEvent?.Prefab?.ContentPackage); } else { @@ -55,7 +56,8 @@ namespace Barotrauma 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}\"."); + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{SecondaryFaction}\".", + contentPackage: ParentEvent.Prefab.ContentPackage); } else { @@ -67,16 +69,17 @@ namespace Barotrauma 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}\"."); + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a location type with the identifier \"{Type}\".", + contentPackage: ParentEvent.Prefab.ContentPackage); } else if (!location.LocationTypeChangesBlocked) { location.ChangeType(campaign, locationType); } } - if (!string.IsNullOrEmpty(Name)) + if (!Name.IsEmpty) { - location.ForceName(TextManager.Get(Name).Fallback(Name).Value); + location.ForceName(Name); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index 64861f6e4..239b95f60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -30,7 +30,8 @@ namespace Barotrauma var enums = Enum.GetValues(typeof(CharacterTeamType)).Cast(); if (!enums.Contains(TeamID)) { - 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)))}."); + 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)))}.", + contentPackage: element.ContentPackage); } } @@ -100,7 +101,7 @@ namespace Barotrauma WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human); if (subWaypoint != null) { - npc.GiveIdCardTags(subWaypoint, requireSpawnPointTagsNotGiven: false, createNetworkEvent: true); + npc.GiveIdCardTags(subWaypoint, createNetworkEvent: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs index 3ab365409..a9e4ffea3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs @@ -13,11 +13,13 @@ namespace Barotrauma { if (Chance >= 1.0f) { - DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 1.0 (100%) or more, the action will always succeed."); + DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 1.0 (100%) or more, the action will always succeed.", + contentPackage: element.ContentPackage); } else if (Chance <= 0.0f) { - DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 0 or less, the action will never succeed."); + DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 0 or less, the action will never succeed.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs index 41c9391f2..f161a63fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs @@ -54,7 +54,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found."); + DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found.", + contentPackage: ParentEvent.Prefab.ContentPackage); } break; @@ -66,7 +67,8 @@ namespace Barotrauma } default: { - DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified."); + DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified.", + contentPackage: ParentEvent.Prefab.ContentPackage); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs index 5361d5696..af798fefc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs @@ -14,7 +14,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Cannot use the action {nameof(SetTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event."); + DebugConsole.ThrowError($"Cannot use the action {nameof(SetTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event.", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs index c74ddea95..b3376e4a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs @@ -23,7 +23,8 @@ namespace Barotrauma { if (TargetTag.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": SkillCheckAction without a target tag (the action needs to know whose skill to check)."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": SkillCheckAction without a target tag (the action needs to know whose skill to check).", + contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index e9c3a624e..15a58b8c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -105,7 +105,8 @@ namespace Barotrauma { DebugConsole.ThrowError( $"Error in even \"{(parentEvent.Prefab?.Identifier.ToString() ?? "unknown")}\". " + - $"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?"); + $"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?", + contentPackage: ParentEvent.Prefab.ContentPackage); } } @@ -233,7 +234,8 @@ namespace Barotrauma { if (MapEntityPrefab.FindByIdentifier(ItemIdentifier) is not ItemPrefab itemPrefab) { - DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)"); + DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)", + contentPackage: ParentEvent.Prefab.ContentPackage); } else { @@ -256,7 +258,8 @@ namespace Barotrauma if (spawnInventory == null) { - DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found."); + DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found.", + contentPackage: ParentEvent.Prefab.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 508ff6a43..8a1e389fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -90,31 +90,46 @@ namespace Barotrauma private void TagPlayers() { - AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters)); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Character, + e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters)); } private void TagTraitors() { - AddTargetPredicate(Tags.Traitor, e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated); + AddTargetPredicate( + Tags.Traitor, + ScriptedEvent.TargetPredicate.EntityType.Character, + e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated); } private void TagNonTraitors() { - AddTargetPredicate(Tags.NonTraitor, e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); + AddTargetPredicate( + Tags.NonTraitor, + ScriptedEvent.TargetPredicate.EntityType.Character, + e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); } private void TagNonTraitorPlayers() { - AddTargetPredicate(Tags.NonTraitorPlayer, e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); + AddTargetPredicate( + Tags.NonTraitorPlayer, + ScriptedEvent.TargetPredicate.EntityType.Character, + e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); } private void TagBots(bool playerCrewOnly) { - AddTargetPredicate(Tag, e => - e is Character c && - c.IsBot && - (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) && - (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Character, + e => + e is Character c && + c.IsBot && + (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) && + (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1)); } private void TagCrew() @@ -139,42 +154,67 @@ namespace Barotrauma private void TagStructuresByIdentifier(Identifier identifier) { - AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Structure, + e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); } private void TagStructuresBySpecialTag(Identifier tag) { - AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Structure, + e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag); } private void TagItemsByIdentifier(Identifier identifier) { - AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Item, + e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); } private void TagItemsByTag(Identifier tag) { - AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag)); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Item, + e => e is Item it && IsValidItem(it) && it.HasTag(tag)); } private void TagHulls() { - AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine)); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Hull, + e => e is Hull h && SubmarineTypeMatches(h.Submarine)); } private void TagHullsByName(Identifier name) { - AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Hull, + e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); } private void TagSubmarinesByType(Identifier type) { - AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); + AddTargetPredicate( + Tag, + ScriptedEvent.TargetPredicate.EntityType.Submarine, + 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); + return + (!it.HiddenInGame || AllowHiddenItems) && + //if the item has just spawned, it may be in a hull but not moved into the coordinate space of the hull yet + //= it.Submarine still null + SubmarineTypeMatches(it.Submarine ?? it.CurrentHull?.Submarine ?? it.ParentInventory?.Owner?.Submarine); } private bool SubmarineTypeMatches(Submarine sub) @@ -197,7 +237,7 @@ namespace Barotrauma } } - private void AddTargetPredicate(Identifier tag, Predicate predicate) + private void AddTargetPredicate(Identifier tag, ScriptedEvent.TargetPredicate.EntityType entityType, Predicate predicate) { if (ChoosePercentage > 0.0f) { @@ -209,7 +249,7 @@ namespace Barotrauma } else { - ParentEvent.AddTargetPredicate(tag, predicate); + ParentEvent.AddTargetPredicate(tag, entityType, predicate); mustRecheckTargets = true; } } @@ -291,7 +331,8 @@ namespace Barotrauma else { string errorMessage = $"Error in TagAction (event \"{ParentEvent.Prefab.Identifier}\") - unrecognized target criteria \"{key}\"."; - DebugConsole.ThrowError(errorMessage); + DebugConsole.ThrowError(errorMessage, + contentPackage: ParentEvent.Prefab?.ContentPackage); GameAnalyticsManager.AddErrorEventOnce($"TagAction.Update:InvalidCriteria_{ParentEvent.Prefab.Identifier}_{key}", GameAnalyticsManager.ErrorSeverity.Error, errorMessage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs index 51e265f83..ae6a8c75c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs @@ -36,7 +36,8 @@ var eventPrefab = EventSet.GetEventPrefab(Identifier); if (eventPrefab == null) { - DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}."); + DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.", + contentPackage: ParentEvent.Prefab.ContentPackage); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs deleted file mode 100644 index 190f7fdc0..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Barotrauma; - -partial class TutorialHighlightAction : EventAction -{ - [Serialize("", IsPropertySaveable.Yes)] - public Identifier TargetTag { get; set; } - - [Serialize(true, IsPropertySaveable.Yes)] - public bool State { get; set; } - - private bool isFinished; - - public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) - { - if (GameMain.NetworkMember != null) - { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(TutorialHighlightAction)} is not supported in multiplayer."); - } - } - - public override void Update(float deltaTime) - { - if (isFinished) { return; } - UpdateProjSpecific(); - isFinished = true; - } - - partial void UpdateProjSpecific(); - - public override bool IsFinished(ref string goToLabel) => isFinished; - - public override void Reset() => isFinished = false; -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs index 601981cb6..89106db36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs @@ -28,7 +28,8 @@ namespace Barotrauma { if (ItemTag.IsEmpty && ItemIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(WaitForItemFabricatedAction)} does't define either a tag or an identifier of the item to check."); + DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(WaitForItemFabricatedAction)} does't define either a tag or an identifier of the item to check.", + contentPackage: element.ContentPackage); } foreach (var item in Item.ItemList) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs index d7e48a299..dce9b2f62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs @@ -30,11 +30,16 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside, and all the hulls it's linked to, when the item is used.")] public Identifier ApplyTagToLinkedHulls { get; set; } + [Serialize(1, IsPropertySaveable.Yes)] + public int RequiredUseCount { get; set; } + private bool isFinished; private readonly HashSet targets = new HashSet(); private readonly HashSet targetComponents = new HashSet(); + private int useCount = 0; + private Identifier onUseEventIdentifier; private Identifier OnUseEventIdentifier { @@ -58,6 +63,14 @@ namespace Barotrauma private void OnItemUsed(Item item, Character user) { + if (!UserTag.IsEmpty) + { + if (!ParentEvent.GetTargets(UserTag).Contains(user)) { return; } + } + + useCount++; + if (useCount < RequiredUseCount) { return; } + if (!ApplyTagToItem.IsEmpty) { ParentEvent.AddTarget(ApplyTagToItem, item); @@ -66,22 +79,7 @@ namespace Barotrauma { ParentEvent.AddTarget(ApplyTagToUser, user); } - if (item.CurrentHull != null) - { - if (!ApplyTagToHull.IsEmpty) - { - ParentEvent.AddTarget(ApplyTagToHull, item.CurrentHull); - } - if (!ApplyTagToLinkedHulls.IsEmpty) - { - ParentEvent.AddTarget(ApplyTagToLinkedHulls, item.CurrentHull); - foreach (var linkedHull in item.CurrentHull.GetLinkedEntities()) - { - ParentEvent.AddTarget(ApplyTagToLinkedHulls, linkedHull); - } - } - } - + ApplyTagsToHulls(item, ApplyTagToHull, ApplyTagToLinkedHulls); DeregisterTargets(); isFinished = true; } @@ -142,6 +140,7 @@ namespace Barotrauma public override void Reset() { isFinished = false; + useCount = 0; DeregisterTargets(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index c7c89bd26..72f17db3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -412,6 +412,7 @@ namespace Barotrauma preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); + timeStamps.Clear(); pathFinder = null; } @@ -518,7 +519,8 @@ namespace Barotrauma { foreach (Identifier missingId in subEventPrefab.GetMissingIdentifiers()) { - DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\"."); + DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\".", + contentPackage: eventSet.ContentPackage); } } @@ -904,7 +906,18 @@ namespace Barotrauma activeEvents.Add(QueuedEvents.Dequeue()); } } - + + public void EntitySpawned(Entity entity) + { + foreach (var ev in activeEvents) + { + if (ev is ScriptedEvent scriptedEvent) + { + scriptedEvent.EntitySpawned(entity); + } + } + } + private void CalculateCurrentIntensity(float deltaTime) { intensityUpdateTimer -= deltaTime; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index e1c076d3c..721f1bcb0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -10,16 +10,48 @@ namespace Barotrauma public readonly ContentXElement ConfigElement; public readonly Type EventType; + + /// + /// The probability for the event to do something if it gets selected. For example, the probability for a MonsterEvent to spawn the monster(s). + /// public readonly float Probability; + + /// + /// When this event occurs, should it trigger the event cooldown during which no new events are triggered? + /// public readonly bool TriggerEventCooldown; + + /// + /// The commonness of the event (i.e. how likely it is for this specific event to be chosen from the event set it's configured in). + /// Only valid if the event set is configured to choose a random event (as opposed to just executing all the events in the set). + /// public readonly float Commonness; + + /// + /// If set, the event set can only be chosen in this biome. + /// public readonly Identifier BiomeIdentifier; + + /// + /// If set, the event set can only be chosen in locations that belong to this faction. + /// public readonly Identifier Faction; public readonly LocalizedString Name; + /// + /// If set, this event is used as an event that can unlock a path to the next biome. + /// public readonly bool UnlockPathEvent; + + /// + /// Only valid if UnlockPathEvent is set to true. The tooltip displayed on the pathway this event is blocking. + /// public readonly string UnlockPathTooltip; + + /// + /// Only valid if UnlockPathEvent is set to true. The reputation requirement displayed on the pathway this event is blocking. + /// public readonly int UnlockPathReputation; public static EventPrefab Create(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) @@ -44,12 +76,14 @@ namespace Barotrauma EventType = Type.GetType("Barotrauma." + ConfigElement.Name, true, true); if (EventType == null) { - DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\"."); + DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\".", + contentPackage: element.ContentPackage); } } catch { - DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\"."); + DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\".", + contentPackage: element.ContentPackage); } Name = TextManager.Get($"eventname.{Identifier}").Fallback(Identifier.ToString()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index e43b63056..b68a159dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -23,6 +23,10 @@ namespace Barotrauma } #endif + /// + /// Event sets are sets of random events that occur within a level (most commonly, monster spawns and scripted events). + /// Event sets can also be nested: a "parent set" can choose from several "subsets", either randomly or by some kind of criteria. + /// sealed class EventSet : Prefab { internal class EventDebugStats @@ -78,21 +82,48 @@ namespace Barotrauma return GetAllEventPrefabs().Find(prefab => prefab.Identifier == identifier); } + /// + /// If enabled, this set can only be chosen in the campaign mode. + /// public readonly bool IsCampaignSet; - //0-100 - public readonly float MinLevelDifficulty, MaxLevelDifficulty; + /// + /// The difficulty of the current level must be equal to or higher than this for this set to be chosen. + /// + public readonly float MinLevelDifficulty; + /// + /// The difficulty of the current level must be equal to or less than this for this set to be chosen. + /// + public readonly float MaxLevelDifficulty; + /// + /// If set, the event set can only be chosen in this biome. + /// public readonly Identifier BiomeIdentifier; + /// + /// If set, the event set can only be chosen in this type of level (outpost level or a connection between outpost levels). + /// public readonly LevelData.LevelType LevelType; + /// + /// If set, the event set can only be chosen in locations of this type. + /// public readonly ImmutableArray LocationTypeIdentifiers; + /// + /// If set, the event set can only be chosen in locations that belong to this faction. + /// public readonly Identifier Faction; + /// + /// If set, one event, or a sub event set, is chosen randomly from this set. + /// public readonly bool ChooseRandom; + /// + /// Only valid if ChooseRandom is enabled. How many random events to choose from the set? + /// private readonly int eventCount = 1; public readonly int SubSetCount = 1; private readonly Dictionary overrideEventCount = new Dictionary(); @@ -102,47 +133,100 @@ namespace Barotrauma /// public readonly bool Exhaustible; + /// + /// The event set won't become active until the submarine has travelled at least this far. A value between 0-1, where 0 is the beginning of the level and 1 the end of the level (e.g. 0.5 would mean the sub needs to be half-way through the level). + /// public readonly float MinDistanceTraveled; + + /// + /// The event set won't become active until the round has lasted at least this many seconds. + /// public readonly float MinMissionTime; //the events in this set are delayed if the current EventManager intensity is not between these values public readonly float MinIntensity, MaxIntensity; + /// + /// If the event is not allowed at start, it won't become active until the submarine has moved at least 50 meters away from the beginning of the level. Only valid in LocationConnections (levels between locations). + /// public readonly bool AllowAtStart; + /// + /// Normally an event (such as a monster spawn) triggers a cooldown during which no new events are created. This can be used to ignore the cooldown. + /// public readonly bool IgnoreCoolDown; + /// + /// Should this event set trigger the event cooldown (during which no new events are created) when it becomes active? + /// + public readonly bool TriggerEventCooldown; + + /// + /// Normally events can only trigger if the intensity of the situation is low enough (e.g. you won't get new monster spawns if the submarine is already facing a disaster). This can be used to ignore the intensity. + /// public readonly bool IgnoreIntensity; - public readonly bool PerRuin, PerCave, PerWreck; + /// + /// The set is applied once per each ruin in the level. Can be used to ensure there's a consistent amount of monster spawns in the ruins in the level regardless of how many there are (and that no ruin monsters spawn if there are no ruins). + /// + public readonly bool PerRuin; + + /// + /// The set is applied once per each cave in the level. Can be used to ensure there's a consistent amount of monster spawns in the cave in the level regardless of how many there are (and that no cave monsters spawn if there are no caves). + /// + public readonly bool PerCave; + + /// + /// The set is applied once per each wreck in the level. Can be used to ensure there's a consistent amount of monster spawns in the wreck in the level regardless of how many there are (and that no wreck monsters spawn if there are no wreck). + /// + public readonly bool PerWreck; + + /// + /// If enabled, this event will not be applied if the level contains hunting grounds. + /// public readonly bool DisableInHuntingGrounds; /// - /// If true, events from this set can only occur once in the level. + /// If enabled, events from this set can only occur once in the level. /// public readonly bool OncePerLevel; + /// + /// Should the event set be delayed if at least half of the crew is away from the submarine? The maximum amount of time the events can get delayed is defined in event manager settings () + /// public readonly bool DelayWhenCrewAway; - public readonly bool TriggerEventCooldown; - + /// + /// Additive sets are important to be aware of when creating custom event sets! If an additive set gets chosen for a level, the game will also select a non-additive one. + /// This means you can for example configure an additive set that spawns custom monsters (and make it very common if you want the monsters to spawn frequently), which will spawn those custom + /// monsters in addition to the vanilla monsters spawned by vanilla sets, without you having to add your custom monsters to every single vanilla set. + /// public readonly bool Additive; + /// + /// The commonness of the event set (i.e. how likely it is for this specific set to be chosen). + /// public readonly float DefaultCommonness; public readonly ImmutableDictionary OverrideCommonness; + /// + /// If set, the event set can trigger again after this amount of seconds has passed since it last triggered. + /// public readonly float ResetTime; /// - /// Used to force an event set based on how many other locations have been discovered before this. (Used for campaign tutorial event sets.) + /// Used to force an event set based on how many other locations have been discovered before this (used for campaign tutorial event sets). /// public readonly int ForceAtDiscoveredNr; /// - /// Used to force an event set based on how many other outposts have been visited before this. (Used for campaign tutorial event sets.) + /// Used to force an event set based on how many other outposts have been visited before this (used for campaign tutorial event sets). /// public readonly int ForceAtVisitedNr; + /// + /// If enabled, this set can only occur when the campaign tutorial is enabled (generally used for the tutorial events). + /// public readonly bool CampaignTutorialOnly; public readonly struct SubEventPrefab @@ -224,7 +308,8 @@ namespace Barotrauma } else { - DebugConsole.AddWarning($"{file.Path}: All root EventSets should have an identifier"); + DebugConsole.AddWarning($"{file.Path}: All root EventSets should have an identifier", + file.ContentPackage); } } @@ -264,7 +349,8 @@ namespace Barotrauma string levelTypeStr = element.GetAttributeString("leveltype", parentSet?.LevelType.ToString() ?? "LocationConnection"); if (!Enum.TryParse(levelTypeStr, true, out LevelType)) { - DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); + DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type.", + contentPackage: element.ContentPackage); } Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); @@ -304,7 +390,8 @@ namespace Barotrauma ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1); if (ForceAtDiscoveredNr >= 0 && ForceAtVisitedNr >= 0) { - DebugConsole.ThrowError($"Error with event set \"{Identifier}\" - both ForceAtDiscoveredNr and ForceAtVisitedNr are defined, this could lead to unexpected behavior"); + DebugConsole.ThrowError($"Error with event set \"{Identifier}\" - both ForceAtDiscoveredNr and ForceAtVisitedNr are defined, this could lead to unexpected behavior", + contentPackage: element.ContentPackage); } DefaultCommonness = element.GetAttributeFloat("commonness", 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index a208a7e50..53ead40a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -123,7 +123,8 @@ namespace Barotrauma var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)); if (!itemsToDestroy.Any()) { - DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\"."); + DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".", + contentPackage: Prefab.ContentPackage); } else { @@ -135,10 +136,11 @@ namespace Barotrauma { foreach (XElement element in itemConfig.Elements()) { - string itemIdentifier = element.GetAttributeString("identifier", ""); - if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) + Identifier itemIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is not ItemPrefab itemPrefab) { - DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found"); + DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found", + contentPackage: Prefab.ContentPackage); continue; } @@ -189,7 +191,8 @@ namespace Barotrauma 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"); + DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found", + contentPackage: Prefab.ContentPackage); continue; } for (int i = 0; i < count; i++) @@ -203,7 +206,8 @@ 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", + contentPackage: Prefab.ContentPackage); continue; } for (int i = 0; i < count; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs index 1a5a8cb3a..d0c52b245 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -51,12 +51,14 @@ namespace Barotrauma TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient); if (TargetRuin == null) { - DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins"); + DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins", + contentPackage: Prefab.ContentPackage); return; } if (targetItemIdentifiers.Length < 1 && targetEnemyIdentifiers.Length < 1) { - DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition"); + DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition", + contentPackage: Prefab.ContentPackage); return; } foreach (var item in Item.ItemList) @@ -88,12 +90,14 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): could not find a character prefab with the species \"{identifier}\""); + DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): could not find a character prefab with the species \"{identifier}\"", + contentPackage: Prefab.ContentPackage); } } if (enemyPrefabs.None()) { - DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no enemy species defined that could be used to spawn more ({minEnemyCount - existingEnemyCount}) enemies"); + DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no enemy species defined that could be used to spawn more ({minEnemyCount - existingEnemyCount}) enemies", + contentPackage: Prefab.ContentPackage); return; } for (int i = 0; i < (minEnemyCount - existingEnemyCount); i++) @@ -102,7 +106,8 @@ namespace Barotrauma var spawnPos = TargetRuin.Submarine.GetWaypoints(false).GetRandomUnsynced(w => w.CurrentHull != null)?.WorldPosition; if (!spawnPos.HasValue) { - DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no valid spawn positions could be found for the additional ({minEnemyCount - existingEnemyCount}) enemies to be spawned"); + DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no valid spawn positions could be found for the additional ({minEnemyCount - existingEnemyCount}) enemies to be spawned", + contentPackage: Prefab.ContentPackage); return; } var newEnemy = Character.Create(prefab.Identifier, spawnPos.Value, ToolBox.RandomSeed(8), createNetworkEvent: false); @@ -151,7 +156,8 @@ namespace Barotrauma #if DEBUG else { - DebugConsole.ThrowError($"Error in Alien Ruin mission (\"{Prefab.Identifier}\"): unexpected target of type {target?.GetType()?.ToString()}"); + DebugConsole.ThrowError($"Error in Alien Ruin mission (\"{Prefab.Identifier}\"): unexpected target of type {target?.GetType()?.ToString()}", + contentPackage: Prefab.ContentPackage); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 224dc40f1..3f22e082b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -46,6 +46,7 @@ namespace Barotrauma } sonarLabel = TextManager.Get("beaconstationsonarlabel"); + DebugConsole.NewMessage("Initialized beacon mission: " + prefab.Identifier, Color.LightSkyBlue, debugOnly: true); } private void LoadMonsters(XElement monsterElement, MonsterSet set) @@ -65,7 +66,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in beacon mission \"{Prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + DebugConsole.ThrowError($"Error in beacon mission \"{Prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: Prefab.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 71dcf88a7..2d40a2fc5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -238,7 +238,8 @@ namespace Barotrauma if (itemConfig == null) { - DebugConsole.ThrowError("Failed to initialize items for cargo mission (itemConfig == null)"); + DebugConsole.ThrowError("Failed to initialize items for cargo mission (itemConfig == null)", + contentPackage: Prefab.ContentPackage); return; } @@ -262,7 +263,7 @@ namespace Barotrauma SpawnedInCurrentOutpost = true, AllowStealing = false }; - item.AddTag("cargomission"); + item.AddTag(Tags.CargoMissionItem); item.AddTag(Prefab.Identifier); foreach (var tag in Prefab.Tags) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 296db0f2a..f4c2a781c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -57,7 +57,7 @@ namespace Barotrauma { for (int n = 0; n < 2; n++) { - descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].Name); + descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].DisplayName); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs index 8eaba7820..1ffc09b93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -110,12 +110,14 @@ namespace Barotrauma 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}\"."); + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: Prefab.ContentPackage); } } else { - DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Monster file not set."); + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Monster file not set.", + contentPackage: Prefab.ContentPackage); } Identifier minionName = prefab.ConfigElement.GetAttributeIdentifier("minionfile", Identifier.Empty); @@ -124,7 +126,8 @@ namespace Barotrauma 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}\"."); + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: Prefab.ContentPackage); } } @@ -137,7 +140,8 @@ namespace Barotrauma 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}\"."); + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find an item prefab with the name \"{projectileId}\".", + contentPackage: Prefab.ContentPackage); } } @@ -152,7 +156,8 @@ namespace Barotrauma bossSpawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag)); if (bossSpawnPoint == null) { - DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\"."); + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\".", + contentPackage: Prefab.ContentPackage); return; } if (!IsClient) @@ -171,14 +176,16 @@ namespace Barotrauma } if (destructibleItemTag.IsEmpty) { - DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Destructible item tag not set."); + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Destructible item tag not set.", + contentPackage: Prefab.ContentPackage); 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}\"."); + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\".", + contentPackage: Prefab.ContentPackage); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 10fa64cb8..9c900601f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -77,7 +77,8 @@ namespace Barotrauma { if (inMission) { - DebugConsole.ThrowError("MainSub was null when trying to retrieve submarine size for determining escorted character count!"); + DebugConsole.ThrowError("MainSub was null when trying to retrieve submarine size for determining escorted character count!", + contentPackage: Prefab.ContentPackage); } return 1; } @@ -117,7 +118,10 @@ namespace Barotrauma { characterStatusEffects[humanPrefab] = new List { newEffect }; } - characterStatusEffects[humanPrefab].Add(newEffect); + else + { + characterStatusEffects[humanPrefab].Add(newEffect); + } } } } @@ -180,7 +184,8 @@ namespace Barotrauma if (scalingCharacterCount * characterConfig.Elements().Count() != characters.Count) { - DebugConsole.AddWarning("Character count did not match expected character count in InitCharacters of EscortMission"); + DebugConsole.AddWarning("Character count did not match expected character count in InitCharacters of EscortMission", + Prefab.ContentPackage); return; } int i = 0; @@ -220,7 +225,8 @@ namespace Barotrauma if (characterConfig == null) { - DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)"); + DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)", + contentPackage: Prefab.ContentPackage); return; } @@ -258,7 +264,7 @@ namespace Barotrauma { 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); + ContentXElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty); if (randomElement != null) { HumanPrefab.InitializeItem(character, randomElement, character.Submarine, humanPrefab: null, createNetworkEvents: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index fbbdc98c1..561fed7d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -119,14 +119,16 @@ namespace Barotrauma { if (MapEntityPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { - DebugConsole.ThrowError($"Error in MineralMission: couldn't find an item prefab (identifier: \"{identifier}\")"); + DebugConsole.ThrowError($"Error in MineralMission: couldn't find an item prefab (identifier: \"{identifier}\")", + contentPackage: Prefab.ContentPackage); continue; } var spawnedResources = level.GenerateMissionResources(prefab, amount, positionType, caves); if (spawnedResources.Count < amount) { - DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{amount} of {prefab.Name}"); + DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{amount} of {prefab.Name}", + contentPackage: Prefab.ContentPackage); } if (spawnedResources.None()) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index b2976762c..be2130aac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -185,7 +185,7 @@ namespace Barotrauma for (int n = 0; n < 2; n++) { - string locationName = $"‖color:gui.orange‖{locations[n].Name}‖end‖"; + string locationName = $"‖color:gui.orange‖{locations[n].DisplayName}‖end‖"; if (description != null) { description = description.Replace("[location" + (n + 1) + "]", locationName); } if (successMessage != null) { successMessage = successMessage.Replace("[location" + (n + 1) + "]", locationName); } if (failureMessage != null) { failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locationName); } @@ -347,7 +347,8 @@ namespace Barotrauma var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier == trigger.EventIdentifier); if (eventPrefab == null) { - DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\")."); + DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\").", + contentPackage: Prefab.ContentPackage); return; } if (GameMain.GameSession?.EventManager != null) @@ -378,7 +379,7 @@ namespace Barotrauma catch (Exception e) { string errorMsg = "Unknown error while giving mission rewards."; - DebugConsole.ThrowError(errorMsg, e); + DebugConsole.ThrowError(errorMsg, e, contentPackage: Prefab.ContentPackage); GameAnalyticsManager.AddErrorEventOnce("Mission.End:GiveReward", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.StackTrace); #if SERVER GameMain.Server?.SendChatMessage(errorMsg + "\n" + e.StackTrace, Networking.ChatMessageType.Error); @@ -430,8 +431,7 @@ namespace Barotrauma IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); // use multipliers here so that we can easily add them together without introducing multiplicative XP stacking - var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f); - crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier)); + var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f, character: null); crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); DistributeExperienceToCrew(crewCharacters, (int)(baseExperienceGain * experienceGainMultiplier.Value)); @@ -547,7 +547,8 @@ namespace Barotrauma { if (element.Attribute("name") != null) { - DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters."); + DebugConsole.ThrowError($"Error in mission \"{Name}\" - use character identifiers instead of names to configure the characters.", + contentPackage: Prefab.ContentPackage); return null; } @@ -556,7 +557,8 @@ namespace Barotrauma HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); if (humanPrefab == null) { - DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\"."); + DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".", + contentPackage: Prefab.ContentPackage); return null; } @@ -587,12 +589,14 @@ namespace Barotrauma ItemPrefab itemPrefab; if (element.Attribute("name") != null) { - DebugConsole.ThrowError($"Error in mission \"{Name}\" - use item identifiers instead of names to configure the items"); + DebugConsole.ThrowError($"Error in mission \"{Name}\" - use item identifiers instead of names to configure the items", + contentPackage: Prefab.ContentPackage); string itemName = element.GetAttributeString("name", ""); itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; if (itemPrefab == null) { - DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemName}\" not found"); + DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemName}\" not found", + contentPackage: Prefab.ContentPackage); } } else @@ -601,7 +605,8 @@ namespace Barotrauma itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; if (itemPrefab == null) { - DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemIdentifier}\" not found"); + DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemIdentifier}\" not found", + contentPackage: Prefab.ContentPackage); } } return itemPrefab; @@ -614,14 +619,16 @@ namespace Barotrauma WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true); if (cargoSpawnPos == null) { - DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": no waypoints marked as Cargo were found"); + DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": no waypoints marked as Cargo were found", + contentPackage: Prefab.ContentPackage); return null; } var cargoRoom = cargoSpawnPos.CurrentHull; if (cargoRoom == null) { - DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": waypoints marked as Cargo must be placed inside a room"); + DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": waypoints marked as Cargo must be placed inside a room", + contentPackage: Prefab.ContentPackage); return null; } @@ -644,16 +651,18 @@ namespace Barotrauma public Mission Mission { get; set; } } - class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission + class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission, IAbilityCharacter { - public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier) + public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier, Character character) { Value = missionExperienceGainMultiplier; Mission = mission; + Character = character; } public float Value { get; set; } public Mission Mission { get; set; } + public Character Character { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index ef4c76774..4df37a741 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -94,8 +94,19 @@ namespace Barotrauma DataRewards = new List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>(); public readonly int Commonness; + /// + /// Displayed difficulty (indicator) + /// public readonly int? Difficulty; public const int MinDifficulty = 1, MaxDifficulty = 4; + /// + /// The actual minimum difficulty of the level allowed for this mission to trigger. + /// + public readonly int MinLevelDifficulty = 0; + /// + /// The actual maximum difficulty of the level allowed for this mission to trigger. + /// + public readonly int MaxLevelDifficulty = 100; public readonly int Reward; @@ -211,6 +222,10 @@ namespace Barotrauma int difficulty = element.GetAttributeInt("difficulty", MinDifficulty); Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } + MinLevelDifficulty = element.GetAttributeInt(nameof(MinLevelDifficulty), MinLevelDifficulty); + MaxLevelDifficulty = element.GetAttributeInt(nameof(MaxLevelDifficulty), MaxLevelDifficulty); + MinLevelDifficulty = Math.Clamp(MinLevelDifficulty, 0, Math.Min(MaxLevelDifficulty, 100)); + MaxLevelDifficulty = Math.Clamp(MaxLevelDifficulty, Math.Max(MinLevelDifficulty, 0), 100); ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false); ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false); @@ -372,7 +387,8 @@ namespace Barotrauma } if (constructor == null) { - DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!"); + DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!", + contentPackage: element.ContentPackage); } InitProjSpecific(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 8b3d03131..4b3e1a2b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -48,7 +48,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: prefab.ContentPackage); } } @@ -78,7 +79,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: prefab.ContentPackage); } } @@ -118,19 +120,19 @@ namespace Barotrauma float minDistBetweenMonsterMissions = 10000; float mindDistFromSub = Level.Loaded.Size.X * 0.3f; var monsterMissions = GameMain.GameSession.Missions.Select(e => e as MonsterMission).Where(m => m != null && m != this && m.spawnPos.HasValue); - if (!Level.Loaded.TryGetInterestingPosition(useSyncedRand: true, spawnPosType, mindDistFromSub, out Vector2 spawnPos, + if (!Level.Loaded.TryGetInterestingPosition(useSyncedRand: true, spawnPosType, mindDistFromSub, out Level.InterestingPosition spawnPos, filter: p => monsterMissions.None(m => Vector2.DistanceSquared(p.Position.ToVector2(), m.spawnPos.Value) < minDistBetweenMonsterMissions * minDistBetweenMonsterMissions), suppressWarning: true)) { Level.Loaded.TryGetInterestingPosition(useSyncedRand: true, spawnPosType, mindDistFromSub, out spawnPos); } - this.spawnPos = spawnPos; + this.spawnPos = spawnPos.Position.ToVector2(); foreach (var (character, amountRange) in monsterPrefabs) { int amount = Rand.Range(amountRange.X, amountRange.Y + 1); for (int i = 0; i < amount; i++) { - monsters.Add(Character.Create(character.Identifier, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false)); + monsters.Add(Character.Create(character.Identifier, this.spawnPos.Value, ToolBox.RandomSeed(8), createNetworkEvent: false)); } } InitializeMonsters(monsters); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 78f840cf9..1c766a7fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -85,7 +85,8 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".", + contentPackage: Prefab.ContentPackage); } } @@ -174,7 +175,8 @@ namespace Barotrauma var itemIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is not ItemPrefab itemPrefab) { - DebugConsole.ThrowError("Couldn't spawn item for nest mission: item prefab \"" + itemIdentifier + "\" not found"); + DebugConsole.ThrowError("Couldn't spawn item for nest mission: item prefab \"" + itemIdentifier + "\" not found", + contentPackage: Prefab.ContentPackage); continue; } @@ -285,7 +287,8 @@ namespace Barotrauma } if (Level.Loaded.IsPositionInsideWall(nestPosition)) { - DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition})."); + DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition}).", + Prefab.ContentPackage); } monsterPrefabs.Clear(); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 80392f0ef..e87e70696 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -11,9 +11,9 @@ namespace Barotrauma { partial class PirateMission : Mission { - private readonly XElement submarineTypeConfig; - private readonly XElement characterConfig; - private readonly XElement characterTypeConfig; + private readonly ContentXElement submarineTypeConfig; + private readonly ContentXElement characterConfig; + private readonly ContentXElement characterTypeConfig; private readonly float addedMissionDifficultyPerPlayer; private float missionDifficulty; @@ -103,7 +103,8 @@ namespace Barotrauma 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}\"."); + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\".", + contentPackage: Prefab.ContentPackage); } } //make sure all defined character types can be found from human prefabs @@ -116,7 +117,8 @@ namespace Barotrauma 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}\"."); + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".", + contentPackage: Prefab.ContentPackage); } } } @@ -151,7 +153,8 @@ namespace Barotrauma ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage); if (submarinePath.IsNullOrEmpty()) { - DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!"); + DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!", + contentPackage: Prefab.ContentPackage); return; } @@ -165,7 +168,8 @@ namespace Barotrauma if (contentFile == null) { - DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!"); + DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!", + contentPackage: Prefab.ContentPackage); return; } @@ -201,16 +205,28 @@ namespace Barotrauma private void CreateMissionPositions(out Vector2 preferredSpawnPos) { - Vector2 patrolPos = enemySub.WorldPosition; + Vector2 patrolPos = Level.Loaded.EndPosition; Point subSize = enemySub.GetDockedBorders().Size; - if (!Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out preferredSpawnPos)) + preferredSpawnPos = Level.Loaded.EndPosition; + + if (Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out var potentialSpawnPos)) { - DebugConsole.ThrowError("Could not spawn pirate submarine in an interesting location! " + this); + preferredSpawnPos = potentialSpawnPos.Position.ToVector2(); } - if (!Level.Loaded.TryGetInterestingPositionAwayFromPoint(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out patrolPos, preferredSpawnPos, minDistFromPoint: 10000f)) + else { - DebugConsole.ThrowError("Could not give pirate submarine an interesting location to patrol to! " + this); + DebugConsole.ThrowError("Could not spawn pirate submarine in an interesting location! " + this, + contentPackage: Prefab.ContentPackage); + } + if (Level.Loaded.TryGetInterestingPositionAwayFromPoint(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out var potentialPatrolPos, preferredSpawnPos, minDistFromPoint: 10000f)) + { + patrolPos = potentialPatrolPos.Position.ToVector2(); + } + else + { + DebugConsole.ThrowError("Could not give pirate submarine an interesting location to patrol to! " + this, + contentPackage: Prefab.ContentPackage); } patrolPos = enemySub.FindSpawnPos(patrolPos, subSize); @@ -266,7 +282,8 @@ namespace Barotrauma if (characterConfig == null) { - DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)"); + DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)", + contentPackage: Prefab.ContentPackage); return; } @@ -281,7 +298,7 @@ namespace Barotrauma Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); bool commanderAssigned = false; - foreach (XElement element in characterConfig.Elements()) + foreach (ContentXElement element in characterConfig.Elements()) { // 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 @@ -293,7 +310,8 @@ namespace Barotrauma if (characterType == null) { - DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\"."); + DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".", + contentPackage: element.ContentPackage); return; } @@ -356,12 +374,12 @@ namespace Barotrauma { DebugConsole.ThrowError(submarineInfo == null ? $"Error in PirateMission: enemy sub was not created (submarineInfo == null)." : - $"Error in PirateMission: enemy sub was not created."); + $"Error in PirateMission: enemy sub was not created.", + contentPackage: Prefab.ContentPackage); return; } - Vector2 spawnPos = Level.Loaded.EndPosition; // in case TryGetInterestingPosition fails, though this should not happen - CreateMissionPositions(out spawnPos); // patrol positions are not explicitly replicated, instead they are acquired the same way the server acquires them + CreateMissionPositions(out Vector2 spawnPos); // patrol positions are not explicitly replicated, instead they are acquired the same way the server acquires them #if DEBUG if (IsClient) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 7c54af4ab..c8184730c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -106,12 +106,14 @@ namespace Barotrauma if (element.GetAttribute("itemname") != null) { - DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); + DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item.", + contentPackage: element.ContentPackage); string itemName = element.GetAttributeString("itemname", ""); ItemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; if (ItemPrefab == null && ExistingItemTag.IsEmpty) { - DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\""); + DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\"", + contentPackage: element.ContentPackage); } } else @@ -128,7 +130,8 @@ namespace Barotrauma } if (ItemPrefab == null && ExistingItemTag.IsEmpty) { - DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\""); + DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\"", + contentPackage: element.ContentPackage); } } @@ -286,7 +289,8 @@ namespace Barotrauma { if (target.ItemPrefab == null && target.ContainerTag.IsEmpty) { - DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag}"); + DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag}", + contentPackage: Prefab.ContentPackage); continue; } target.Item = new Item(target.ItemPrefab, position, null); @@ -379,7 +383,8 @@ namespace Barotrauma if (target.Item == null) { #if DEBUG - DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)"); + DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)", + contentPackage: Prefab.ContentPackage); #endif return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index 364675efb..8ee06a07d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -64,7 +64,8 @@ namespace Barotrauma if (itemConfig == null) { - DebugConsole.ThrowError("Failed to initialize a Scan mission: item config is not set"); + DebugConsole.ThrowError("Failed to initialize a Scan mission: item config is not set", + contentPackage: Prefab.ContentPackage); return; } @@ -77,7 +78,8 @@ namespace Barotrauma TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient); if (TargetRuin == null) { - DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins"); + DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins", + contentPackage: Prefab.ContentPackage); return; } @@ -85,7 +87,8 @@ namespace Barotrauma ruinWaypoints.RemoveAll(wp => wp.CurrentHull == null); if (ruinWaypoints.Count < targetsToScan) { - DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})"); + DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})", + contentPackage: Prefab.ContentPackage); return; } var availableWaypoints = new List(); @@ -107,7 +110,8 @@ namespace Barotrauma if (availableWaypoints.None()) { #if DEBUG - DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})"); + DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})", + contentPackage: Prefab.ContentPackage); #endif break; } @@ -131,7 +135,8 @@ namespace Barotrauma } if (scanTargets.Count < targetsToScan) { - DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})"); + DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})", + contentPackage: Prefab.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 71eebca71..187c8f919 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -10,14 +10,45 @@ namespace Barotrauma { class MonsterEvent : Event { + /// + /// The name of the species to spawn + /// public readonly Identifier SpeciesName; - public readonly int MinAmount, MaxAmount; + + /// + /// Minimum amount of monsters to spawn. You can also use "Amount" if you want to spawn a fixed number of monsters. + /// + public readonly int MinAmount; + /// + /// Maximum amount of monsters to spawn. You can also use "Amount" if you want to spawn a fixed number of monsters. + /// + public readonly int MaxAmount; + private readonly List monsters = new List(); + /// + /// The monsters are spawned at least this distance away from the players and submarines. + /// public readonly float SpawnDistance; + + /// + /// Amount of random variance in the spawn position, in pixels. Can be used to prevent all the monsters from spawning at the exact same position. + /// private readonly float scatter; + + /// + /// Used for offsetting the spawns towards the end position of the level, so that they spawn farther afront the sub than normally. In pixels. + /// private readonly float offset; + + /// + /// Delay between spawning the monsters. Only relevant if the event spawns more than one monster. + /// private readonly float delayBetweenSpawns; + + /// + /// Number seconds before the event resets after all the monsters are dead. Can be used to make the event spawn monsters multiple times. + /// private float resetTime; private float resetTimer; @@ -25,11 +56,22 @@ namespace Barotrauma private bool disallowed; + /// + /// Where should the monster spawn? + /// public readonly Level.PositionType SpawnPosType; + + /// + /// If set, the monsters will spawn at a spawnpoint that has this tag. Only relevant for events that spawn monsters in a submarine, beacon station, wreck, outpost or ruin. + /// private readonly string spawnPointTag; private bool spawnPending, spawnReady; + /// + /// Maximum number of the specific type of monster in the entire level. Can be used to prevent the event from spawning more monsters if there's + /// already enough of that type of monster, e.g. spawned by another event or by a mission. + /// public readonly int MaxAmountPerLevel = int.MaxValue; public IReadOnlyList Monsters => monsters; @@ -82,13 +124,7 @@ namespace Barotrauma MaxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue); - var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); - if (string.IsNullOrWhiteSpace(spawnPosTypeStr) || - !Enum.TryParse(spawnPosTypeStr, true, out SpawnPosType)) - { - SpawnPosType = Level.PositionType.MainPath; - } - + SpawnPosType = prefab.ConfigElement.GetAttributeEnum("spawntype", Level.PositionType.MainPath); //backwards compatibility if (prefab.ConfigElement.GetAttributeBool("spawndeep", false)) { @@ -127,7 +163,8 @@ namespace Barotrauma var file = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ContentFile; if (file == null) { - DebugConsole.ThrowError($"Failed to find config file for species \"{SpeciesName}\""); + DebugConsole.ThrowError($"Failed to find config file for species \"{SpeciesName}\".", + contentPackage: Prefab.ContentPackage); yield break; } else @@ -159,7 +196,8 @@ namespace Barotrauma Character createdCharacter = Character.Create(SpeciesName, Vector2.Zero, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true, throwErrorIfNotFound: false); if (createdCharacter == null) { - DebugConsole.AddWarning($"Error in MonsterEvent: failed to spawn the character \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown"}\"."); + DebugConsole.AddWarning($"Error in MonsterEvent: failed to spawn the character \"{SpeciesName}\". Content package: \"{prefab.ConfigElement?.ContentPackage?.Name ?? "unknown"}\".", + Prefab.ContentPackage); disallowed = true; continue; } @@ -355,29 +393,28 @@ namespace Barotrauma { if (offset > 0) { - Vector2 dir; - var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null && wp.Ruin == null); - var nearestWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value)).FirstOrDefault(); - if (nearestWaypoint != null) + var tunnelType = chosenPosition.PositionType == Level.PositionType.MainPath ? Level.TunnelType.MainPath : Level.TunnelType.SidePath; + var waypoints = WayPoint.WayPointList.FindAll(wp => + wp.Submarine == null && + wp.Ruin == null && + wp.Tunnel?.Type == tunnelType && + wp.WorldPosition.X > spawnPos.Value.X); + + if (waypoints.None()) { - int currentIndex = waypoints.IndexOf(nearestWaypoint); - var nextWaypoint = waypoints[Math.Min(currentIndex + 20, waypoints.Count - 1)]; - dir = Vector2.Normalize(nextWaypoint.WorldPosition - nearestWaypoint.WorldPosition); - // Ensure that the spawn position is not offset to the left. - if (dir.X < 0) - { - dir.X = 0; - } + DebugConsole.AddWarning($"Failed to find a spawn position offset from {spawnPos.Value}.", + Prefab.ContentPackage); } else { - dir = new Vector2(1, Rand.Range(-1f, 1f)); - } - Vector2 targetPos = spawnPos.Value + dir * offset; - var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault(); - if (targetWaypoint != null) - { - spawnPos = targetWaypoint.WorldPosition; + float offsetSqr = offset * offset; + //find the waypoint whose distance from the spawnPos is closest to the desired offset + var targetWaypoint = waypoints.OrderBy(wp => + Math.Abs(Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value) - offsetSqr)).FirstOrDefault(); + if (targetWaypoint != null) + { + spawnPos = targetWaypoint.WorldPosition; + } } } // Ensure that the position is not inside a submarine (in practice wrecks). @@ -636,7 +673,7 @@ namespace Barotrauma monster.AnimController.SetPosition(FarseerPhysics.ConvertUnits.ToSimUnits(pos)); var eventManager = GameMain.GameSession.EventManager; - if (eventManager != null) + if (eventManager != null && monster.Params.AI != null) { if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath)) { @@ -663,7 +700,7 @@ namespace Barotrauma //this will do nothing if the monsters have no swarm behavior defined, //otherwise it'll make the spawned characters act as a swarm SwarmBehavior.CreateSwarm(monsters.Cast()); - DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true); + DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI?.CombatStrength ?? 0))}.", Color.LightBlue, debugOnly: true); } if (GameMain.GameSession != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 85c735b0d..517875dd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -7,7 +7,21 @@ namespace Barotrauma { class ScriptedEvent : Event { - private readonly Dictionary>> targetPredicates = new Dictionary>>(); + public sealed record TargetPredicate( + TargetPredicate.EntityType Type, + Predicate Predicate) + { + public enum EntityType + { + Character, + Hull, + Item, + Structure, + Submarine + } + } + + private readonly Dictionary> targetPredicates = new Dictionary>(); private readonly Dictionary> cachedTargets = new Dictionary>(); @@ -17,7 +31,7 @@ namespace Barotrauma /// private readonly Dictionary initialAmounts = new Dictionary(); - private int prevEntityCount; + private bool newEntitySpawned; private int prevPlayerCount, prevBotCount; private Character prevControlled; @@ -50,7 +64,8 @@ namespace Barotrauma } if (elementId == "statuseffect") { - DebugConsole.ThrowError($"Error in event prefab \"{prefab.Identifier}\". Status effect configured as an action. Please configure status effects as child elements of a StatusEffectAction."); + DebugConsole.ThrowError($"Error in event prefab \"{prefab.Identifier}\". Status effect configured as an action. Please configure status effects as child elements of a StatusEffectAction.", + contentPackage: prefab.ContentPackage); continue; } var action = EventAction.Instantiate(this, element); @@ -59,7 +74,8 @@ namespace Barotrauma if (!Actions.Any()) { - DebugConsole.ThrowError($"Scripted event \"{prefab.Identifier}\" has no actions. The event will do nothing."); + DebugConsole.ThrowError($"Scripted event \"{prefab.Identifier}\" has no actions. The event will do nothing.", + contentPackage: prefab.ContentPackage); } requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null); @@ -69,8 +85,9 @@ namespace Barotrauma foreach (var gotoAction in allActions.OfType()) { if (allActions.None(a => a is Label label && label.Name == gotoAction.Name)) - { - DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Could not find a label matching the GoTo \"{gotoAction.Name}\"."); + { + DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Could not find a label matching the GoTo \"{gotoAction.Name}\".", + contentPackage: prefab.ContentPackage); } } @@ -108,7 +125,8 @@ namespace Barotrauma if (target is Character character) { return character.Name; } if (target is Hull hull) { return hull.DisplayName.Value; } if (target is Submarine sub) { return sub.Info.DisplayName.Value; } - DebugConsole.AddWarning($"Failed to get the name of the event target {target} as a replacement for the tag {tag} in an event text."); + DebugConsole.AddWarning($"Failed to get the name of the event target {target} as a replacement for the tag {tag} in an event text.", + prefab.ContentPackage); return target.ToString(); } else @@ -187,13 +205,13 @@ namespace Barotrauma } } - public void AddTargetPredicate(Identifier tag, Predicate predicate) + public void AddTargetPredicate(Identifier tag, TargetPredicate.EntityType entityType, Predicate predicate) { if (!targetPredicates.ContainsKey(tag)) { - targetPredicates.Add(tag, new List>()); + targetPredicates.Add(tag, new List()); } - targetPredicates[tag].Add(predicate); + targetPredicates[tag].Add(new TargetPredicate(entityType, predicate)); // force re-search for this tag if (cachedTargets.ContainsKey(tag)) { @@ -225,7 +243,6 @@ namespace Barotrauma } List targetsToReturn = new List(); - if (Targets.ContainsKey(tag)) { foreach (Entity e in Targets[tag]) @@ -236,11 +253,24 @@ namespace Barotrauma } if (targetPredicates.ContainsKey(tag)) { - foreach (Entity entity in Entity.GetEntities()) + foreach (var targetPredicate in targetPredicates[tag]) { - if (targetPredicates[tag].Any(p => p(entity)) && !targetsToReturn.Contains(entity)) + IEnumerable entityList = targetPredicate.Type switch { - targetsToReturn.Add(entity); + TargetPredicate.EntityType.Character => Character.CharacterList, + TargetPredicate.EntityType.Item => Item.ItemList, + TargetPredicate.EntityType.Structure => MapEntity.MapEntityList.Where(m => m is Structure), + TargetPredicate.EntityType.Hull => Hull.HullList, + TargetPredicate.EntityType.Submarine => Submarine.Loaded, + _ => Entity.GetEntities(), + }; + foreach (Entity entity in entityList) + { + if (targetsToReturn.Contains(entity)) { continue; } + if (targetPredicate.Predicate(entity)) + { + targetsToReturn.Add(entity); + } } } } @@ -289,14 +319,8 @@ namespace Barotrauma { int botCount = 0; int playerCount = 0; - bool forceRefreshTargets = false; foreach (Character c in Character.CharacterList) { - if (c.Removed) - { - forceRefreshTargets = true; - continue; - } if (c.IsPlayer) { playerCount++; @@ -306,10 +330,11 @@ namespace Barotrauma botCount++; } } - if (forceRefreshTargets || Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled) + + if (botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled || NeedsToRefreshCachedTargets()) { cachedTargets.Clear(); - prevEntityCount = Entity.EntityCount; + newEntitySpawned = false; prevBotCount = botCount; prevPlayerCount = playerCount; prevControlled = Character.Controlled; @@ -349,7 +374,8 @@ namespace Barotrauma } if (CurrentActionIndex == -1) { - DebugConsole.AddWarning($"Could not find the GoTo label \"{goTo}\" in the event \"{Prefab.Identifier}\". Ending the event."); + DebugConsole.AddWarning($"Could not find the GoTo label \"{goTo}\" in the event \"{Prefab.Identifier}\". Ending the event.", + prefab.ContentPackage); } } @@ -364,6 +390,47 @@ namespace Barotrauma } } + private bool NeedsToRefreshCachedTargets() + { + if (newEntitySpawned) { return true; } + foreach (var cachedTargetList in cachedTargets.Values) + { + foreach (var target in cachedTargetList) + { + //one of the previously cached entities has been removed -> force refresh + if (target.Removed) + { + return true; + } + } + } + return false; + } + + public void EntitySpawned(Entity entity) + { + if (newEntitySpawned) { return; } + if (entity is Character character && + Level.Loaded?.StartOutpost != null && + Level.Loaded.StartOutpost.Info.OutpostNPCs.Values.Any(npcList => npcList.Contains(character))) + { + newEntitySpawned = true; + return; + } + //new entity matches one of the existing predicates -> force refresh + foreach (var targetPredicateList in targetPredicates.Values) + { + foreach (var targetPredicate in targetPredicateList) + { + if (targetPredicate.Predicate(entity)) + { + newEntitySpawned = true; + return; + } + } + } + } + public override bool LevelMeetsRequirements() { if (requiredDestinationTypes == null) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index abbb65f70..6453c4d52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -294,6 +294,11 @@ namespace Barotrauma.Extensions .Where(nullable => nullable.HasValue) .Select(nullable => nullable.Value); + public static IEnumerable NotNull(this IEnumerable source) where T : class + => source + .Where(nullable => nullable != null) + .Select(nullable => nullable!); + public static IEnumerable NotNone(this IEnumerable> source) { foreach (var o in source) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index f7273d22c..9f6cbf6dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -26,6 +26,13 @@ namespace Barotrauma public readonly int BuyerCharacterInfoIdentifier; + /// + /// Should the items be given to the buyer immediately, as opposed to spawning them in the sub the next round? + /// + public bool DeliverImmediately { get; set; } + + public bool Delivered; + public PurchasedItem(ItemPrefab itemPrefab, int quantity, int buyerCharacterInfoId) { ItemPrefabIdentifier = itemPrefab.Identifier; @@ -216,9 +223,11 @@ namespace Barotrauma public List GetPurchasedItems(Location.StoreInfo store, bool create = false) => GetPurchasedItems(store?.Identifier ?? Identifier.Empty, create); - public PurchasedItem GetPurchasedItem(Identifier identifier, ItemPrefab prefab) => GetPurchasedItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab); + public int GetPurchasedItemCount(Location.StoreInfo store, ItemPrefab prefab) => + GetPurchasedItemCount(store?.Identifier ?? Identifier.Empty, prefab); - public PurchasedItem GetPurchasedItem(Location.StoreInfo store, ItemPrefab prefab) => GetPurchasedItem(store?.Identifier ?? Identifier.Empty, prefab); + public int GetPurchasedItemCount(Identifier identifier, ItemPrefab prefab) => + GetPurchasedItems(identifier)?.Where(i => i.ItemPrefab == prefab).Sum(it => it.Quantity) ?? 0; public List GetSoldItems(Identifier identifier, bool create = false) => GetItems(identifier, SoldItems, create); @@ -287,23 +296,6 @@ namespace Barotrauma OnItemsInSellFromSubCrateChanged?.Invoke(this); } -#if SERVER - public void OnNewItemsPurchased(Identifier storeIdentifier, List newItems, Client client) - { - StringBuilder sb = new StringBuilder(); - int price = 0; - Dictionary buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, newItems.Select(i => i.ItemPrefab)); - foreach (PurchasedItem item in newItems) - { - int itemValue = item.Quantity * buyValues[item.ItemPrefab]; - GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); - sb.Append($"\n - {item.ItemPrefab.Name} x{item.Quantity}"); - price += itemValue; - } - GameServer.Log($"{NetworkMember.ClientLogName(client, client?.Name ?? "Unknown")} purchased {newItems.Count} item(s) for {TextManager.FormatCurrency(price)}{sb.ToString()}", ServerLog.MessageType.Money); - } -#endif - public void PurchaseItems(Identifier storeIdentifier, List itemsToPurchase, bool removeFromCrate, Client client = null) { var store = Location.GetStore(storeIdentifier); @@ -314,27 +306,58 @@ namespace Barotrauma var itemsInStoreCrate = GetBuyCrateItems(storeIdentifier, create: true); foreach (PurchasedItem item in itemsToPurchase) { + if (item.Quantity <= 0) { continue; } // Exchange money int itemValue = item.Quantity * buyValues[item.ItemPrefab]; if (!campaign.TryPurchase(client, itemValue)) { continue; } // Add to the purchased items - var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab); + var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab && pi.DeliverImmediately == item.DeliverImmediately); if (purchasedItem != null) { purchasedItem.Quantity += item.Quantity; } else { - purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client); + purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client) { DeliverImmediately = item.DeliverImmediately }; itemsPurchasedFromStore.Add(purchasedItem); } + purchasedItem.Delivered = item.DeliverImmediately; if (GameMain.IsSingleplayer) { GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); } store.Balance += itemValue; - if (removeFromCrate) + } + if (GameMain.NetworkMember is not { IsClient: true }) + { + Character targetCharacter; +#if CLIENT + targetCharacter = Character.Controlled; + if (targetCharacter == null) + { + DebugConsole.ThrowError("Failed to deliver items directly to a character (not controlling a character)."); + } +#else + targetCharacter = client?.Character; + if (targetCharacter == null) + { + DebugConsole.ThrowError($"Failed to deliver items directly to a character ({(client == null ? "client was null" : $"client {client.Name} is not controlling a character")})."); + } +#endif + if (targetCharacter == null) + { + DeliverItemsToSub(itemsToPurchase.Where(it => it.DeliverImmediately), Submarine.MainSub, this); + } + else + { + DeliverItemsToCharacter(itemsToPurchase.Where(it => it.DeliverImmediately), targetCharacter, this); + } + + } + if (removeFromCrate) + { + foreach (PurchasedItem item in itemsToPurchase) { // Remove from the shopping crate if (itemsInStoreCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } crateItem) @@ -387,9 +410,9 @@ namespace Barotrauma var items = new List(); foreach (var storeSpecificItems in PurchasedItems) { - items.AddRange(storeSpecificItems.Value); + items.AddRange(storeSpecificItems.Value.Where(it => !it.DeliverImmediately)); } - CreateItems(items, Submarine.MainSub, this); + DeliverItemsToSub(items, Submarine.MainSub, this); PurchasedItems.Clear(); OnPurchasedItemsChanged?.Invoke(this); } @@ -440,10 +463,22 @@ namespace Barotrauma 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.RootContainer is Item rootContainer && rootContainer.HasTag(Tags.DontSellItems)) { return false; } + if (!AllContainersAllowSellingItems(item)) { return false; } return true; }).Distinct(); + static bool AllContainersAllowSellingItems(Item item) + { + do + { + item = item.Container; + if (item is null) { return true; } + if (item.HasTag(Tags.DontSellItems)) { return false; } + if (item.Components.Any(static c => c.DisallowSellingItemsFromContainer)) { return false; } + } while (item != null); + return true; + } + static bool ItemAndAllContainersInteractable(Item item) { do @@ -501,7 +536,7 @@ namespace Barotrauma => items.Where(it => it.HasTag(Tags.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))) + FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && !it.HasTag(Tags.CargoMissionItem) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull))) .Select(it => it.GetComponent()) .Where(c => c != null); @@ -553,9 +588,9 @@ namespace Barotrauma return itemContainer; } - public static void CreateItems(List itemsToSpawn, Submarine sub, CargoManager cargoManager) + public static void DeliverItemsToSub(IEnumerable itemsToSpawn, Submarine sub, CargoManager cargoManager) { - if (itemsToSpawn.Count == 0) { return; } + if (!itemsToSpawn.Any()) { return; } WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub); if (wp == null) @@ -571,7 +606,7 @@ namespace Barotrauma return; } - if (sub == Submarine.MainSub) + if (sub == Submarine.MainSub && itemsToSpawn.Any(it => !it.Delivered && it.Quantity > 0)) { #if CLIENT new GUIMessageBox("", @@ -597,37 +632,66 @@ namespace Barotrauma List availableContainers = FindReusableCargoContainers(connectedSubs, FindCargoRooms(connectedSubs)).ToList(); foreach (PurchasedItem pi in itemsToSpawn) { + pi.Delivered = true; Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab); - for (int i = 0; i < pi.Quantity; i++) { var item = new Item(pi.ItemPrefab, position, wp.Submarine); var itemContainer = GetOrCreateCargoContainerFor(pi.ItemPrefab, cargoRoom, ref availableContainers); itemContainer?.Inventory.TryPutItem(item, null); - var idCard = item.GetComponent(); - if (cargoManager != null && idCard != null && pi.BuyerCharacterInfoIdentifier != 0) - { - cargoManager.purchasedIDCards.Add((pi, idCard)); - } - itemSpawned(pi, item); + ItemSpawned(pi, item, cargoManager); #if SERVER Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif - (itemContainer?.Item ?? item).AssignCampaignInteractionType(CampaignMode.InteractionType.Cargo); - static void itemSpawned(PurchasedItem purchased, Item item) - { - Submarine sub = item.Submarine ?? item.RootContainer?.Submarine; - if (sub != null) - { - foreach (WifiComponent wifiComponent in item.GetComponents()) - { - wifiComponent.TeamID = sub.TeamID; - } - } - } + (itemContainer?.Item ?? item).AssignCampaignInteractionType(CampaignMode.InteractionType.Cargo); + } + } + } + + public static void DeliverItemsToCharacter(IEnumerable itemsToSpawn, Character character, CargoManager cargoManager) + { + if (!itemsToSpawn.Any()) { return; } + + foreach (PurchasedItem pi in itemsToSpawn) + { + pi.Delivered = true; + for (int i = 0; i < pi.Quantity; i++) + { + var item = new Item(pi.ItemPrefab, character.Position, character.Submarine); +#if SERVER + Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); +#endif + if (!character.Inventory.TryPutItem(item, user: null, item.AllowedSlots)) + { + foreach (Item containedItem in character.Inventory.AllItemsMod) + { + if (containedItem.OwnInventory != null && + containedItem.OwnInventory.TryPutItem(item, user: null, item.AllowedSlots)) + { + break; + } + } + } + ItemSpawned(pi, item, cargoManager); + } + } + } + private static void ItemSpawned(PurchasedItem purchased, Item item, CargoManager cargoManager) + { + var idCard = item.GetComponent(); + if (cargoManager != null && idCard != null && purchased.BuyerCharacterInfoIdentifier != 0) + { + cargoManager.purchasedIDCards.Add((purchased, idCard)); + } + + Submarine sub = item.Submarine ?? item.RootContainer?.Submarine; + if (sub != null) + { + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = sub.TeamID; } } - itemsToSpawn.Clear(); } private readonly List<(PurchasedItem purchaseInfo, IdCard idCard)> purchasedIDCards = new List<(PurchasedItem purchaseInfo, IdCard idCard)>(); @@ -685,6 +749,7 @@ namespace Barotrauma new XAttribute("id", item.ItemPrefab.Identifier), new XAttribute("qty", item.Quantity), new XAttribute("storeid", storeSpecificItems.Key), + new XAttribute("deliverimmediately", item.DeliverImmediately), new XAttribute("buyer", item.BuyerCharacterInfoIdentifier))); } } @@ -700,17 +765,22 @@ namespace Barotrauma { string prefabId = itemElement.GetAttributeString("id", null); if (string.IsNullOrWhiteSpace(prefabId)) { continue; } - var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == prefabId); - if (prefab == null) { continue; } + if (!ItemPrefab.Prefabs.TryGet(prefabId.ToIdentifier(), out var prefab)) { continue; } int qty = itemElement.GetAttributeInt("qty", 0); Identifier storeId = itemElement.GetAttributeIdentifier("storeid", "merchant"); + bool deliverImmediately = itemElement.GetAttributeBool("deliverimmediately", false); int buyerId = itemElement.GetAttributeInt("buyer", 0); if (!purchasedItems.TryGetValue(storeId, out var storeItems)) { storeItems = new List(); purchasedItems.Add(storeId, storeItems); } - storeItems.Add(new PurchasedItem(prefab, qty, buyerId)); + storeItems.Add(new PurchasedItem(prefab, qty, buyerId) + { + DeliverImmediately = deliverImmediately, + //must have already been delivered if we had opted for immediate delivery + Delivered = deliverImmediately + }); } } SetPurchasedItems(purchasedItems); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index c2f3a80d4..762807d0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -120,7 +120,7 @@ namespace Barotrauma foreach (var characterElement in element.Elements()) { if (!characterElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } - CharacterInfo characterInfo = new CharacterInfo(characterElement); + CharacterInfo characterInfo = new CharacterInfo(new ContentXElement(contentPackage: null, characterElement)); #if CLIENT if (characterElement.GetAttributeBool("lastcontrolled", false)) { characterInfo.LastControlled = true; } characterInfo.CrewListIndex = characterElement.GetAttributeInt("crewlistindex", -1); @@ -262,7 +262,7 @@ namespace Barotrauma while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count) { spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]); - } + } } if (spawnWaypoints == null || !spawnWaypoints.Any()) { @@ -306,15 +306,16 @@ namespace Barotrauma } character.LoadTalents(); - character.GiveIdCardTags(new List() { mainSubWaypoints[i], spawnWaypoints[i] }); - character.Info.StartItemsGiven = true; + character.GiveIdCardTags(mainSubWaypoints[i]); + character.GiveIdCardTags(spawnWaypoints[i]); + character.Info.StartItemsGiven = true; if (character.Info.OrderData != null) { character.Info.ApplyOrderData(); } } - + AddCharacter(character, sortCrewList: false); #if CLIENT if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index 004c007bc..b8c680577 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -45,7 +45,14 @@ namespace Barotrauma } else { - data.Add(identifier, Convert.ChangeType(value, type, NumberFormatInfo.InvariantInfo)); + try + { + data.Add(identifier, Convert.ChangeType(value, type, NumberFormatInfo.InvariantInfo)); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to change the type of the value \"{value}\" to {type}.", e); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 1ff492a18..a24303b83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -57,7 +57,7 @@ namespace Barotrauma if (increase != 0 && Character.Controlled != null) { Character.Controlled.AddMessage( - TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name).Value, + TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.DisplayName ?? Faction.Prefab.Name).Value, increase > 0 ? GUIStyle.Green : GUIStyle.Red, playSound: true, Identifier, increase, lifetime: 5.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 2722ee955..e12456b62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -382,22 +382,28 @@ namespace Barotrauma currentLocation.DeselectMission(mission); } } - if (levelData.HasBeaconStation && !levelData.IsBeaconActive) + if (levelData.HasBeaconStation && !levelData.IsBeaconActive && Missions.None(m => m.Prefab.Type == MissionType.Beacon)) { - var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("beaconnoreward")).OrderBy(m => m.UintIdentifier); + var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.IsSideObjective && m.Type == MissionType.Beacon); if (beaconMissionPrefabs.Any()) { - Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, p => (float)p.Commonness, rand); - if (!Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type)) + var filteredMissions = beaconMissionPrefabs.Where(m => levelData.Difficulty >= m.MinLevelDifficulty && levelData.Difficulty <= m.MaxLevelDifficulty); + if (filteredMissions.None()) { - extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); + DebugConsole.AddWarning($"No suitable beacon mission found matching the level difficulty {levelData.Difficulty}. Ignoring the restriction."); } + else + { + beaconMissionPrefabs = filteredMissions; + } + Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); + var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, p => p.Commonness, rand); + extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); } } if (levelData.HasHuntingGrounds) { - var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("huntinggrounds")).OrderBy(m => m.UintIdentifier); + var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.IsSideObjective && 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."); @@ -552,8 +558,8 @@ namespace Barotrauma if (availableTransition == TransitionType.None) { DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " + - "(current location: " + (map.CurrentLocation?.Name ?? "null") + ", " + - "selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " + + "(current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " + + "selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " + "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " + "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" + @@ -564,8 +570,8 @@ namespace Barotrauma { DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " + "(transition type: " + availableTransition + ", " + - "current location: " + (map.CurrentLocation?.Name ?? "null") + ", " + - "selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " + + "current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " + + "selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " + "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " + "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" + @@ -576,8 +582,8 @@ namespace Barotrauma ShowCampaignUI = ForceMapUI = false; #endif DebugConsole.NewMessage("Transitioning to " + (nextLevel?.Seed ?? "null") + - " (current location: " + (map.CurrentLocation?.Name ?? "null") + ", " + - "selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " + + " (current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " + + "selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " + "leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " + "at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " + "at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ", " + @@ -862,6 +868,16 @@ namespace Barotrauma } } + //remove ID cards left in duffel bags + foreach (var item in Item.ItemList.ToList()) + { + if (item.HasTag(Tags.IdCardTag) && + (item.Container?.HasTag(Tags.DespawnContainer) ?? false)) + { + item.Remove(); + } + } + foreach (CharacterInfo ci in CrewManager.CharacterInfos.ToList()) { if (ci.CauseOfDeath != null) @@ -1008,7 +1024,7 @@ namespace Barotrauma return ToolBox.SelectWeightedRandom(factionsList, weights, random); } - public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null) + public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Character hirer, Client client = null) { if (characterInfo == null) { return false; } if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) @@ -1018,7 +1034,8 @@ namespace Barotrauma return false; } } - if (!TryPurchase(client, characterInfo.Salary)) { return false; } + + if (!TryPurchase(client, HireManager.GetSalaryFor(characterInfo))) { return false; } characterInfo.IsNewHire = true; characterInfo.Title = null; location.RemoveHireableCharacter(characterInfo); @@ -1247,7 +1264,7 @@ namespace Barotrauma { DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White); DebugConsole.NewMessage(" Money: " + Bank.Balance, Color.White); - DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.Name, Color.White); + DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.DisplayName, Color.White); DebugConsole.NewMessage(" Available destinations: ", Color.White); for (int i = 0; i < map.CurrentLocation.Connections.Count; i++) @@ -1255,11 +1272,11 @@ namespace Barotrauma Location destination = map.CurrentLocation.Connections[i].OtherLocation(map.CurrentLocation); if (destination == map.SelectedLocation) { - DebugConsole.NewMessage(" " + i + ". " + destination.Name + " [SELECTED]", Color.White); + DebugConsole.NewMessage(" " + i + ". " + destination.DisplayName + " [SELECTED]", Color.White); } else { - DebugConsole.NewMessage(" " + i + ". " + destination.Name, Color.White); + DebugConsole.NewMessage(" " + i + ". " + destination.DisplayName, Color.White); } } @@ -1291,7 +1308,7 @@ namespace Barotrauma { if (NumberOfMissionsAtLocation(location) > Settings.TotalMaxMissionCount) { - DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.Name}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions."); + DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.DisplayName}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions."); foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.TotalMaxMissionCount).ToList()) { currentLocation.DeselectMission(mission); @@ -1344,7 +1361,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); + var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); // Remove items from the old sub foreach (Item item in Item.ItemList) { @@ -1405,8 +1422,8 @@ namespace Barotrauma return; } // First move the cargo containers, so that we can reuse them - var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag(Tags.Crate)); - foreach (var (item, oldContainer) in cargoContainers) + var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag(Tags.Crate)).ToHashSet(); + foreach (var (item, _) in cargoContainers) { Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); @@ -1414,7 +1431,6 @@ namespace Barotrauma 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) { @@ -1444,7 +1460,7 @@ namespace Barotrauma newContainerName = cargoContainer.Item.Prefab.Identifier.ToString(); } } - string msg = "Item transfer log error."; + string msg; if (oldContainer != null) { if (newContainer == null && oldContainer == item.Container) @@ -1466,6 +1482,27 @@ namespace Barotrauma DebugConsole.Log(msg); #endif } + + foreach (var (item, _) in itemsToTransfer) + { + // This ensures that the new submarine takes ownership of + // the items contained within the items that are being transferred directly, + // i.e. circuit box components and wires + PropagateSubmarineProperty(item); + } + + static void PropagateSubmarineProperty(Item item) + { + foreach (var ownedContainer in item.GetComponents()) + { + foreach (var containedItem in ownedContainer.Inventory.AllItems) + { + containedItem.Submarine = item.Submarine; + PropagateSubmarineProperty(containedItem); + } + } + } + newSub.Info.NoItems = false; // Serialize the new sub PendingSubmarineSwitch = new SubmarineInfo(newSub); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 31aa8aa9e..a9f757191 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -83,6 +83,12 @@ namespace Barotrauma } } + [Serialize(0.2f, IsPropertySaveable.Yes, description: "How likely it is for security to inspect player characters for stolen items when your reputation is high?")] + public float MinStolenItemInspectionProbability { get; set; } + + [Serialize(0.9f, IsPropertySaveable.Yes, description: "How likely it is for security to inspect player characters for stolen items when your reputation is low?")] + public float MaxStolenItemInspectionProbability { get; set; } + public const int DefaultMaxMissionCount = 2; public const int MaxMissionCountLimit = 10; public const int MinMissionCountLimit = 1; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index a6c56b8f6..a3bce5575 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -319,6 +319,7 @@ namespace Barotrauma foreach (var item in storeItems.Value) { msg.WriteIdentifier(item.ItemPrefabIdentifier); + msg.WriteBoolean(item.DeliverImmediately); msg.WriteRangedInteger(item.Quantity, 0, CargoManager.MaxQuantity); } } @@ -336,8 +337,12 @@ namespace Barotrauma for (int j = 0; j < itemCount; j++) { Identifier itemId = msg.ReadIdentifier(); + bool deliverImmediately = msg.ReadBoolean(); +#if SERVER + if (!AllowImmediateItemDelivery(sender)) { deliverImmediately = false; } +#endif int quantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - items[storeId].Add(new PurchasedItem(itemId, quantity, sender)); + items[storeId].Add(new PurchasedItem(itemId, quantity, sender) { DeliverImmediately = deliverImmediately }); } } return items; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 5a52f982e..38a259fd5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -161,16 +161,18 @@ namespace Barotrauma { switch (subElement.Name.ToString().ToLowerInvariant()) { -#if CLIENT case "gamemode": //legacy support case "singleplayercampaign": +#if CLIENT CrewManager = new CrewManager(true); var campaign = SinglePlayerCampaign.Load(subElement); campaign.LoadNewLevel(); GameMode = campaign; InitOwnedSubs(submarineInfo, ownedSubmarines); - break; +#else + throw new Exception("The server cannot load a single player campaign."); #endif + break; case "multiplayercampaign": CrewManager = new CrewManager(false); var mpCampaign = MultiPlayerCampaign.LoadNew(subElement); @@ -567,7 +569,7 @@ namespace Barotrauma if (EndLocation != null && levelData != null) { 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); + GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.DisplayName), Color.CadetBlue, playSound: false); var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage); if (missionsToShow.Count() > 1) { @@ -582,7 +584,7 @@ namespace Barotrauma } else { - GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.Name), Color.CadetBlue, playSound: false); + GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.DisplayName), Color.CadetBlue, playSound: false); } } @@ -871,6 +873,7 @@ namespace Barotrauma //Clear the grids to allow for garbage collection Powered.Grids.Clear(); + Powered.ChangedConnections.Clear(); try { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs index 9e76c344f..5e2ecdd71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; namespace Barotrauma @@ -20,6 +21,23 @@ namespace Barotrauma AvailableCharacters.Remove(character); } + public static int GetSalaryFor(IReadOnlyCollection hires) + { + return hires.Sum(hire => GetSalaryFor(hire)); + } + + public static int GetSalaryFor(CharacterInfo hire) + { + IEnumerable crew = GameSession.GetSessionCrewCharacters(CharacterType.Both); + float multiplier = 0; + foreach (var character in crew) + { + multiplier += character?.Info?.GetSavedStatValueWithAll(StatTypes.HireCostMultiplier, hire.Job.Prefab.Identifier) ?? 0; + } + float finalMultiplier = 1f + MathF.Max(multiplier, -1f); + return (int)(hire.Salary * finalMultiplier); + } + public void GenerateCharacters(Location location, int amount) { AvailableCharacters.ForEach(c => c.Remove()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 5c416862d..fd7a6fcc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -177,7 +177,7 @@ namespace Barotrauma return; } - int price = prefab.Price.GetBuyPrice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(prefab, GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation); int currentLevel = GetUpgradeLevel(prefab, category); int newLevel = currentLevel + 1; @@ -198,20 +198,23 @@ namespace Barotrauma return result; } - switch (GameMain.NetworkMember) + if (!force) { - case null when Character.Controlled is { } controlled: // singleplayer - if (!TryTakeResources(controlled)) { return; } - break; - case { IsClient: true }: - if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return; } - break; - case { IsServer: true } when client?.Character is { } character: - if (!TryTakeResources(character)) { return; } - break; - default: - DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" without a player."); - return; + switch (GameMain.NetworkMember) + { + case null when Character.Controlled is { } controlled: // singleplayer + if (!TryTakeResources(controlled)) { return; } + break; + case { IsClient: true }: + if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return; } + break; + case { IsServer: true } when client?.Character is { } character: + if (!TryTakeResources(character)) { return; } + break; + default: + DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" without a player."); + return; + } } if (price < 0) @@ -683,7 +686,8 @@ namespace Barotrauma { // automatically fix this if it ever happens? DebugConsole.AddWarning($"The upgrade {newUpgrade.Prefab.Name} in {target.Name} has a different level compared to other items! \n" + - $"Expected level was ${newLevel} but got {newUpgrade.Level} instead."); + $"Expected level was ${newLevel} but got {newUpgrade.Level} instead.", + newUpgrade.Prefab.ContentPackage); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index c6522899c..a8d47043e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -48,13 +48,13 @@ namespace Barotrauma private set; } - private static string[] ParseSlotTypes(XElement element) + private static string[] ParseSlotTypes(ContentXElement element) { string slotString = element.GetAttributeString("slots", null); return slotString == null ? Array.Empty() : slotString.Split(','); } - public CharacterInventory(XElement element, Character character, bool spawnInitialItems) + public CharacterInventory(ContentXElement element, Character character, bool spawnInitialItems) : base(character, ParseSlotTypes(element).Length) { this.character = character; @@ -73,7 +73,8 @@ namespace Barotrauma slotTypeNames[i] = slotTypeNames[i].Trim(); if (!Enum.TryParse(slotTypeNames[i], out parsedSlotType)) { - DebugConsole.ThrowError("Error in the inventory config of \"" + character.SpeciesName + "\" - " + slotTypeNames[i] + " is not a valid inventory slot type."); + DebugConsole.ThrowError("Error in the inventory config of \"" + character.SpeciesName + "\" - " + slotTypeNames[i] + " is not a valid inventory slot type.", + contentPackage: element.ContentPackage); } SlotTypes[i] = parsedSlotType; switch (SlotTypes[i]) @@ -99,7 +100,8 @@ namespace Barotrauma DebugConsole.ThrowError( $"Character \"{character.SpeciesName}\" is configured to spawn with so many items it will have less than 2 free inventory slots. " + "This can cause issues with talents that spawn extra loot in monsters' inventories." - + " Consider increasing the inventory size."); + + " Consider increasing the inventory size.", + contentPackage: element.ContentPackage); } #endif @@ -115,7 +117,8 @@ namespace Barotrauma string itemIdentifier = subElement.GetAttributeString("identifier", ""); if (!ItemPrefab.Prefabs.TryGet(itemIdentifier, out var itemPrefab)) { - DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found."); + DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found.", + contentPackage: element.ContentPackage); continue; } @@ -131,7 +134,7 @@ namespace Barotrauma 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); + DebugConsole.ThrowError(errorMsg, contentPackage: element.ContentPackage); GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); } else if (!character.Enabled) @@ -148,6 +151,19 @@ namespace Barotrauma partial void InitProjSpecific(XElement element); + public Item FindEquippedItemByTag(Identifier tag) + { + if (tag.IsEmpty) { return null; } + for (int i = 0; i < slots.Length; i++) + { + if (SlotTypes[i] == InvSlotType.Any) { continue; } + + var item = slots[i].FirstOrDefault(); + if (item != null && item.HasTag(tag)) { return item; } + } + return null; + } + public int FindLimbSlot(InvSlotType limbSlot) { for (int i = 0; i < slots.Length; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 41ff58290..041465193 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -184,6 +184,8 @@ namespace Barotrauma.Items.Components public bool IsFullyClosed => IsClosed && OpenState <= 0f; + public bool HasWindow => Window != Rectangle.Empty; + [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; } @@ -341,7 +343,12 @@ namespace Barotrauma.Items.Components OnFailedToOpen(); return; } - toggleCooldownTimer = ToggleCoolDown; + if (ToggleWhenClicked) + { + //do not activate cooldown at this point if the door doesn't get toggled when clicked + //(i.e. if it just sends out a signal that might get passed back to the door and try to toggle it) + toggleCooldownTimer = ToggleCoolDown; + } if (IsStuck || IsJammed) { #if CLIENT @@ -381,6 +388,31 @@ namespace Barotrauma.Items.Components return false; } + /// + /// Is the given position inside the vertical bounds of the window, and roughly on the door horizontally? Or the other way around if the door opens horizontally. + /// + /// Position in the same coordinate space as the door. + /// Maximum horizontal distance from the door (or vertical if the door opens horizontally) + public bool IsPositionOnWindow(Vector2 position, float maxPerpendicularDistance = 10.0f) + { + if (IsHorizontal) + { + return + position.X >= item.Rect.X + Window.X && + position.X <= item.Rect.X + Window.X + Window.Width && + position.Y >= item.Rect.Y - maxPerpendicularDistance && + position.Y <= item.Rect.Y - item.Rect.Height - maxPerpendicularDistance; + } + else + { + return + position.Y >= item.Rect.Y + Window.Y && + position.Y <= item.Rect.Y + Window.Y + Window.Height && + position.X >= item.Rect.X - maxPerpendicularDistance && + position.X <= item.Rect.Right + maxPerpendicularDistance; + } + } + public override void Update(float deltaTime, Camera cam) { UpdateProjSpecific(deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index a1b8e5738..df2d173c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -226,7 +226,8 @@ namespace Barotrauma.Items.Components foreach ((Character character, Node node) in charactersInRange) { if (character == null || character.Removed) { continue; } - character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor)); + character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor), + impulseDirection: character.WorldPosition - node.WorldPosition); } } DischargeProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index 11b26aee2..2edaabeff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -168,8 +168,8 @@ namespace Barotrauma.Items.Components conditionIncrease += user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f; if (item.Prefab == otherGeneticMaterial.item.Prefab) { + float taintedProbability = GetTaintedProbabilityOnRefine(otherGeneticMaterial, user); item.Condition = Math.Max(item.Condition, otherGeneticMaterial.item.Condition) + conditionIncrease; - float taintedProbability = GetTaintedProbabilityOnRefine(user); if (taintedProbability >= Rand.Range(0.0f, 1.0f)) { MakeTainted(); @@ -221,10 +221,10 @@ namespace Barotrauma.Items.Components return taintedEffectStrength; } - private float GetTaintedProbabilityOnRefine(Character user) + private float GetTaintedProbabilityOnRefine(GeneticMaterial otherGeneticMaterial, Character user) { if (user == null) { return 1.0f; } - float probability = MathHelper.Lerp(0.0f, 0.99f, item.Condition / 100.0f); + float probability = MathHelper.Lerp(0.0f, 0.99f, Math.Max(item.Condition, otherGeneticMaterial.Item.Condition) / 100.0f); probability *= MathHelper.Lerp(1.0f, 0.25f, DegreeOfSuccess(user)); return MathHelper.Clamp(probability, 0.0f, 1.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index f15afb13c..846e92df5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -59,7 +59,8 @@ namespace Barotrauma.Items.Components StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name.Value); if (effect.type != ActionType.OnProduceSpawned) { - DebugConsole.ThrowError("Only OnProduceSpawned type can be used in ."); + DebugConsole.ThrowError("Only OnProduceSpawned type can be used in .", + contentPackage: element.ContentPackage); continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index c482ec981..7d789b672 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -80,9 +80,6 @@ namespace Barotrauma.Items.Components [Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public Vector2 OwnerSheetIndex { get; set; } - [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] - public bool SpawnPointTagsGiven { get; set; } - public IdCard(Item item, ContentXElement element) : base(item, element) { } public void Initialize(WayPoint spawnPoint, Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 05fac61cd..737ae2232 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -439,9 +439,10 @@ namespace Barotrauma.Items.Components if (targetItem.Removed) { return; } var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f); #if CLIENT - if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar) + if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null && + (user == Character.Controlled || Character.Controlled.CanSeeTarget(item))) { - Character.Controlled?.UpdateHUDProgressBar(targetItem, + Character.Controlled.UpdateHUDProgressBar(targetItem, targetItem.WorldPosition, targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 0e3083684..4935c05f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components } #if CLIENT - if (requiredTime < float.MaxValue) + if (requiredTime < float.MaxValue && picker == Character.Controlled) { Character.Controlled?.UpdateHUDProgressBar( this, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 950e8cfae..315f8cc71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -134,7 +134,8 @@ namespace Barotrauma.Items.Components 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."); + DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement.", + item.Prefab.ContentPackage); } InitProjSpecific(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index e5dfda8a4..4f0686cef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -137,7 +137,8 @@ namespace Barotrauma.Items.Components if (element.GetAttribute("limbfixamount") != null) { - DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute."); + DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute.", + contentPackage: element.ContentPackage); } fixableEntities = new HashSet(); @@ -149,7 +150,8 @@ namespace Barotrauma.Items.Components case "fixable": if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities."); + DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities.", + contentPackage: element.ContentPackage); fixableEntities.Add(subElement.GetAttribute("name").Value.ToIdentifier()); } else @@ -536,7 +538,7 @@ namespace Barotrauma.Items.Components { Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * lastPickedFraction * 0.9f); if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; } - new FireSource(displayPos); + new FireSource(displayPos, sourceCharacter: user); } } } @@ -570,8 +572,15 @@ namespace Barotrauma.Items.Components structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureDamageMultiplier); } + var didLeak = targetStructure.SectionIsLeakingFromOutside(sectionIndex); + targetStructure.AddDamage(sectionIndex, -structureFixAmount * degreeOfSuccess, user); + if (didLeak && !targetStructure.SectionIsLeakingFromOutside(sectionIndex)) + { + user.CheckTalents(AbilityEffectType.OnRepairedOutsideLeak); + } + //if the next section is small enough, apply the effect to it as well //(to make it easier to fix a small "left-over" section) for (int i = -1; i < 2; i += 2) @@ -658,9 +667,10 @@ namespace Barotrauma.Items.Components float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); levelResource.DeattachTimer += addedDetachTime; #if CLIENT - if (targetItem.Prefab.ShowHealthBar) + if (targetItem.Prefab.ShowHealthBar && Character.Controlled != null && + (user == Character.Controlled || Character.Controlled.CanSeeTarget(item))) { - Character.Controlled?.UpdateHUDProgressBar( + Character.Controlled.UpdateHUDProgressBar( this, targetItem.WorldPosition, levelResource.DeattachTimer / levelResource.DeattachDuration, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 41d9c5df0..e972c855e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components { if (aimPos == Vector2.Zero) { - aimPos = new Vector2(0.6f, 0.1f); + aimPos = new Vector2(0.45f, 0.1f); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 8930f1603..16d6b788e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -60,12 +60,19 @@ namespace Barotrauma.Items.Components set { if (parent == value) { return; } - if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; } - if (value != null) { value.OnActiveStateChanged += SetActiveState; } + if (InheritParentIsActive) + { + if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; } + if (value != null) { value.OnActiveStateChanged += SetActiveState; } + } parent = value; } } + + [Serialize(true, IsPropertySaveable.No, description: "If this is a child component of another component, should this component inherit the IsActive state of the parent?")] + public bool InheritParentIsActive { get; set; } + public readonly ContentXElement originalElement; protected const float CorrectionDelay = 1.0f; @@ -77,6 +84,12 @@ namespace Barotrauma.Items.Components /// public virtual bool DontTransferInventoryBetweenSubs => false; + /// + /// If enabled, the items inside any of the item containers on this item cannot be sold at an outpost. + /// Use in similar cases as . + /// + public virtual bool DisallowSellingItemsFromContainer => false; + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes to pick up the item (in seconds).")] public float PickingTime { @@ -291,7 +304,8 @@ namespace Barotrauma.Items.Components } catch (Exception e) { - DebugConsole.ThrowError("Invalid select key in " + element + "!", e); + DebugConsole.ThrowError("Invalid select key in " + element + "!", e, + contentPackage: element.ContentPackage); } PickKey = InputType.Select; @@ -304,7 +318,8 @@ namespace Barotrauma.Items.Components } catch (Exception e) { - DebugConsole.ThrowError("Invalid pick key in " + element + "!", e); + DebugConsole.ThrowError("Invalid pick key in " + element + "!", e, + contentPackage: element.ContentPackage); } SerializableProperties = SerializableProperty.DeserializeProperties(this, element); @@ -316,7 +331,8 @@ namespace Barotrauma.Items.Components var component = item.Components.Find(ic => ic.Name.Equals(inheritRequiredSkillsFrom, StringComparison.OrdinalIgnoreCase)); if (component == null) { - DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its required skills from \"{inheritRequiredSkillsFrom}\", but a component of that type couldn't be found."); + DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its required skills from \"{inheritRequiredSkillsFrom}\", but a component of that type couldn't be found.", + contentPackage: element.ContentPackage); } else { @@ -331,7 +347,8 @@ namespace Barotrauma.Items.Components var component = item.Components.Find(ic => ic.Name.Equals(inheritStatusEffectsFrom, StringComparison.OrdinalIgnoreCase)); if (component == null) { - DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its StatusEffects from \"{inheritStatusEffectsFrom}\", but a component of that type couldn't be found."); + DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its StatusEffects from \"{inheritStatusEffectsFrom}\", but a component of that type couldn't be found.", + contentPackage: element.ContentPackage); } else if (component.statusEffectLists != null) { @@ -366,7 +383,8 @@ namespace Barotrauma.Items.Components case "requiredskills": if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill.", + contentPackage: element.ContentPackage); continue; } @@ -383,8 +401,11 @@ namespace Barotrauma.Items.Components if (ic == null) { break; } ic.Parent = this; - ic.IsActive = isActive; - OnActiveStateChanged += ic.SetActiveState; + if (ic.InheritParentIsActive) + { + ic.IsActive = isActive; + OnActiveStateChanged += ic.SetActiveState; + } item.AddComponent(ic); break; @@ -432,7 +453,8 @@ namespace Barotrauma.Items.Components } else if (!allowEmpty) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers.", + contentPackage: element.ContentPackage); } } @@ -974,7 +996,8 @@ namespace Barotrauma.Items.Components { if (errorMessages) { - DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})"); + DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", + contentPackage: element.ContentPackage); } return null; } @@ -983,7 +1006,8 @@ namespace Barotrauma.Items.Components { if (errorMessages) { - DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e); + DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e, + contentPackage: element.ContentPackage); } return null; } @@ -996,14 +1020,16 @@ namespace Barotrauma.Items.Components if (constructor == null) { DebugConsole.ThrowError( - $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})"); + $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", + contentPackage: element.ContentPackage); return null; } } catch (Exception e) { DebugConsole.ThrowError( - $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e); + $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e, + contentPackage: element.ContentPackage); return null; } ItemComponent ic = null; @@ -1016,7 +1042,7 @@ namespace Barotrauma.Items.Components } catch (TargetInvocationException e) { - DebugConsole.ThrowError($"Error while loading component of the type {type}.", e.InnerException); + DebugConsole.ThrowError($"Error while loading component of the type {type}.", e.InnerException, contentPackage: element.ContentPackage); GameAnalyticsManager.AddErrorEventOnce( $"ItemComponent.Load:TargetInvocationException{item.Name}{element.Name}", GameAnalyticsManager.ErrorSeverity.Error, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index db2d49659..8a164684c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -236,6 +236,12 @@ namespace Barotrauma.Items.Components get => Inventory.AllItems.Count(it => it.Condition > 0.0f); } + public int ExtraStackSize + { + get => Inventory.ExtraStackSize; + set => Inventory.ExtraStackSize = value; + } + private readonly ImmutableArray slotRestrictions; readonly List targets = new List(); @@ -284,7 +290,8 @@ namespace Barotrauma.Items.Components RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name); if (containable == null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.", + contentPackage: element.ContentPackage); continue; } ContainableItems ??= new List(); @@ -297,7 +304,10 @@ namespace Barotrauma.Items.Components } } Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow); - + + // we have to assign this here because the fields are serialized before the inventory is created otherwise + ExtraStackSize = element.GetAttributeInt(nameof(ExtraStackSize), 0); + List newSlotRestrictions = new List(totalCapacity); for (int i = 0; i < capacity; i++) { @@ -321,7 +331,8 @@ namespace Barotrauma.Items.Components RelatedItem containable = RelatedItem.Load(subSubElement, returnEmpty: false, parentDebugName: item.Name); if (containable == null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers.", + contentPackage: element.ContentPackage); continue; } subContainableItems.Add(containable); @@ -349,7 +360,8 @@ namespace Barotrauma.Items.Components RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name); if (containable == null) { - DebugConsole.ThrowError("Error when loading containable restrictions for \"" + item.Name + "\" - containable with no identifiers."); + DebugConsole.ThrowError("Error when loading containable restrictions for \"" + item.Name + "\" - containable with no identifiers.", + contentPackage: element.ContentPackage); continue; } ContainableItems[containableIndex] = containable; @@ -386,6 +398,7 @@ namespace Barotrauma.Items.Components public void OnItemContained(Item containedItem) { int index = Inventory.FindIndex(containedItem); + RelatedItem relatedItem = null; if (index >= 0 && index < slotRestrictions.Length) { if (slotRestrictions[index].ContainableItems != null) @@ -394,6 +407,8 @@ namespace Barotrauma.Items.Components foreach (var containableItem in slotRestrictions[index].ContainableItems) { if (!containableItem.MatchesItem(containedItem)) { continue; } + //the 1st matching ContainableItem of the slot determines the hiding, position and rotation of the item + relatedItem ??= containableItem; foreach (StatusEffect effect in containableItem.StatusEffects) { activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition)); @@ -402,7 +417,6 @@ namespace Barotrauma.Items.Components } } - var relatedItem = FindContainableItem(containedItem); var containedItemInfo = new ContainedItem(containedItem, Hide: relatedItem?.Hide ?? false, ItemPos: relatedItem?.ItemPos, @@ -783,12 +797,9 @@ namespace Barotrauma.Items.Components private RelatedItem FindContainableItem(Item item) { - var relatedItem = ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item)); - if (relatedItem == null && AllSubContainableItems != null) - { - relatedItem = AllSubContainableItems.FirstOrDefault(ci => ci.MatchesItem(item)); - } - return relatedItem; + int index = Inventory.FindIndex(item); + if (index == -1 ) { return null; } + return slotRestrictions[index]?.ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item)); } /// @@ -1092,6 +1103,7 @@ namespace Barotrauma.Items.Components itemIds[i].Add(idRemap.GetOffsetId(id)); } } + ExtraStackSize = componentElement.GetAttributeInt(nameof(ExtraStackSize), 0); } public override XElement Save(XElement parentElement) @@ -1104,6 +1116,7 @@ namespace Barotrauma.Items.Components itemIdStrings[i] = string.Join(';', items.Select(it => it.ID.ToString())); } componentElement.Add(new XAttribute("contained", string.Join(',', itemIdStrings))); + componentElement.Add(new XAttribute(nameof(ExtraStackSize), ExtraStackSize)); return componentElement; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index caa19f47c..7b04931f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -622,7 +622,7 @@ namespace Barotrauma.Items.Components return element; } - private void LoadLimbPositions(XElement element) + private void LoadLimbPositions(ContentXElement element) { limbPositions.Clear(); foreach (var subElement in element.Elements()) @@ -631,7 +631,8 @@ namespace Barotrauma.Items.Components string limbStr = subElement.GetAttributeString("limb", ""); if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType)) { - DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type."); + DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type.", + contentPackage: element.ContentPackage); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index d5a5368ee..e48ec1934 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -104,7 +104,7 @@ namespace Barotrauma.Items.Components // doesn't quite work properly, remaining time changes if tinkering stops float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease); - float deconstructionSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed); + float deconstructionSpeed = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed); if (DeconstructItemsSimultaneously) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 0796cd686..311da9b7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -149,13 +149,13 @@ namespace Barotrauma.Items.Components { forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100)); } - currForce *= item.StatManager.GetAdjustedValue(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier; + currForce *= item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier; if (item.GetComponent() is { IsTinkering: true } repairable) { currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease; } - currForce = item.StatManager.GetAdjustedValue(ItemTalentStats.EngineSpeed, currForce); + currForce = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineSpeed, currForce); //less effective when in a bad condition currForce *= MathHelper.Lerp(0.5f, 2.0f, condition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 0ef1b1dc2..63de9cce6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -101,7 +101,8 @@ namespace Barotrauma.Items.Components { if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase)) { - DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator."); + DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator.", + contentPackage: element.ContentPackage); break; } } @@ -119,12 +120,16 @@ namespace Barotrauma.Items.Components } } + //the errors below may be caused by a mod overriding a base item instead of this one, log the package of the base item in that case + var packageToLog = itemPrefab.GetParentModPackageOrThisPackage(); + bool recipeInvalid = false; foreach (var requiredItem in recipe.RequiredItems) { if (requiredItem.ItemPrefabs.None()) { - DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\"."); + DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\".", + contentPackage: packageToLog); recipeInvalid = true; } } @@ -132,7 +137,8 @@ namespace Barotrauma.Items.Components if (fabricationRecipes.TryGetValue(recipe.RecipeHash, out var duplicateRecipe)) { - DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\"."); + DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\".", + contentPackage: packageToLog); continue; } fabricationRecipes.Add(recipe.RecipeHash, recipe); @@ -463,7 +469,7 @@ namespace Barotrauma.Items.Components character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); - quality = GetFabricatedItemQuality(fabricatedItem, user); + quality = GetFabricatedItemQuality(fabricatedItem, user).RollQuality(); } int amount = (int)fabricationitemAmount.Value; @@ -528,12 +534,10 @@ namespace Barotrauma.Items.Components { foreach (Skill skill in fabricatedItem.RequiredSkills) { - float userSkill = user.GetSkillLevel(skill.Identifier); - float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f); + float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill; var addedSkillValue = new AbilityFabricatorSkillGain(skill.Identifier, addedSkill); user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue); - - user.Info.IncreaseSkillLevel( + user.Info.ApplySkillGain( skill.Identifier, addedSkillValue.Value); } @@ -570,10 +574,52 @@ namespace Barotrauma.Items.Components return currPowerConsumption; } - private static int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) + public static float CalculateBonusRollPercentage(float skillLevel, float target) + => Math.Clamp((skillLevel - target) / (100f - target) * 100f, min: 0, max: 100); + + public readonly record struct QualityResult(int Quality, float PlusOnePercentage, float PlusTwoPercentage) { - if (user?.Info == null) { return 0; } - if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; } + public static readonly QualityResult Empty = new QualityResult(0, 0, 0); + + public bool HasRandomQualityRollChance => PlusOnePercentage > 0f || PlusTwoPercentage > 0f; + + // The total real world percentage for a roll to succeed, taking into account that +1 needs to succeed for +2 to be attempted and + // that the chance for only +1 goes down as +2 increases since some of the +1's will turn into +2s + public float TotalPlusOnePercentage => Math.Clamp(PlusOnePercentage * (100f - PlusTwoPercentage) / 100f, min: 0, max: 100); + public float TotalPlusTwoPercentage => Math.Clamp(PlusOnePercentage * PlusTwoPercentage / 100f, min: 0, max: 100); + + public int RollQuality() + { + int additionalQuality = 0; + if (Roll(PlusOnePercentage)) + { + additionalQuality++; + if (Roll(PlusTwoPercentage)) + { + additionalQuality++; + } + } + + return Quality + additionalQuality; + + static bool Roll(float percentage) + => percentage >= Rand.Range(0, 100, Rand.RandSync.Unsynced); + } + } + + public const int PlusOneQualityBonusThreshold = 50, + PlusTwoQualityBonusThreshold = 75; + + public const int PlusOneTarget = 100, + PlusTwoTarget = 125; + + public const float PlusOneLerp = 0.2f, + PlusTwoLerp = 0.4f; + + private static QualityResult GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) + { + if (user?.Info == null) { return QualityResult.Empty; } + if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return QualityResult.Empty; } int quality = 0; float floatQuality = 0.0f; floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false); @@ -587,34 +633,63 @@ namespace Barotrauma.Items.Components } quality = (int)floatQuality; - const int MaxCraftingSkill = 100; + // Use Option here instead of 0 because we want the lowest value and a value of 0 would always be lower than any other chance + Option plusOne = Option.None, + plusTwo = Option.None; - //having a higher-than-100 skill (e.g. due to talents) gives +1 quality - quality += fabricatedItem.RequiredSkills.All(s => user.GetSkillLevel(s.Identifier) >= MaxCraftingSkill) ? 1 : 0; foreach (var skill in fabricatedItem.RequiredSkills) { - //+1 quality if the character's skill level is >20% from the min requirement towards max skill - //e.g. if the skill requirement is 10 -> 28 - //40 -> 52 - //90 -> 92 - float skillRequirement = MathHelper.Lerp(skill.Level, MaxCraftingSkill, 0.2f); - if (user.GetSkillLevel(skill.Identifier) > skillRequirement) + float skillLevel = user.GetSkillLevel(skill.Identifier); + + if (skillLevel >= PlusOneQualityBonusThreshold) { - quality += 1; + //+1 quality chance if the character's skill level is >20% from the min requirement towards max skill as well as higher than 50 + //e.g. if the skill requirement is 10 -> 28 (but minimum 50 threshold) + //40 -> 52 + //90 -> 92 + var bonusChance1 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusOneTarget, PlusOneLerp)); + plusOne = OverrideChanceIfLess(plusOne, bonusChance1); + + if (skillLevel >= PlusTwoQualityBonusThreshold) + { + var bonusChance2 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusTwoTarget, PlusTwoLerp)); + plusTwo = OverrideChanceIfLess(plusTwo, bonusChance2); + } + else + { + break; + } + } + else + { + break; + } + + static Option OverrideChanceIfLess(Option original, float bonusChance) + { + if (original.TryUnwrap(out var originalChance)) + { + return originalChance > bonusChance ? Option.Some(bonusChance) : original; + } + + return Option.Some(bonusChance); } } - return quality; + + return new QualityResult(quality, + PlusOnePercentage: plusOne.Match(some: static f => f, none: static () => 0f), + PlusTwoPercentage: plusTwo.Match(some: static f => f, none: static () => 0f)); } partial void UpdateRequiredTimeProjSpecific(); private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item) { - return + return (user != null && user.HasRecipeForItem(item.Identifier)) || GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier)); } - + private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary> availableIngredients, Character character) { if (fabricableItem == null) { return false; } @@ -692,7 +767,7 @@ namespace Barotrauma.Items.Components //fabricating takes 100 times longer if degree of success is close to 0 //characters with a higher skill than required can fabricate up to 100% faster - float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f); + float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f); if (user?.Info is { } info && fabricableItem.TargetItem is { } it) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index a6c4704cc..6d51be758 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -138,14 +138,14 @@ namespace Barotrauma.Items.Components float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor); - currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValue(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor; + currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor; if (item.GetComponent() is { IsTinkering: true } repairable) { currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease; } - currFlow = item.StatManager.GetAdjustedValue(ItemTalentStats.PumpSpeed, currFlow); + currFlow = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpSpeed, currFlow); //less effective when in a bad condition currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 74aec6ffc..84c5d4d11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -875,7 +875,7 @@ namespace Barotrauma.Items.Components } } - private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput); - private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate); + private float GetMaxOutput() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput); + private float GetFuelConsumption() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index f2c4b506b..9784e92b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -336,8 +336,7 @@ namespace Barotrauma.Items.Components { showIceSpireWarning = false; if (user != null && user.Info != null && - user.SelectedItem == item && - controlledSub != null && controlledSub.Velocity.LengthSquared() > 0.01f) + user.SelectedItem == item) { IncreaseSkillLevel(user, deltaTime); } @@ -402,14 +401,15 @@ namespace Barotrauma.Items.Components private void IncreaseSkillLevel(Character user, float deltaTime) { + if (controlledSub == null) { return; } + if (controlledSub.Velocity.LengthSquared() < 0.01f) { return; } if (user?.Info == null) { return; } // Do not increase the helm skill when "steering" the sub while docked into something static (e.g. outpost or wreck) - if (GameMain.GameSession?.Campaign != null && controlledSub != null && controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; } + if (GameMain.GameSession?.Campaign != null&& controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; } - float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f; - user.Info.IncreaseSkillLevel( - "helm".ToIdentifier(), - SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime); + float speedMultiplier = MathHelper.Clamp(TargetVelocity.Length() / 100.0f, 0.0f, 1.0f); + user.Info.ApplySkillGain(Tags.HelmSkill, + SkillSettings.Current.SkillIncreasePerSecondWhenSteering * speedMultiplier * deltaTime); } private void UpdateAutoPilot(float deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 8881ce000..8e17e7a44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -395,6 +395,6 @@ namespace Barotrauma.Items.Components } } - public float GetCapacity() => item.StatManager.GetAdjustedValue(ItemTalentStats.BatteryCapacity, Capacity); + public float GetCapacity() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.BatteryCapacity, Capacity); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index f023ff870..93d47b5c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -407,7 +407,7 @@ namespace Barotrauma.Items.Components } } - if (!(this is RelayComponent)) + if (this is not RelayComponent) { if (PowerConnections.Any(p => !p.IsOutput) && PowerConnections.Any(p => p.IsOutput)) { @@ -454,6 +454,7 @@ namespace Barotrauma.Items.Components base.RemoveComponentSpecific(); connectedRecipients?.Clear(); connectionDirty?.Clear(); + recipientsToRefresh.Clear(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 2af67598e..9b3444649 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -306,7 +306,8 @@ namespace Barotrauma.Items.Components if (item.body == null) { - DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!"); + DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!", + contentPackage: element.ContentPackage); return; } @@ -1016,9 +1017,10 @@ namespace Barotrauma.Items.Components { attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); #if CLIENT - if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar) + if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null && + (User == Character.Controlled || Character.Controlled.CanSeeTarget(item))) { - Character.Controlled?.UpdateHUDProgressBar(targetItem, + Character.Controlled.UpdateHUDProgressBar(targetItem, targetItem.WorldPosition, targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index a41e1e1c8..03b727bc3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -60,7 +60,8 @@ namespace Barotrauma.Items.Components string statTypeString = subElement.GetAttributeString("stattype", ""); if (!Enum.TryParse(statTypeString, true, out StatType statType)) { - DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + ((MapEntity)item).Prefab.Identifier + ")"); + DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + ((MapEntity)item).Prefab.Identifier + ")", + contentPackage: element.ContentPackage); } float statValue = subElement.GetAttributeFloat("value", 0f); statValues.TryAdd(statType, statValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 93721c36f..873325957 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -495,9 +495,7 @@ namespace Barotrauma.Items.Components { foreach (Skill skill in requiredSkills) { - float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier); - CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier, - SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f)); + CurrentFixer.Info?.ApplySkillGain(skill.Identifier, SkillSettings.Current.SkillIncreasePerRepair); } SteamAchievementManager.OnItemRepaired(item, CurrentFixer); CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item)); @@ -570,7 +568,7 @@ namespace Barotrauma.Items.Components if (item.ConditionPercentage > MinDeteriorationCondition) { - float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); + float deteriorationSpeed = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); if (ForceDeteriorationTimer > 0.0f) { deteriorationSpeed = Math.Max(deteriorationSpeed, 1.0f); } item.Condition -= deteriorationSpeed * deltaTime; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs index 1d592e015..bd84d052f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs @@ -82,7 +82,8 @@ namespace Barotrauma.Items.Components Holdable = item.GetComponent(); if (Holdable == null || !Holdable.Attachable) { - DebugConsole.ThrowError("Error in initializing a Scanner component: an attachable Holdable component is required on the same item and none was found"); + DebugConsole.ThrowError("Error in initializing a Scanner component: an attachable Holdable component is required on the same item and none was found", + contentPackage: item.Prefab.ContentPackage); IsActive = false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs index c7fd99f51..6902b3b5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs @@ -26,7 +26,8 @@ namespace Barotrauma.Items.Components RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null); if (RequiredSignalCount < 1) { - DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!"); + DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!", + contentPackage: element.ContentPackage); } InitProjSpecific(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs index 2cfd5b3da..69c395e3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -29,6 +29,9 @@ namespace Barotrauma.Items.Components // We don't want the components and wires to transfer between subs as it would cause issues. public override bool DontTransferInventoryBetweenSubs => true; + // We don't want to sell the components and wires inside the circuit box + public override bool DisallowSellingItemsFromContainer => true; + public Option FindInputOutputConnection(Identifier connectionName) { foreach (CircuitBoxInputConnection input in Inputs) @@ -154,7 +157,7 @@ namespace Barotrauma.Items.Components delayedElementToLoad = Option.None; } - private void LoadFromXML(ContentXElement loadElement) + public void LoadFromXML(ContentXElement loadElement) { foreach (var subElement in loadElement.Elements()) { @@ -395,8 +398,8 @@ namespace Barotrauma.Items.Components { Components.Add(new CircuitBoxComponent(id, spawnedItem, pos, this, usedResource)); onItemSpawned?.Invoke(spawnedItem); + OnViewUpdateProjSpecific(); }); - OnViewUpdateProjSpecific(); return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 82c21a973..b1424074a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -383,6 +383,12 @@ namespace Barotrauma.Items.Components wire.RemoveConnection(item); } } + c.Grid = null; + } + foreach (var connection in Connections) + { + Powered.ChangedConnections.Remove(connection); + connection.Recipients.Clear(); } Connections.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index f405963c9..ec26fafb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -305,16 +305,17 @@ namespace Barotrauma.Items.Components (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) { - if (item.body != null && !item.body.Enabled) - { - lightBrightness = 0.0f; - SetLightSourceState(false, 0.0f); - } - else + if (item.body == null || item.body.Enabled || + (item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems)) { lightBrightness = 1.0f; SetLightSourceState(true, lightBrightness); } + else + { + lightBrightness = 0.0f; + SetLightSourceState(false, 0.0f); + } isOn = true; SetLightSourceTransformProjSpecific(); base.IsActive = false; @@ -341,8 +342,22 @@ namespace Barotrauma.Items.Components #if CLIENT Light.ParentSub = item.Submarine; #endif + + + bool visibleInContainer; var ownerCharacter = item.GetRootInventoryOwner() as Character; - if ((item.Container != null && ownerCharacter == null) || + if (ownerCharacter != null && item.RootContainer?.GetComponent() is not { IsActive: true }) + { + //if the item is in a character inventory, the light should only be visible if the character is holding the item + //(not if it's e.q. inside a wearable item, or in a rifle worn on the back) + visibleInContainer = false; + } + else + { + visibleInContainer = item.FindParentInventory(static it => it is ItemInventory { Container.HideItems: true }) == null; + } + + if ((item.Container != null && !visibleInContainer && ownerCharacter == null) || (ownerCharacter != null && ownerCharacter.InvisibleTimer > 0.0f)) { lightBrightness = 0.0f; @@ -352,7 +367,7 @@ namespace Barotrauma.Items.Components SetLightSourceTransformProjSpecific(); PhysicsBody body = ParentBody ?? item.body; - if (body != null && !body.Enabled) + if (body != null && !body.Enabled && !visibleInContainer) { lightBrightness = 0.0f; SetLightSourceState(false, 0.0f); @@ -432,6 +447,11 @@ namespace Barotrauma.Items.Components target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness); } + public override void Drop(Character dropper, bool setTransform = true) + { + SetLightSourceTransform(); + } + partial void SetLightSourceState(bool enabled, float brightness); public void SetLightSourceTransform() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 85e995e5f..8f1228208 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -97,7 +97,8 @@ namespace Barotrauma.Items.Components string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character"); if (!Enum.TryParse(triggeredByAttribute, out triggeredBy)) { - DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type."); + DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type.", + contentPackage: element.ContentPackage); } triggerOnce = element.GetAttributeBool("triggeronce", false); string parentDebugName = $"TriggerComponent in {item.Name}"; @@ -274,6 +275,15 @@ namespace Barotrauma.Items.Components } } + protected override void RemoveComponentSpecific() + { + if (PhysicsBody != null) + { + PhysicsBody.Remove(); + PhysicsBody = null; + } + } + public override void ReceiveSignal(Signal signal, Connection connection) { base.ReceiveSignal(signal, connection); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 0ad0227b2..c89a073aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -61,6 +61,22 @@ namespace Barotrauma.Items.Components private const float CrewAiFindTargetMaxInterval = 1.0f; private const float CrewAIFindTargetMinInverval = 0.2f; + /// + /// Bots consider the projectile to move at least this fast when calculating how far ahead a moving target they need to aim. + /// Aiming ahead doesn't work reliably with very slow projectiles, because we'd need to take into account drag and gravity, + /// and the target would most likely move in a different direction anyway before the projectile reaches it. + /// + private const float MinimumProjectileVelocityForAimAhead = 20.0f; + + /// + /// Bots don't try to aim ahead a moving target by more than this amount. If the target is very fast and/or the projectile very slow, + /// we'd need to aim so far ahead it'd most likely fail anyway. + /// + private const float MaximumAimAhead = 10.0f; + + private float projectileSpeed; + private Item previousAmmo; + private int currentLoaderIndex; private const float TinkeringPowerCostReduction = 0.2f; @@ -563,8 +579,9 @@ namespace Barotrauma.Items.Components // Do not increase the weapons skill when operating a turret in an outpost level if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost)) { - user.Info.IncreaseSkillLevel("weapons".ToIdentifier(), - SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f)); + user.Info.ApplySkillGain( + Tags.WeaponsSkill, + SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime); } float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f); @@ -664,6 +681,11 @@ namespace Barotrauma.Items.Components return GetAvailableInstantaneousBatteryPower() >= GetPowerRequiredToShoot(); } + private Vector2 GetBarrelDir() + { + return new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + } + private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false) { tryingToCharge = true; @@ -709,7 +731,8 @@ namespace Barotrauma.Items.Components ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent(); if (projectileContainer != null && projectileContainer.Item != item) { - projectileContainer?.Item.Use(deltaTime); + //user needs to be null because the ammo boxes shouldn't be directly usable by characters + projectileContainer?.Item.Use(deltaTime, user: null, userForOnUsedEvent: user); } } else @@ -735,7 +758,7 @@ namespace Barotrauma.Items.Components ItemContainer projectileContainer = containerItem.GetComponent(); if (projectileContainer != null) { - containerItem.Use(deltaTime); + containerItem.Use(deltaTime, user: null, userForOnUsedEvent: user); projectiles = GetLoadedProjectiles(); if (projectiles.Any()) { return true; } } @@ -930,6 +953,7 @@ namespace Barotrauma.Items.Components Projectile projectileComponent = projectile.GetComponent(); if (projectileComponent != null) { + TryDetermineProjectileSpeed(projectileComponent); projectileComponent.Launcher = item; projectileComponent.Attacker = projectileComponent.User = user; if (projectileComponent.Attack != null) @@ -960,6 +984,16 @@ namespace Barotrauma.Items.Components LaunchProjSpecific(); } + private void TryDetermineProjectileSpeed(Projectile projectile) + { + if (projectile != null && !projectile.Hitscan) + { + projectileSpeed = + ConvertUnits.ToDisplayUnits( + MathHelper.Clamp((projectile.LaunchImpulse + LaunchImpulse) / projectile.Item.body.Mass, MinimumProjectileVelocityForAimAhead, NetConfig.MaxPhysicsBodyVelocity)); + } + } + partial void LaunchProjSpecific(); private static void ShiftItemsInProjectileContainer(ItemContainer container) @@ -1143,7 +1177,7 @@ namespace Barotrauma.Items.Components if (target is Hull targetHull) { - Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + Vector2 barrelDir = GetBarrelDir(); if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _)) { return; @@ -1244,8 +1278,24 @@ namespace Barotrauma.Items.Components if (container != null) { maxProjectileCount += container.Capacity; - int projectiles = projectileContainer.ContainedItems.Count(it => it.Condition > 0.0f); - usableProjectileCount += projectiles; + var projectiles = projectileContainer.ContainedItems.Where(it => it.Condition > 0.0f); + var firstProjectile = projectiles.FirstOrDefault(); + + if (firstProjectile?.Prefab != previousAmmo?.Prefab) + { + //assume the projectiles are infinitely fast (no aiming ahead of the target) if we can't find projectiles to calculate the speed based on, + //and if the projectile type isn't the same as before + projectileSpeed = float.PositiveInfinity; + } + previousAmmo = firstProjectile; + if (projectiles.Any()) + { + var projectile = + firstProjectile.GetComponent() ?? + firstProjectile.ContainedItems.FirstOrDefault()?.GetComponent(); + TryDetermineProjectileSpeed(projectile); + usableProjectileCount += projectiles.Count(); + } } } } @@ -1409,6 +1459,7 @@ namespace Barotrauma.Items.Components targetPos = currentTarget.WorldPosition; } bool iceSpireSpotted = false; + Vector2 targetVelocity = Vector2.Zero; // Adjust the target character position (limb or submarine) if (currentTarget is Character targetCharacter) { @@ -1424,20 +1475,39 @@ namespace Barotrauma.Items.Components 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; + float closestDistSqr = closestDistance; foreach (Limb limb in targetCharacter.AnimController.Limbs) { if (limb.IsSevered) { continue; } if (limb.Hidden) { continue; } if (!IsWithinAimingRadius(limb.WorldPosition)) { continue; } - float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); - if (dist < closestDist) + float distSqr = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); + if (distSqr < closestDistSqr) { - closestDist = dist; + closestDistSqr = distSqr; + if (limb == targetCharacter.AnimController.MainLimb) + { + //prefer main limb (usually a much better target than the extremities that are often the closest limbs) + closestDistSqr *= 0.5f; + } targetPos = limb.WorldPosition; } } - if (closestDist > shootDistance * shootDistance) + if (projectileSpeed < float.PositiveInfinity && targetPos.HasValue) + { + //lead the target (aim where the target will be in the future) + float dist = MathF.Sqrt(closestDistSqr); + float projectileMovementTime = dist / projectileSpeed; + + targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity; + Vector2 movementAmount = targetVelocity * projectileMovementTime; + //don't try to compensate more than 10 meters - if the target is so fast or the projectile so slow we need to go beyond that, + //it'd most likely fail anyway + movementAmount = ConvertUnits.ToDisplayUnits(movementAmount.ClampLength(MaximumAimAhead)); + Vector2 futurePosition = targetPos.Value + movementAmount; + targetPos = Vector2.Lerp(targetPos.Value, futurePosition, DegreeOfSuccess(character)); + } + if (closestDistSqr > shootDistance * shootDistance) { aiFindTargetTimer = CrewAIFindTargetMinInverval; ResetTarget(); @@ -1512,7 +1582,9 @@ namespace Barotrauma.Items.Components if (targetPos == null) { return false; } // Force the highest priority so that we don't change the objective while targeting enemies. objective.ForceHighestPriority = true; - +#if CLIENT + debugDrawTargetPos = targetPos.Value; +#endif if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget) { if (character.IsOnPlayerTeam) @@ -1563,8 +1635,28 @@ namespace Barotrauma.Items.Components if (IsPointingTowards(targetPos.Value)) { - Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); - Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); + Vector2 barrelDir = GetBarrelDir(); + Vector2 aimStartPos = item.WorldPosition; + Vector2 aimEndPos = item.WorldPosition + barrelDir * shootDistance; + bool allowShootingIfNothingInWay = false; + if (currentTarget != null) + { + Vector2 targetStartPos = currentTarget.WorldPosition; + Vector2 targetEndPos = currentTarget.WorldPosition + targetVelocity * ConvertUnits.ToDisplayUnits(MaximumAimAhead); + + //if there's nothing in the way (not even the target we're trying to aim towards), + //shooting should only be allowed if we're aiming ahead of the target, in which case it's to be expected that we're aiming at "thin air" + allowShootingIfNothingInWay = + targetVelocity.LengthSquared() > 0.001f && + MathUtils.LineSegmentsIntersect( + aimStartPos, aimEndPos, + targetStartPos, targetEndPos) && + //target needs to be moving roughly perpendicular to us for aiming ahead of it to make sense + Math.Abs(Vector2.Dot(Vector2.Normalize(aimEndPos - aimStartPos), Vector2.Normalize(targetEndPos - targetStartPos))) < 0.5f; + } + + Vector2 start = ConvertUnits.ToSimUnits(aimStartPos); + Vector2 end = ConvertUnits.ToSimUnits(aimEndPos); // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target. Body worldTarget = CheckLineOfSight(start, end); if (closestEnemy != null && closestEnemy.Submarine != null) @@ -1572,11 +1664,13 @@ namespace Barotrauma.Items.Components start -= closestEnemy.Submarine.SimPosition; end -= closestEnemy.Submarine.SimPosition; Body transformedTarget = CheckLineOfSight(start, end); - canShoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character)); + canShoot = + CanShoot(transformedTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay) && + (worldTarget == null || CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay)); } else { - canShoot = CanShoot(worldTarget, character); + canShoot = CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay); } if (!canShoot) { return false; } if (character.IsOnPlayerTeam) @@ -1666,9 +1760,13 @@ namespace Barotrauma.Items.Components } } - private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true) + private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true, bool allowShootingIfNothingInWay = false) { - if (targetBody == null) { return false; } + if (targetBody == null) + { + //nothing in the way (not even the target we're trying to shoot) -> no point in firing at thin air + return allowShootingIfNothingInWay; + } Character targetCharacter = null; if (targetBody.UserData is Character c) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index c94cc73e0..180487e83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -362,7 +362,8 @@ namespace Barotrauma.Items.Components case "sprite": if (subElement.GetAttribute("texture") == null) { - DebugConsole.ThrowError("Item \"" + item.Name + "\" doesn't have a texture specified!"); + DebugConsole.ThrowError("Item \"" + item.Name + "\" doesn't have a texture specified!", + contentPackage: element.ContentPackage); return; } @@ -558,6 +559,7 @@ namespace Barotrauma.Items.Components foreach (WearableSprite wearableSprite in wearableSprites) { wearableSprite?.Sprite?.Remove(); + wearableSprite.Picker = null; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 4c4c9c0a8..078765cf1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -230,11 +230,19 @@ namespace Barotrauma protected readonly int capacity; protected readonly ItemSlot[] slots; - + public bool Locked; protected float syncItemsDelay; + + private int extraStackSize; + public int ExtraStackSize + { + get => extraStackSize; + set => extraStackSize = MathHelper.Max(value, 0); + } + /// /// All items contained in the inventory. Stacked items are returned as individual instances. DO NOT modify the contents of the inventory while enumerating this list. /// @@ -1018,7 +1026,6 @@ namespace Barotrauma visualSlots[n].ShowBorderHighlight(Color.White, 0.1f, 0.4f); if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; } } - syncItemsDelay = 1.0f; #endif CharacterHUD.RecreateHudTextsIfFocused(item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 379a616a7..c44369a96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -240,7 +240,21 @@ namespace Barotrauma } } - public Item RootContainer { get; private set; } + private Item rootContainer; + public Item RootContainer + { + get { return rootContainer; } + private set + { + if (value == this) + { + DebugConsole.ThrowError($"Attempted to set the item \"{Prefab.Identifier}\" as it's own root container!\n{Environment.StackTrace.CleanupStackTrace()}"); + rootContainer = null; + return; + } + rootContainer = value; + } + } private bool inWaterProofContainer; @@ -369,7 +383,7 @@ namespace Barotrauma public float RotationRad { get; private set; } - [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, IsPropertySaveable.Yes)] + [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)] public float Rotation { get @@ -379,7 +393,7 @@ namespace Barotrauma set { if (!Prefab.AllowRotatingInEditor) { return; } - RotationRad = MathHelper.ToRadians(value); + RotationRad = MathUtils.WrapAnglePi(MathHelper.ToRadians(value)); #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { @@ -443,7 +457,7 @@ namespace Barotrauma set { if (scale == value) { return; } - scale = MathHelper.Clamp(value, 0.01f, 10.0f); + scale = MathHelper.Clamp(value, Prefab.MinScale, Prefab.MaxScale); float relativeScale = scale / base.Prefab.Scale; @@ -1077,7 +1091,8 @@ 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 + ")", + contentPackage: element.ContentPackage); } else { @@ -1232,7 +1247,8 @@ namespace Barotrauma var holdables = components.Where(c => c is Holdable); if (holdables.Count() > 1) { - DebugConsole.AddWarning($"Item {Prefab.Identifier} has multiple {nameof(Holdable)} components ({string.Join(", ", holdables.Select(h => h.GetType().Name))})."); + DebugConsole.AddWarning($"Item {Prefab.Identifier} has multiple {nameof(Holdable)} components ({string.Join(", ", holdables.Select(h => h.GetType().Name))}).", + Prefab.ContentPackage); } InsertToList(); @@ -1311,8 +1327,11 @@ namespace Barotrauma } } - if (FlippedX) clone.FlipX(false); - if (FlippedY) clone.FlipY(false); + if (FlippedX) { clone.FlipX(false); } + if (FlippedY) { clone.FlipY(false); } + + // Flipping an item tampers with its rotation, so restore it + clone.Rotation = Rotation; foreach (ItemComponent component in clone.components) { @@ -1624,6 +1643,9 @@ namespace Barotrauma return transformedRect; } + public override Quad2D GetTransformedQuad() + => Quad2D.FromSubmarineRectangle(rect).Rotated(-RotationRad); + /// /// goes through every item and re-checks which hull they are in /// @@ -1676,6 +1698,12 @@ namespace Barotrauma while (rootContainer.Container != null) { rootContainer = rootContainer.Container; + if (rootContainer == this) + { + DebugConsole.ThrowError($"Invalid container hierarchy: \"{Prefab.Identifier}\" was contained inside itself!\n{Environment.StackTrace.CleanupStackTrace()}"); + rootContainer = null; + break; + } inWaterProofContainer |= rootContainer.WaterProof; } newRootContainer = rootContainer; @@ -1938,7 +1966,7 @@ namespace Barotrauma } - public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime,bool playSound = true) { if (Indestructible || InvulnerableToDamage) { return new AttackResult(); } @@ -2494,7 +2522,7 @@ namespace Barotrauma if (Prefab.AllowRotatingInEditor) { - RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad); + RotationRad = MathUtils.WrapAnglePi(-RotationRad); } #if CLIENT if (Prefab.CanSpriteFlipX) @@ -2521,6 +2549,10 @@ namespace Barotrauma return; } + if (Prefab.AllowRotatingInEditor) + { + RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad); + } #if CLIENT if (Prefab.CanSpriteFlipY) { @@ -3021,7 +3053,10 @@ namespace Barotrauma return -1; } - public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null) + /// User to pass to the OnUsed event. May need to be different than the user in cases like loaders using ammo boxes: + /// the box is technically being used by the loader, and doesn't allow a character to use it, but we may still need to know which character caused + /// the box to be used. + public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null, Character userForOnUsedEvent = null) { if (RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim))) { @@ -3046,7 +3081,7 @@ namespace Barotrauma ic.PlaySound(ActionType.OnUse, user); #endif ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, user, targetLimb, useTarget: useTarget, user: user); - ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user)); + ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user ?? userForOnUsedEvent)); if (ic.DeleteOnUse) { remove = true; } } } @@ -3504,7 +3539,26 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - if (!CanClientAccess(sender) || !(property.GetAttribute()?.IsEditable(this) ?? true)) + bool conditionAllowsEditing = true; + if (property.GetAttribute() is { } condition) + { + conditionAllowsEditing = condition.IsEditable(this); + } + + bool canAccess = false; + if (Container?.GetComponent() != null && + Container.CanClientAccess(sender)) + { + //items inside circuit boxes are inaccessible by "normal" means, + //but the properties can still be edited through the circuit box UI + canAccess = true; + } + else + { + canAccess = CanClientAccess(sender); + } + + if (!canAccess || !conditionAllowsEditing) { allowEditing = false; } @@ -3777,6 +3831,11 @@ namespace Barotrauma } break; } + case "itemstats": + { + item.StatManager.Load(subElement); + break; + } default: { ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString()); @@ -3902,7 +3961,7 @@ namespace Barotrauma foreach (ItemComponent component in item.components) { - if (component.Parent != null) { component.IsActive = component.Parent.IsActive; } + if (component.Parent != null && component.InheritParentIsActive) { component.IsActive = component.Parent.IsActive; } component.OnItemLoaded(); } @@ -3972,6 +4031,8 @@ namespace Barotrauma upgrade.Save(element); } + statManager?.Save(element); + element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture))); var conditionAttribute = element.GetAttribute("condition"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index fecfef994..e0bf2763e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -23,9 +23,10 @@ namespace Barotrauma Upgrade = 8, ItemStat = 9, DroppedStack = 10, + SetHighlight = 11, MinValue = 0, - MaxValue = 10 + MaxValue = 11 } public interface IEventData : NetEntityEvent.IData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 42cefb7bb..1b6d9aff5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -229,7 +229,7 @@ namespace Barotrauma /// public readonly int FabricationLimitMin, FabricationLimitMax; - public FabricationRecipe(XElement element, Identifier itemPrefab) + public FabricationRecipe(ContentXElement element, Identifier itemPrefab) { TargetItemPrefabIdentifier = itemPrefab; var displayNameIdentifier = element.GetAttributeIdentifier("displayname", ""); @@ -245,7 +245,8 @@ namespace Barotrauma OutCondition = element.GetAttributeFloat("outcondition", 1.0f); if (OutCondition > 1.0f) { - DebugConsole.AddWarning($"Error in \"{itemPrefab}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100})."); + DebugConsole.AddWarning($"Error in \"{itemPrefab}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100}).", + element.ContentPackage); } var requiredItems = new List(); RequiresRecipe = element.GetAttributeBool("requiresrecipe", false); @@ -267,9 +268,10 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "requiredskill": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! Use skill identifiers instead of names."); + DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! Use skill identifiers instead of names.", + contentPackage: element.ContentPackage); continue; } @@ -283,7 +285,8 @@ namespace Barotrauma Identifier requiredItemTag = subElement.GetAttributeIdentifier("tag", Identifier.Empty); if (requiredItemIdentifier == Identifier.Empty && requiredItemTag == Identifier.Empty) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! One of the required items has no identifier or tag."); + DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! One of the required items has no identifier or tag.", + contentPackage: element.ContentPackage); continue; } @@ -819,7 +822,7 @@ namespace Barotrauma [Serialize(null, IsPropertySaveable.No)] public string EquipConfirmationText { get; set; } - [Serialize(true, IsPropertySaveable.No, description: "Can the item be rotated in the submarine editor.")] + [Serialize(true, IsPropertySaveable.No, description: "Can the item be rotated in the submarine editor?")] public bool AllowRotatingInEditor { get; set; } [Serialize(false, IsPropertySaveable.No)] @@ -830,7 +833,13 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.No)] public bool CanFlipY { get; private set; } - + + [Serialize(0.1f, IsPropertySaveable.No)] + public float MinScale { get; private set; } + + [Serialize(10.0f, IsPropertySaveable.No)] + public float MaxScale { get; private set; } + [Serialize(false, IsPropertySaveable.No)] public bool IsDangerous { get; private set; } @@ -862,24 +871,43 @@ namespace Barotrauma public int GetMaxStackSize(Inventory inventory) { + int extraStackSize = inventory switch + { + ItemInventory { Owner: Item it } i => (int)it.StatManager.GetAdjustedValueAdditive(ItemTalentStats.ExtraStackSize, i.ExtraStackSize), + CharacterInventory { Owner: Character { Info: { } info } } i => i.ExtraStackSize + (int)info.GetSavedStatValueWithAll(StatTypes.InventoryExtraStackSize, Category.ToIdentifier()), + not null => inventory.ExtraStackSize, + null => 0 + }; + if (inventory is CharacterInventory && maxStackSizeCharacterInventory > 0) { - return maxStackSizeCharacterInventory; + return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize); } else if (inventory?.Owner is Item item && (item.GetComponent() is { Attachable: false } || item.GetComponent() != null)) { if (maxStackSizeHoldableOrWearableInventory > 0) { - return maxStackSizeHoldableOrWearableInventory; + return MaxStackWithExtra(maxStackSizeHoldableOrWearableInventory, extraStackSize); } else if (maxStackSizeCharacterInventory > 0) { //if maxStackSizeHoldableOrWearableInventory is not set, it defaults to maxStackSizeCharacterInventory - return maxStackSizeCharacterInventory; + return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize); } } - return maxStackSize; + + return MaxStackWithExtra(maxStackSize, extraStackSize); + + static int MaxStackWithExtra(int maxStackSize, int extraStackSize) + { + extraStackSize = Math.Max(extraStackSize, 0); + if (maxStackSize == 1) + { + return Math.Min(maxStackSize, Inventory.MaxPossibleStackSize); + } + return Math.Min(maxStackSize + extraStackSize, Inventory.MaxPossibleStackSize); + } } [Serialize(false, IsPropertySaveable.No)] @@ -1016,7 +1044,8 @@ namespace Barotrauma if (ConfigElement.GetAttribute("cargocontainername") != null) { - DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name.", + contentPackage: ConfigElement.ContentPackage); } SerializableProperty.DeserializeProperties(this, ConfigElement); @@ -1054,7 +1083,8 @@ namespace Barotrauma if (subElement.GetAttribute("sourcerect") == null && subElement.GetAttribute("sheetindex") == null) { - DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!"); + DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!", + contentPackage: ConfigElement.ContentPackage); } Size = Sprite.size; @@ -1072,7 +1102,8 @@ namespace Barotrauma if (priceInfo.StoreIdentifier.IsEmpty) { continue; } if (storePrices.ContainsKey(priceInfo.StoreIdentifier)) { - DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the store \"{priceInfo.StoreIdentifier}\" defined more than once."); + DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the store \"{priceInfo.StoreIdentifier}\" defined more than once.", + ContentPackage); storePrices[priceInfo.StoreIdentifier] = priceInfo; } else @@ -1085,7 +1116,8 @@ namespace Barotrauma { if (storePrices.ContainsKey(locationType)) { - DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the location type \"{locationType}\" defined more than once."); + DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the location type \"{locationType}\" defined more than once.", + ContentPackage); storePrices[locationType] = new PriceInfo(subElement); } else @@ -1103,13 +1135,15 @@ namespace Barotrauma { if (itemElement.Attribute("name") != null) { - DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items."); + DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items.", + contentPackage: ConfigElement.ContentPackage); continue; } var deconstructItem = new DeconstructItem(itemElement, Identifier); if (deconstructItem.ItemIdentifier.IsEmpty) { - DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier."); + DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier.", + contentPackage: ConfigElement.ContentPackage); continue; } deconstructItems.Add(deconstructItem); @@ -1122,12 +1156,18 @@ namespace Barotrauma var newRecipe = new FabricationRecipe(subElement, Identifier); if (fabricationRecipes.TryGetValue(newRecipe.RecipeHash, out var prevRecipe)) { + //the errors below may be caused by a mod overriding a base item instead of this one, log the package of the base item in that case + var packageToLog = + (variantOf.ContentPackage != null && variantOf.ContentPackage != ContentPackageManager.VanillaCorePackage) ? + variantOf.ContentPackage : + GetParentModPackageOrThisPackage(); + int prevRecipeIndex = loadedRecipes.IndexOf(prevRecipe); - DebugConsole.ThrowError( + DebugConsole.AddWarning( $"Error in item prefab \"{ToString()}\": " + - $"Fabrication recipe #{loadedRecipes.Count + 1} has the same hash as recipe #{prevRecipeIndex + 1}. This is most likely caused by identical, duplicate recipes." + - $"This will cause issues with fabrication." - ); + $"Fabrication recipe #{loadedRecipes.Count + 1} has the same hash as recipe #{prevRecipeIndex + 1}. This is most likely caused by identical, duplicate recipes. " + + $"This will cause issues with fabrication.", + contentPackage: packageToLog); } else { @@ -1142,7 +1182,8 @@ namespace Barotrauma //it's ok for variants to clear the primary and secondary containers to disable the PreferredContainer element if (variantOf == null) { - DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement})."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement}).", + contentPackage: ConfigElement.ContentPackage); } } else @@ -1192,7 +1233,8 @@ namespace Barotrauma case "suitabletreatment": if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names.", + contentPackage: ConfigElement.ContentPackage); } Identifier treatmentIdentifier = subElement.GetAttributeIdentifier("identifier", subElement.GetAttributeIdentifier("type", Identifier.Empty)); float suitability = subElement.GetAttributeFloat("suitability", 0.0f); @@ -1201,6 +1243,8 @@ namespace Barotrauma } } + Size = ConfigElement.GetAttributeVector2(nameof(Size), Size); + #if CLIENT ParseSubElementsClient(ConfigElement, variantOf); #endif @@ -1231,7 +1275,7 @@ namespace Barotrauma if (Sprite == null) { - DebugConsole.ThrowError($"Item \"{ToString()}\" has no sprite!"); + DebugConsole.ThrowError($"Item \"{ToString()}\" has no sprite!", contentPackage: ConfigElement.ContentPackage); #if SERVER this.sprite = new Sprite("", Vector2.Zero); this.sprite.SourceRect = new Rectangle(0, 0, 32, 32); @@ -1248,7 +1292,8 @@ namespace Barotrauma if (Identifier == Identifier.Empty) { DebugConsole.ThrowError( - $"Item prefab \"{ToString()}\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); + $"Item prefab \"{ToString()}\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading.", + contentPackage: ConfigElement.ContentPackage); } #if DEBUG @@ -1256,7 +1301,8 @@ namespace Barotrauma { if (!string.IsNullOrEmpty(OriginalName)) { - DebugConsole.AddWarning($"Item \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages."); + DebugConsole.AddWarning($"Item \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages.", + ContentPackage); } } #endif @@ -1316,9 +1362,9 @@ namespace Barotrauma { string message = $"Tried to get price info for \"{Identifier}\" with a null store parameter!\n{Environment.StackTrace.CleanupStackTrace()}"; #if DEBUG - DebugConsole.LogError(message); + DebugConsole.LogError(message, contentPackage: ContentPackage); #else - DebugConsole.AddWarning(message); + DebugConsole.AddWarning(message, ContentPackage); GameAnalyticsManager.AddErrorEventOnce("ItemPrefab.GetPriceInfo:StoreParameterNull", GameAnalyticsManager.ErrorSeverity.Error, message); #endif return null; @@ -1525,6 +1571,9 @@ namespace Barotrauma void CheckXML(XElement originalElement, XElement variantElement, XElement result) { + //if either the parent or the variant are non-vanilla, assume the error is coming from that package + var packageToLog = parent.ContentPackage != GameMain.VanillaContent ? parent.ContentPackage : ContentPackage; + if (result == null) { return; } if (result.Name.ToIdentifier() == "RequiredItem" && result.Parent?.Name.ToIdentifier() == "Fabricate") @@ -1538,7 +1587,8 @@ namespace Barotrauma { 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."); + $"If this is not intentional, you can use empty elements in the item variant to remove any excess inherited fabrication requirements.", + packageToLog); } return; } @@ -1549,7 +1599,8 @@ namespace Barotrauma 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."); + "Specify the amount in the variant to fix this.", + packageToLog); } } if (originalElement?.Name.ToIdentifier() == "Deconstruct" && @@ -1559,18 +1610,35 @@ namespace Barotrauma variantElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem")) { DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + - $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. Overriding the base recipe may not work correctly."); + $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. Overriding the base recipe may not work correctly.", + packageToLog); } if (variantElement.Elements().Any(e => e.Name.ToIdentifier() == "Item") && originalElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem")) { DebugConsole.AddWarning($"Potential error in item \"{parent.Identifier}\": " + - $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. The item variant \"{Identifier}\" may not override the base recipe correctly."); + $"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. The item variant \"{Identifier}\" may not override the base recipe correctly.", + packageToLog); } } } } + /// + /// If the base prefab this one is a variant of is defined in a non-vanilla package, returns that non-vanilla package. + /// Otherwise returns the package of this prefab. Can be useful for logging errors that may have been caused by a mod overriding + /// the base item. + /// + public ContentPackage GetParentModPackageOrThisPackage() + { + if (ParentPrefab != null && + ParentPrefab.ContentPackage != ContentPackageManager.VanillaCorePackage) + { + return ParentPrefab.ContentPackage; + } + return ContentPackage; + } + public override string ToString() { return $"{Name} (identifier: {Identifier})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs index ba7aef6a9..d1b4ace8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs @@ -2,24 +2,46 @@ using System; using System.Collections.Generic; +using System.Xml.Linq; namespace Barotrauma { [NetworkSerialize] - internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option UniqueCharacterId) : INetSerializableStruct + internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option UniqueCharacterId, bool Save) : INetSerializableStruct { /// /// Stackable identifiers feature a unique ID to allow multiple stats applied by the same talent from different characters to coexist. /// public static TalentStatIdentifier CreateStackable(ItemTalentStats stat, Identifier talentIdentifier, UInt32 characterId) - => new(stat, talentIdentifier, Option.Some(characterId)); + => new(stat, talentIdentifier, Option.Some(characterId), Save: false); /// /// Unstackable identifiers do not have a unique ID causing them to be identical to other stats applied by the same talent from different characters and thus only one of them will be applied. /// will always use the highest value for unstackable stats. /// - public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier) - => new(stat, talentIdentifier, Option.None); + public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier, bool Save) + => new(stat, talentIdentifier, Option.None, Save); + + public XElement Serialize() + => new XElement("Stat", + new XAttribute("type", Stat), + new XAttribute("talent", TalentIdentifier)); + + public static Option TryLoadFromXML(XElement element) + { + var stat = element.GetAttributeEnum("type", ItemTalentStats.None); + var talentIdentifier = element.GetAttributeIdentifier("talent", Identifier.Empty); + + if (stat == ItemTalentStats.None || talentIdentifier == Identifier.Empty) + { + var error = $"Failed to load talent stat identifier from XML {element}"; + DebugConsole.ThrowError(error); + GameAnalyticsManager.AddErrorEventOnce("ItemStatManager.TryLoadFromXML:Invalid", GameAnalyticsManager.ErrorSeverity.Error, error); + return Option.None; + } + + return Option.Some(CreateUnstackable(stat, talentIdentifier, true)); + } } internal sealed class ItemStatManager @@ -29,14 +51,14 @@ namespace Barotrauma public ItemStatManager(Item item) => this.item = item; - public void ApplyStat(ItemTalentStats stat, bool stackable, float value, CharacterTalent talent) + public void ApplyStat(ItemTalentStats stat, bool stackable, bool save, float value, CharacterTalent talent) { if (talent.Character?.ID is not { } characterId || talent.Prefab?.Identifier is not { } talentIdentifier) { return; } var identifier = stackable ? TalentStatIdentifier.CreateStackable(stat, talentIdentifier, characterId) - : TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier); + : TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier, save); if (!stackable) { @@ -57,12 +79,45 @@ namespace Barotrauma #endif } + public void Save(XElement parent) + { + var element = new XElement("itemstats"); + + foreach (var (key, value) in talentStats) + { + if (!key.Save) { continue; } + + var statElement = key.Serialize(); + statElement.Add(new XAttribute("value", value)); + + element.Add(statElement); + } + + parent.Add(element); + } + + public void Load(XElement element) + { + foreach (XElement statElement in element.Elements()) + { + if (!TalentStatIdentifier.TryLoadFromXML(statElement).TryUnwrap(out var identifier)) { continue; } + + var value = statElement.GetAttributeFloat("value", 0f); + + ApplyStatDirect(identifier, value); + } + } + /// /// Used for setting the value value from network packet; bypassing all validity checks. /// - public void ApplyStatDirect(TalentStatIdentifier identifier, float value) => talentStats[identifier] = value; + public void ApplyStatDirect(TalentStatIdentifier identifier, float value) + => talentStats[identifier] = value; - public float GetAdjustedValue(ItemTalentStats stat, float originalValue) + /// + /// Adjusts the value by multiplying it with the value of the talent stat + /// + public float GetAdjustedValueMultiplicative(ItemTalentStats stat, float originalValue) { float total = originalValue; @@ -74,5 +129,21 @@ namespace Barotrauma return total; } + + /// + /// Adjusts the value by adding the value of the talent stat instead of multiplying it + /// + public float GetAdjustedValueAdditive(ItemTalentStats stat, float originalValue) + { + float total = originalValue; + + foreach (var (key, value) in talentStats) + { + if (key.Stat != stat) { continue; } + total += value; + } + + return total; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 4e65d9b65..f868f7d3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -222,7 +222,7 @@ namespace Barotrauma 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."); + DebugConsole.ThrowError($"Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names.", contentPackage: element.ContentPackage); Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty()); //attempt to convert to identifiers and tags List convertedIdentifiers = new List(); @@ -299,7 +299,7 @@ namespace Barotrauma } if (!Enum.TryParse(typeStr, true, out type)) { - DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type."); + DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type.", contentPackage: element.ContentPackage); type = RelationType.Invalid; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs index a50646860..80428221a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/DummyFireSource.cs @@ -9,7 +9,8 @@ namespace Barotrauma public bool CausedByPsychosis; - public DummyFireSource(Vector2 maxSize, Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) : base(worldPosition, spawningHull, isNetworkMessage) + public DummyFireSource(Vector2 maxSize, Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) : + base(worldPosition, spawningHull, sourceCharacter: null, isNetworkMessage: isNetworkMessage) { this.maxSize = maxSize; DamagesItems = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index 6d9577b89..3fa6c8e18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -91,7 +91,12 @@ namespace Barotrauma get { return hull; } } - public FireSource(Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) + /// + /// Which character caused this fire (if any)? + /// + public readonly Character SourceCharacter; + + public FireSource(Vector2 worldPosition, Hull spawningHull = null, Character sourceCharacter = null, bool isNetworkMessage = false) { hull = Hull.FindHull(worldPosition, spawningHull); if (hull == null || worldPosition.Y < hull.WorldSurface) { return; } @@ -109,6 +114,8 @@ namespace Barotrauma position -= Submarine.Position; } + SourceCharacter = sourceCharacter; + #if CLIENT lightSource = new LightSource(this.position, 50.0f, new Color(1.0f, 0.9f, 0.7f), hull?.Submarine); #endif @@ -306,8 +313,12 @@ namespace Barotrauma foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered) { continue; } - c.LastDamageSource = null; - c.DamageLimb(WorldPosition, limb, AfflictionPrefab.Burn.Instantiate(dmg).ToEnumerable(), 0.0f, false, 0.0f); + c.LastDamageSource = SourceCharacter; + c.DamageLimb(WorldPosition, limb, AfflictionPrefab.Burn.Instantiate(dmg).ToEnumerable(), + stun: 0.0f, + playSound: false, + attackImpulse: Vector2.Zero, + attacker: SourceCharacter); } #if CLIENT //let clients display the client-side damage immediately, otherwise they may not be able to react to the damage fast enough diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 7f899899a..049d7f621 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -442,6 +442,11 @@ namespace Barotrauma public List FakeFireSources { get; private set; } + /// + /// Can be used by conditionals + /// + public int FireCount => FireSources?.Count ?? 0; + public BallastFloraBehavior BallastFlora { get; set; } public Hull(Rectangle rectangle) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs index 1aeb8ddd4..bc01ab05d 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, Vector2 impulseDirection, float deltaTime, bool playSound = true); public readonly struct AttackEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs index f513fb078..4731ca9a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs @@ -89,7 +89,7 @@ namespace Barotrauma partial void AddDamageProjSpecific(float damage, Vector2 worldPosition); - public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true) { AddDamage(attack.StructureDamage, worldPosition); return new AttackResult(attack.StructureDamage); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 2c0a60a8a..5db866ad8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -115,6 +115,20 @@ namespace Barotrauma Ruin = null; Cave = cave; } + + /// + /// Caves, ruins, outposts and similar enclosed areas + /// + /// + public bool IsEnclosedArea() + { + return + PositionType == PositionType.Cave || + PositionType == PositionType.Ruin || + PositionType == PositionType.Outpost || + PositionType == PositionType.BeaconStation || + PositionType == PositionType.AbyssCave; + } } public enum TunnelType @@ -619,14 +633,14 @@ namespace Barotrauma { endHole = new Tunnel( TunnelType.SidePath, - new List() { startPosition, startExitPosition, new Point(0, Size.Y) }, + new List() { startPosition, new Point(0, startPosition.Y) }, minWidth, parentTunnel: mainPath); } else { endHole = new Tunnel( TunnelType.SidePath, - new List() { endPosition, endExitPosition, Size }, + new List() { endPosition, new Point(Size.X, endPosition.Y) }, minWidth, parentTunnel: mainPath); } Tunnels.Add(endHole); @@ -930,6 +944,7 @@ namespace Barotrauma foreach (AbyssIsland abyssIsland in AbyssIslands) { + abyssIsland.Cells.RemoveAll(c => c.CellType == CellType.Path); cells.AddRange(abyssIsland.Cells); } @@ -1726,7 +1741,9 @@ namespace Barotrauma { bool tooClose = false; - if (cell.IsPointInsideAABB(position, margin: minDistance)) + //if the cell is very large, the position can be far away from the edges while being inside the cell + //so we need to check that here too + if (cell.IsPointInside(position)) { tooClose = true; } @@ -3257,13 +3274,13 @@ namespace Barotrauma } Vector2 position = Vector2.Zero; - int tries = 0; do { - TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter); + TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out InterestingPosition potentialPos, filter); Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient); + Vector2 startPos = potentialPos.Position.ToVector2(); if (!IsPositionInsideWall(startPos + offset)) { startPos += offset; @@ -3271,14 +3288,18 @@ namespace Barotrauma Vector2 endPos = startPos - Vector2.UnitY * Size.Y; - if (Submarine.PickBody( - ConvertUnits.ToSimUnits(startPos), - ConvertUnits.ToSimUnits(endPos), - ExtraWalls.Where(w => w.Body?.BodyType == BodyType.Dynamic || w is DestructibleLevelWall).Select(w => w.Body).Union(Submarine.Loaded.Where(s => s.Info.Type == SubmarineType.Player).Select(s => s.PhysicsBody.FarseerBody)), - Physics.CollisionLevel | Physics.CollisionWall) != null) + //try to find a level wall below the position unless the position is indoors + if (!potentialPos.IsEnclosedArea()) { - position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos) * offsetFromWall; - break; + if (Submarine.PickBody( + ConvertUnits.ToSimUnits(startPos), + ConvertUnits.ToSimUnits(endPos), + ExtraWalls.Where(w => w.Body?.BodyType == BodyType.Dynamic || w is DestructibleLevelWall).Select(w => w.Body).Union(Submarine.Loaded.Where(s => s.Info.Type == SubmarineType.Player).Select(s => s.PhysicsBody.FarseerBody)), + Physics.CollisionLevel | Physics.CollisionWall)?.UserData is VoronoiCell) + { + position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos) * offsetFromWall; + break; + } } tries++; @@ -3293,25 +3314,25 @@ namespace Barotrauma return position; } - public bool TryGetInterestingPositionAwayFromPoint(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Vector2 awayPoint, float minDistFromPoint, Func filter = null) + public bool TryGetInterestingPositionAwayFromPoint(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Vector2 awayPoint, float minDistFromPoint, Func filter = null) { - bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, awayPoint, minDistFromPoint, filter); - position = pos.ToVector2(); + position = default; + bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out position, awayPoint, minDistFromPoint, filter); return success; } - public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Func filter = null, bool suppressWarning = false) + public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Func filter = null, bool suppressWarning = false) { - bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, Vector2.Zero, minDistFromPoint: 0, filter, suppressWarning); - position = pos.ToVector2(); + position = default; + bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out position, Vector2.Zero, minDistFromPoint: 0, filter, suppressWarning); return success; } - public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position, Vector2 awayPoint, float minDistFromPoint = 0f, Func filter = null, bool suppressWarning = false) + public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out InterestingPosition position, Vector2 awayPoint, float minDistFromPoint = 0f, Func filter = null, bool suppressWarning = false) { if (!PositionsOfInterest.Any()) { - position = new Point(Size.X / 2, Size.Y / 2); + position = default; return false; } @@ -3323,7 +3344,20 @@ namespace Barotrauma 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())); +#if DEBUG + for (int i = 0; i < PositionsOfInterest.Count; i++) + { + var pos = PositionsOfInterest[i]; + if (!suitablePositions.Contains(pos)) { continue; } + if (IsInvalid(pos)) + { + pos.IsValid = false; + PositionsOfInterest[i] = pos; + } + } +#endif + suitablePositions.RemoveAll(p => IsInvalid(p)); + bool IsInvalid(InterestingPosition p) => IsPositionInsideWall(p.Position.ToVector2()); } if (!suitablePositions.Any()) { @@ -3335,7 +3369,7 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg); #endif } - position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position; + position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))]; return false; } @@ -3361,14 +3395,14 @@ namespace Barotrauma DebugConsole.ThrowError(errorMsg); #endif float maxDist = 0.0f; - position = suitablePositions.First().Position; + position = suitablePositions.First(); foreach (InterestingPosition pos in suitablePositions) { float dist = Submarine.Loaded.Sum(s => Submarine.MainSubs.Contains(s) ? Vector2.DistanceSquared(s.WorldPosition, pos.Position.ToVector2()) : 0.0f); if (dist > maxDist) { - position = pos.Position; + position = pos; maxDist = dist; } } @@ -3376,7 +3410,7 @@ namespace Barotrauma return false; } - position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)].Position; + position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)]; return true; } @@ -3933,12 +3967,28 @@ namespace Barotrauma { var totalSW = new Stopwatch(); totalSW.Start(); + var wreckFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) .OrderBy(f => f.UintIdentifier).ToList(); + + for (int i = wreckFiles.Count - 1; i >= 0; i--) + { + var wreckFile = wreckFiles[i]; + var wreckInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsWreck); + var matchingInfo = wreckInfos.SingleOrDefault(info => info.FilePath == wreckFile.Path.Value); + Debug.Assert(matchingInfo != null); + if (matchingInfo?.WreckInfo is WreckInfo wreckInfo) + { + if (Difficulty < wreckInfo.MinLevelDifficulty || Difficulty > wreckInfo.MaxLevelDifficulty) + { + wreckFiles.RemoveAt(i); + } + } + } if (wreckFiles.None()) { - DebugConsole.ThrowError("No wreck files found in the selected content packages!"); + DebugConsole.ThrowError($"No wreck files found for the level difficulty {LevelData.Difficulty}!"); Wrecks = new List(); return; } @@ -4072,7 +4122,7 @@ namespace Barotrauma if (location != null) { - DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})"); + DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.DisplayName}, level type: {LevelData.Type})"); outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost); } else @@ -4180,7 +4230,20 @@ namespace Barotrauma } } - spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1); + Vector2 preferredSpawnPos = i == 0 ? StartPosition : EndPosition; + //if we're placing the outpost at the end of the level, close to the bottom-right, + //and there's a hole leading out the right side of the level, move the spawn position towards that hole. + //Makes outpost placement a little nicer in levels with lots of verticality: if there's a tall vertical + //shaft leading down to the end position, we don't want the outpost to be placed all the way up to wherever the + //ceiling is at the top of that shaft. + if (i == 1 && GenerationParams.CreateHoleNextToEnd && + preferredSpawnPos.X > Size.X * 0.75f && + preferredSpawnPos.Y < Size.Y * 0.25f) + { + preferredSpawnPos.X = (preferredSpawnPos.X + Size.X) / 2; + } + + spawnPos = outpost.FindSpawnPos(preferredSpawnPos, 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); @@ -4204,7 +4267,7 @@ namespace Barotrauma if (StartLocation != null) { outpost.TeamID = StartLocation.Type.OutpostTeam; - outpost.Info.Name = StartLocation.Name; + outpost.Info.Name = StartLocation.DisplayName.Value; } } else @@ -4213,7 +4276,7 @@ namespace Barotrauma if (EndLocation != null) { outpost.TeamID = EndLocation.Type.OutpostTeam; - outpost.Info.Name = EndLocation.Name; + outpost.Info.Name = EndLocation.DisplayName.Value; } } } @@ -4235,7 +4298,8 @@ namespace Barotrauma ContentFile contentFile = null; if (!string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { - contentFile = beaconStationFiles.OrderBy(b => b.UintIdentifier).FirstOrDefault(f => f.Path == GenerationParams.ForceBeaconStation); + var contentPath = ContentPath.FromRaw(GenerationParams.ContentPackage, GenerationParams.ForceBeaconStation); + contentFile = beaconStationFiles.OrderBy(b => b.UintIdentifier).FirstOrDefault(f => f.Path == contentPath); if (contentFile == null) { DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead..."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 77c3a7cf0..25e106aba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -241,7 +241,7 @@ namespace Barotrauma /// public LevelData(Location location, Map map, float difficulty) { - Seed = location.BaseName + map.Locations.IndexOf(location); + Seed = location.NameIdentifier.Value + map.Locations.IndexOf(location); Biome = location.Biome; Type = LevelType.Outpost; Difficulty = difficulty; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs index 1975dc33d..6d90bcda0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs @@ -160,7 +160,7 @@ namespace Barotrauma partial void InitProjSpecific(); - public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true) { if (Health <= 0.0f) { return new AttackResult(0.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 462235bed..98b36532c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -526,12 +526,13 @@ namespace Barotrauma { List spawnPosTypes = new List(4); List availableSpawnPositions = new List(); + bool requireCaveSpawnPos = spawnPosType == LevelObjectPrefab.SpawnPosType.CaveWall; foreach (var cell in cells) { foreach (var edge in cell.Edges) { if (!edge.IsSolid || edge.OutsideLevel) { continue; } - if (spawnPosType != LevelObjectPrefab.SpawnPosType.CaveWall && edge.NextToCave) { continue; } + if (requireCaveSpawnPos != edge.NextToCave) { continue; } Vector2 normal = edge.GetNormal(cell); Alignment edgeAlignment = 0; @@ -638,6 +639,7 @@ namespace Barotrauma public override void Remove() { + objectsInRange.Clear(); if (objects != null) { foreach (LevelObject obj in objects) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 03ebcb980..552b93ade 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -55,8 +55,17 @@ namespace Barotrauma public readonly List Connections = new List(); - private string baseName; + public LocalizedString DisplayName { get; private set; } + + public Identifier NameIdentifier => nameIdentifier; + private int nameFormatIndex; + private Identifier nameIdentifier; + + /// + /// For backwards compatibility: a non-localizable name from the old text files. + /// + private string rawName; private LocationType addInitialMissionsForType; @@ -75,10 +84,6 @@ namespace Barotrauma public bool DisallowLocationTypeChanges; - public string BaseName { get => baseName; } - - public string Name { get; private set; } - public Biome Biome { get; set; } public Vector2 MapPosition { get; private set; } @@ -309,7 +314,7 @@ namespace Barotrauma if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive) { price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false)); - price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, new Identifier("all"))); + price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll)); 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)); @@ -484,7 +489,7 @@ namespace Barotrauma { if (missionIndex < 0 || missionIndex >= availableMissions.Count) { - DebugConsole.ThrowError($"Failed to select a mission in location \"{Name}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})"); + DebugConsole.ThrowError($"Failed to select a mission in location \"{DisplayName}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})"); break; } selectedMissions.Add(availableMissions[missionIndex]); @@ -536,15 +541,15 @@ namespace Barotrauma public override string ToString() { - return $"Location ({Name ?? "null"})"; + return $"Location ({DisplayName ?? "null"})"; } public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable existingLocations = null) { Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost); - Name = RandomName(Type, rand, existingLocations); + CreateRandomName(Type, rand, existingLocations); MapPosition = mapPosition; - PortraitId = ToolBox.StringToInt(Name); + PortraitId = ToolBox.StringToInt(nameIdentifier.Value); Connections = new List(); } @@ -561,9 +566,21 @@ namespace Barotrauma GetTypeOrFallback(originalLocationTypeId, out LocationType originalType); OriginalType = originalType; - baseName = element.GetAttributeString("basename", ""); - Name = element.GetAttributeString("name", ""); - MapPosition = element.GetAttributeVector2("position", Vector2.Zero); + nameIdentifier = element.GetAttributeIdentifier(nameof(nameIdentifier), ""); + if (nameIdentifier.IsEmpty) + { + //backwards compatibility + rawName = element.GetAttributeString("basename", ""); + nameIdentifier = rawName.ToIdentifier(); + DisplayName = element.GetAttributeString("name", ""); + } + else + { + nameFormatIndex = element.GetAttributeInt(nameof(nameFormatIndex), 0); + DisplayName = GetName(Type, nameFormatIndex, nameIdentifier); + } + + MapPosition = element.GetAttributeVector2("position", Vector2.Zero); PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); @@ -639,9 +656,9 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!"); Type ??= LocationType.Prefabs.First(); - LevelData = new LevelData(element.Element("Level"), clampDifficultyToBiome: true); + LevelData = new LevelData(element.GetChildElement("Level"), clampDifficultyToBiome: true); - PortraitId = ToolBox.StringToInt(Name); + PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value); LoadStores(element); LoadMissions(element); @@ -659,15 +676,12 @@ namespace Barotrauma if (type == null) { DebugConsole.AddWarning($"Could not find location type \"{identifier}\". Using location type \"None\" instead."); - LocationType.Prefabs.TryGet("None".ToIdentifier(), out type); - if (type == null) - { - type = LocationType.Prefabs.First(); - } + LocationType.Prefabs.TryGet("None".ToIdentifier(), out type); + type ??= LocationType.Prefabs.First(); } if (type != null) { - element.SetAttributeValue("type", type.Identifier); + element.SetAttributeValue("type", type.Identifier.ToString()); } return false; } @@ -690,7 +704,7 @@ namespace Barotrauma int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0); if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >= Type.CanChangeTo.Count) { - DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{Name}\". Location index out of bounds ({locationTypeChangeIndex})."); + DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{DisplayName}\". Location index out of bounds ({locationTypeChangeIndex})."); continue; } PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null); @@ -701,7 +715,7 @@ namespace Barotrauma var mission = MissionPrefab.Prefabs[missionIdentifier]; if (mission == null) { - DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found."); + DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{DisplayName}\". Matching mission not found."); continue; } PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission); @@ -738,14 +752,27 @@ namespace Barotrauma if (newType == null) { - DebugConsole.ThrowError($"Failed to change the type of the location \"{Name}\" to null.\n" + Environment.StackTrace.CleanupStackTrace()); + DebugConsole.ThrowError($"Failed to change the type of the location \"{DisplayName}\" to null.\n" + Environment.StackTrace.CleanupStackTrace()); return; } - DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType); - Type = newType; - Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); + if (rawName != null) + { + DebugConsole.Log($"Location {rawName} changed it's type from {Type} to {newType}"); + DisplayName = + Type.NameFormats == null || !Type.NameFormats.Any() ? + rawName : + Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", rawName); + } + else + { + DebugConsole.Log($"Location {DisplayName.Value} changed it's type from {Type} to {newType}"); + DisplayName = + Type.NameFormats == null || !Type.NameFormats.Any() ? + TextManager.Get(nameIdentifier) : + Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", TextManager.Get(nameIdentifier).Value); + } if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { @@ -776,11 +803,11 @@ namespace Barotrauma { if (Type.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(randSync)); + UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(randSync), invokingContentPackage: Type.ContentPackage); } if (Type.MissionTags.Any()) { - UnlockMissionByTag(Type.MissionTags.GetRandom(randSync)); + UnlockMissionByTag(Type.MissionTags.GetRandom(randSync), invokingContentPackage: Type.ContentPackage); } } @@ -798,7 +825,7 @@ namespace Barotrauma AddMission(InstantiateMission(missionPrefab)); } - public Mission UnlockMissionByIdentifier(Identifier identifier) + public Mission UnlockMissionByIdentifier(Identifier identifier, ContentPackage invokingContentPackage = null) { if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; } if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; } @@ -806,7 +833,8 @@ namespace Barotrauma var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier); if (missionPrefab == null) { - DebugConsole.ThrowError($"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found."); + DebugConsole.ThrowError($"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found.", + contentPackage: invokingContentPackage); } else { @@ -823,13 +851,13 @@ namespace Barotrauma return null; } - public Mission UnlockMissionByTag(Identifier tag, Random random = null) + public Mission UnlockMissionByTag(Identifier tag, Random random = null, ContentPackage invokingContentPackage = null) { 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."); + DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found.", contentPackage: invokingContentPackage); } else { @@ -841,7 +869,16 @@ namespace Barotrauma { suitableMissions = unusedMissions; } - + var filteredMissions = suitableMissions.Where(m => LevelData.Difficulty >= m.MinLevelDifficulty && LevelData.Difficulty <= m.MaxLevelDifficulty); + if (filteredMissions.None()) + { + DebugConsole.AddWarning($"No suitable mission matching the level difficulty {LevelData.Difficulty} found with the tag \"{tag}\". Ignoring the restriction.", + contentPackage: invokingContentPackage); + } + else + { + suitableMissions = filteredMissions; + } MissionPrefab missionPrefab = random != null ? ToolBox.SelectWeightedRandom(suitableMissions.OrderBy(m => m.Identifier), m => m.Commonness, random) : @@ -854,12 +891,13 @@ namespace Barotrauma return null; } AddMission(mission); - DebugConsole.NewMessage($"Unlocked a random mission by \"{tag}\".", debugOnly: true); + DebugConsole.NewMessage($"Unlocked a random mission by \"{tag}\": {mission.Prefab.Identifier} (difficulty level: {LevelData.Difficulty})", debugOnly: true); return mission; } else { - DebugConsole.AddWarning($"Failed to unlock a mission with the tag \"{tag}\": all available missions have already been unlocked."); + DebugConsole.AddWarning($"Failed to unlock a mission with the tag \"{tag}\": all available missions have already been unlocked.", + contentPackage: invokingContentPackage); } } @@ -988,11 +1026,11 @@ namespace Barotrauma { if (addInitialMissionsForType.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandomUnsynced()); + UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandomUnsynced(), invokingContentPackage: Type.ContentPackage); } if (addInitialMissionsForType.MissionTags.Any()) { - UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandomUnsynced()); + UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandomUnsynced(), invokingContentPackage: Type.ContentPackage); } addInitialMissionsForType = null; } @@ -1050,12 +1088,12 @@ namespace Barotrauma { if (!Type.HasHireableCharacters) { - DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace()); + DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace()); return; } if (HireManager == null) { - DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace()); + DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace()); return; } @@ -1078,22 +1116,52 @@ namespace Barotrauma return HireManager.AvailableCharacters; } - private string RandomName(LocationType type, Random rand, IEnumerable existingLocations) + private void CreateRandomName(LocationType type, Random rand, IEnumerable existingLocations) { - if (!type.ForceLocationName.IsNullOrEmpty()) + if (!type.ForceLocationName.IsEmpty) { - baseName = type.ForceLocationName.Value; - return baseName; + nameIdentifier = type.ForceLocationName; + DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value); + return; + } + nameIdentifier = type.GetRandomNameId(rand, existingLocations); + if (nameIdentifier.IsEmpty) + { + rawName = type.GetRandomRawName(rand, existingLocations); + if (rawName.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"Failed to generate a name for a location of the type {type.Identifier}. No names found in localization files or the .txt files."); + rawName = "none"; + } + nameIdentifier = rawName.ToIdentifier(); + DisplayName = rawName; + } + else + { + if (type.NameFormats == null || !type.NameFormats.Any()) + { + DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value); + return; + } + nameFormatIndex = rand.Next() % type.NameFormats.Count; + DisplayName = GetName(Type, nameFormatIndex, nameIdentifier); } - 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) + private static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId) { - baseName = Name = name; + if (type?.NameFormats == null || !type.NameFormats.Any()) + { + return TextManager.Get(nameId); + } + return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace("[name]", TextManager.Get(nameId).Value); + } + + public void ForceName(Identifier nameId) + { + rawName = string.Empty; + nameIdentifier = nameId; + DisplayName = TextManager.Get(nameId).Fallback(nameId.Value); } public void LoadStores(XElement locationElement) @@ -1117,7 +1185,7 @@ namespace Barotrauma } else { - string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": duplicate identifier."; + string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": duplicate identifier."; DebugConsole.ThrowError(msg); GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:DuplicateStoreInfo", GameAnalyticsManager.ErrorSeverity.Error, msg); continue; @@ -1125,7 +1193,7 @@ namespace Barotrauma } else { - string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier."; + string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier."; DebugConsole.ThrowError(msg); GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:IncorrectStoreIdentifier", GameAnalyticsManager.ErrorSeverity.Error, msg); continue; @@ -1436,8 +1504,9 @@ namespace Barotrauma var locationElement = new XElement("location", new XAttribute("type", Type.Identifier), new XAttribute("originaltype", (Type ?? OriginalType).Identifier), - new XAttribute("basename", BaseName), - new XAttribute("name", Name), + /*not used currently (we load the nameIdentifier instead), + * but could make sense to include still for backwards compatibility reasons*/ + new XAttribute("name", DisplayName), new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty), new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)), new XAttribute("pricemultiplier", PriceMultiplier), @@ -1447,6 +1516,16 @@ namespace Barotrauma new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation), new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated)); + if (!rawName.IsNullOrEmpty()) + { + locationElement.Add(new XAttribute(nameof(rawName), rawName)); + } + else + { + locationElement.Add(new XAttribute(nameof(nameIdentifier), nameIdentifier)); + locationElement.Add(new XAttribute(nameof(nameFormatIndex), nameFormatIndex)); + } + if (Faction != null) { locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier)); @@ -1483,7 +1562,7 @@ namespace Barotrauma changeElement.Add(new XAttribute("index", index)); if (index == -1) { - DebugConsole.AddWarning($"Invalid location type change in the location \"{Name}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType})."); + DebugConsole.AddWarning($"Invalid location type change in the location \"{DisplayName}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType})."); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 475fd10e0..b300c4d02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -1,4 +1,5 @@ -using Barotrauma.IO; +using Barotrauma.Extensions; +using Barotrauma.IO; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -13,7 +14,7 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private readonly ImmutableArray names; + private readonly ImmutableArray rawNames; private readonly ImmutableArray portraits; // @@ -26,7 +27,7 @@ namespace Barotrauma public readonly LocalizedString Name; public readonly LocalizedString Description; - public readonly LocalizedString ForceLocationName; + public readonly Identifier ForceLocationName; public readonly float BeaconStationChance; @@ -54,12 +55,20 @@ namespace Barotrauma private set; } + private readonly ImmutableArray? nameIdentifiers = null; + + private LanguageIdentifier nameFormatLanguage; + private ImmutableArray? nameFormats = null; public IReadOnlyList NameFormats { get { - nameFormats ??= TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray(); + if (nameFormats == null || GameSettings.CurrentConfig.Language != nameFormatLanguage) + { + nameFormats = TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray(); + nameFormatLanguage = GameSettings.CurrentConfig.Language; + } return nameFormats; } } @@ -143,29 +152,37 @@ namespace Barotrauma if (element.GetAttribute("name") != null) { - ForceLocationName = TextManager.Get(element.GetAttributeString("name", string.Empty)); + ForceLocationName = element.GetAttributeIdentifier("name", string.Empty); } else { - string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" }); var names = new List(); - foreach (string rawPath in rawNamePaths) + //backwards compatibility for location names defined in a text file + string[] rawNamePaths = element.GetAttributeStringArray("namefile", Array.Empty()); + if (rawNamePaths.Any()) { - try + foreach (string rawPath in rawNamePaths) { - var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim()); - names.AddRange(File.ReadAllLines(path.Value).ToList()); + 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); + } } - catch (Exception e) + if (!names.Any()) { - DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e); + names.Add("ERROR: No names found"); } + this.rawNames = names.ToImmutableArray(); } - if (!names.Any()) + else { - names.Add("ERROR: No names found"); + nameIdentifiers = element.GetAttributeIdentifierArray("nameidentifiers", new Identifier[] { Identifier }).ToImmutableArray(); } - this.names = names.ToImmutableArray(); } string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty()); @@ -259,17 +276,64 @@ namespace Barotrauma return portraits[Math.Abs(randomSeed) % portraits.Length]; } - public string GetRandomName(Random rand, IEnumerable existingLocations) + public Identifier GetRandomNameId(Random rand, IEnumerable existingLocations) { + if (nameIdentifiers == null) + { + return Identifier.Empty; + } + List nameIds = new List(); + foreach (var nameId in nameIdentifiers) + { + int index = 0; + while (true) + { + Identifier tag = $"LocationName.{nameId}.{index}".ToIdentifier(); + if (TextManager.ContainsTag(tag, TextManager.DefaultLanguage)) + { + nameIds.Add(tag); + index++; + } + else + { + if (index == 0) + { + DebugConsole.ThrowError($"Could not find any location names for the location type {Identifier}. Name identifier: {nameId}"); + } + break; + } + } + } + if (nameIds.None()) + { + return Identifier.Empty; + } if (existingLocations != null) { - var unusedNames = names.Where(name => !existingLocations.Any(l => l.BaseName == name)).ToList(); + var unusedNameIds = nameIds.FindAll(nameId => existingLocations.None(l => l.NameIdentifier == nameId)); + if (unusedNameIds.Count > 0) + { + return unusedNameIds[rand.Next() % unusedNameIds.Count]; + } + } + return nameIds[rand.Next() % nameIds.Count]; + } + + /// + /// For backwards compatibility. Chooses a random name from the names defined in the .txt name files (). + /// + public string GetRandomRawName(Random rand, IEnumerable existingLocations) + { + if (rawNames == null || rawNames.None()) { return string.Empty; } + if (existingLocations != null) + { + var unusedNames = rawNames.Where(name => !existingLocations.Any(l => l.DisplayName.Value == name)).ToList(); if (unusedNames.Count > 0) { return unusedNames[rand.Next() % unusedNames.Count]; } } - return names[rand.Next() % names.Length]; + return rawNames[rand.Next() % rawNames.Length]; } public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func predicate = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index 8d9e03673..94377cf34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -55,7 +55,7 @@ namespace Barotrauma /// public readonly bool RequireHuntingGrounds; - public Requirement(XElement element, LocationTypeChange change) + public Requirement(ContentXElement element, LocationTypeChange change) { RequiredLocations = element.GetAttributeIdentifierArray("requiredlocations", element.GetAttributeIdentifierArray("requiredadjacentlocations", Array.Empty())).ToImmutableArray(); RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 0); @@ -80,13 +80,15 @@ namespace Barotrauma { DebugConsole.AddWarning( $"Invalid location type change in location type \"{change.CurrentType}\". " + - "Probability is configured to increase when near some other type of location, but the RequiredLocations attribute is not set."); + "Probability is configured to increase when near some other type of location, but the RequiredLocations attribute is not set.", + element.ContentPackage); } if (Probability >= 1.0f) { DebugConsole.AddWarning( $"Invalid location type change in location type \"{change.CurrentType}\". " + - "Probability is configured to increase when near some other type of location, but the base probability is already 100%"); + "Probability is configured to increase when near some other type of location, but the base probability is already 100%", + element.ContentPackage); } } } @@ -173,7 +175,7 @@ namespace Barotrauma public readonly Point RequiredDurationRange; - public LocationTypeChange(Identifier currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f) + public LocationTypeChange(Identifier currentType, ContentXElement element, bool requireChangeMessages, float defaultProbability = 0.0f) { CurrentType = currentType; ChangeToType = element.GetAttributeIdentifier("type", element.GetAttributeIdentifier("to", "")); @@ -190,13 +192,13 @@ namespace Barotrauma CooldownAfterChange = Math.Max(element.GetAttributeInt("cooldownafterchange", 0), 0); //backwards compatibility - if (element.Attribute("requiredlocations") != null) + if (element.GetAttribute("requiredlocations") != null) { Requirements.Add(new Requirement(element, this)); } //backwards compatibility - if (element.Attribute("requiredduration") != null) + if (element.GetAttribute("requiredduration") != null) { RequiredDurationRange = new Point(element.GetAttributeInt("requiredduration", 0)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index bbb70a021..1934e9c69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -262,9 +262,9 @@ namespace Barotrauma foreach (var endLocation in EndLocations) { if (endLocation.Type?.ForceLocationName != null && - !endLocation.Type.ForceLocationName.IsNullOrEmpty()) + !endLocation.Type.ForceLocationName.IsEmpty) { - endLocation.ForceName(endLocation.Type.ForceLocationName.Value); + endLocation.ForceName(endLocation.Type.ForceLocationName); } } @@ -1005,10 +1005,10 @@ namespace Barotrauma CurrentLocation.CreateStores(); OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation)); - if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } }) + if (GameMain.GameSession is { Campaign.CampaignMetadata: { } metadata }) { metadata.SetValue("campaign.location.id".ToIdentifier(), CurrentLocationIndex); - metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.Name); + metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.NameIdentifier.Value); metadata.SetValue("campaign.location.biome".ToIdentifier(), CurrentLocation.Biome?.Identifier ?? "null".ToIdentifier()); metadata.SetValue("campaign.location.type".ToIdentifier(), CurrentLocation.Type?.Identifier ?? "null".ToIdentifier()); } @@ -1077,7 +1077,7 @@ namespace Barotrauma if (SelectedConnection?.Locked ?? false) { string errorMsg = - $"A locked connection was selected ({SelectedConnection.Locations[0].Name} -> {SelectedConnection.Locations[1].Name}." + + $"A locked connection was selected ({SelectedConnection.Locations[0].DisplayName} -> {SelectedConnection.Locations[1].DisplayName}." + $" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); @@ -1093,7 +1093,7 @@ namespace Barotrauma { if (!Locations.Contains(location)) { - string errorMsg = "Failed to select a location. " + (location?.Name ?? "null") + " not found in the map."; + string errorMsg = $"Failed to select a location. {location?.DisplayName ?? "null"} not found in the map."; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Map.SelectLocation:LocationNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; @@ -1301,11 +1301,11 @@ namespace Barotrauma private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change) { - string prevName = location.Name; + LocalizedString prevName = location.DisplayName; if (!LocationType.Prefabs.TryGet(change.ChangeToType, out var newType)) { - DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found."); + DebugConsole.ThrowError($"Failed to change the type of the location \"{location.DisplayName}\". Location type \"{change.ChangeToType}\" not found."); return false; } @@ -1372,7 +1372,7 @@ namespace Barotrauma } - partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change); + partial void ChangeLocationTypeProjSpecific(Location location, LocalizedString prevName, LocationTypeChange change); partial void ClearAnimQueue(); @@ -1498,7 +1498,7 @@ namespace Barotrauma } Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty); - string prevLocationName = location.Name; + LocalizedString prevLocationName = location.DisplayName; LocationType prevLocationType = location.Type; LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First(); location.ChangeType(campaign, newLocationType); @@ -1619,7 +1619,7 @@ namespace Barotrauma //this should not be possible, you can't enter non-outpost locations (= natural formations) if (CurrentLocation != null && !CurrentLocation.Type.HasOutpost && SelectedConnection == null) { - DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.Name}). Loading the first adjacent connection..."); + DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.DisplayName}). Loading the first adjacent connection..."); SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 39f35865a..657c3c83f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -576,15 +576,10 @@ namespace Barotrauma base.Remove(); MapEntityList.Remove(this); - #if CLIENT Submarine.ForceRemoveFromVisibleEntities(this); - if (SelectedList.Contains(this)) - { - SelectedList = SelectedList.Where(e => e != this).ToHashSet(); - } + SelectedList.Remove(this); #endif - if (aiTarget != null) { aiTarget.Remove(); @@ -686,6 +681,9 @@ namespace Barotrauma Move(-relative * 2.0f); } + public virtual Quad2D GetTransformedQuad() + => Quad2D.FromSubmarineRectangle(rect); + public static List LoadAll(Submarine submarine, XElement parentElement, string filePath, int idOffset) { IdRemap idRemap = new IdRemap(parentElement, idOffset); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/ExtraSubmarineInfo.cs similarity index 54% rename from Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs rename to Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/ExtraSubmarineInfo.cs index feec0ea3d..c7b6155c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/ExtraSubmarineInfo.cs @@ -3,13 +3,11 @@ using System.Xml.Linq; namespace Barotrauma { - class BeaconStationInfo : ISerializableEntity + abstract class ExtraSubmarineInfo : ISerializableEntity { - [Serialize(true, IsPropertySaveable.Yes), Editable] - public bool AllowDamagedWalls { get; set; } + public string Name { get; protected set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] - public bool AllowDisconnectedWires { get; set; } + public Dictionary SerializableProperties { get; protected set; } [Serialize(0.0f, IsPropertySaveable.Yes), Editable] public float MinLevelDifficulty { get; set; } @@ -17,26 +15,19 @@ 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; } - - public BeaconStationInfo(SubmarineInfo submarineInfo, XElement element) + public ExtraSubmarineInfo(SubmarineInfo submarineInfo, XElement element) { - Name = $"BeaconStationInfo ({submarineInfo.Name})"; + Name = $"{nameof(ExtraSubmarineInfo)} ({submarineInfo.Name})"; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } - public BeaconStationInfo(SubmarineInfo submarineInfo) + public ExtraSubmarineInfo(SubmarineInfo submarineInfo) { - Name = $"BeaconStationInfo ({submarineInfo.Name})"; + Name = $"{nameof(ExtraSubmarineInfo)} ({submarineInfo.Name})"; SerializableProperties = SerializableProperty.DeserializeProperties(this); } - public BeaconStationInfo(BeaconStationInfo original) + public ExtraSubmarineInfo(ExtraSubmarineInfo original) { Name = original.Name; SerializableProperties = new Dictionary(); @@ -55,4 +46,43 @@ namespace Barotrauma SerializableProperty.SerializeProperties(this, element); } } + + class BeaconStationInfo : ExtraSubmarineInfo + { + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool AllowDamagedWalls { get; set; } + + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool AllowDisconnectedWires { get; set; } + + [Serialize(Level.PlacementType.Bottom, IsPropertySaveable.Yes), Editable] + public Level.PlacementType Placement { get; set; } + + public BeaconStationInfo(SubmarineInfo submarineInfo, XElement element) : base(submarineInfo, element) + { + Name = $"{nameof(BeaconStationInfo)} ({submarineInfo.Name})"; + } + + public BeaconStationInfo(SubmarineInfo submarineInfo) : base(submarineInfo) + { + Name = $"{nameof(BeaconStationInfo)} ({submarineInfo.Name})"; + } + + public BeaconStationInfo(BeaconStationInfo original) : base(original) { } + } + + class WreckInfo : ExtraSubmarineInfo + { + public WreckInfo(SubmarineInfo submarineInfo, XElement element) : base(submarineInfo, element) + { + Name = $"{nameof(WreckInfo)} ({submarineInfo.Name})"; + } + + public WreckInfo(SubmarineInfo submarineInfo) : base(submarineInfo) + { + Name = $"{nameof(WreckInfo)} ({submarineInfo.Name})"; + } + + public WreckInfo(WreckInfo original) : base(original) { } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index b55acd04e..7bb55ccd6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -249,7 +249,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Error in outpost generation parameters \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); + DebugConsole.ThrowError($"Error in outpost generation parameters \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type.", contentPackage: element.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 2d4ce70e3..afbdb5271 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -111,7 +111,7 @@ namespace Barotrauma set; } - public bool IsHorizontal { get; private set; } + public bool IsHorizontal { get; } public int SectionCount { @@ -240,6 +240,25 @@ namespace Barotrauma } } + protected float rotationRad = 0f; + [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)] + public float Rotation + { + get => MathHelper.ToDegrees(rotationRad); + set + { + rotationRad = MathHelper.WrapAngle(MathHelper.ToRadians(value)); + if (StairDirection != Direction.None) + { + CreateStairBodies(); + } + else if (Prefab.Body) + { + CreateSections(); + UpdateSections(); + } + } + } protected Vector2 textureScale = Vector2.One; @@ -336,9 +355,18 @@ namespace Barotrauma { get { - float rotation = MathHelper.ToRadians(Prefab.BodyRotation); - if (FlippedX) rotation = -MathHelper.Pi - rotation; - if (FlippedY) rotation = -rotation; + float rotation = MathHelper.ToRadians(Prefab.BodyRotation) + this.rotationRad; + if (IsHorizontal) + { + if (FlippedX) { rotation = -MathHelper.Pi - rotation; } + if (FlippedY) { rotation = -rotation; } + } + else + { + if (FlippedX) { rotation = -rotation; } + if (FlippedY) { rotation = -MathHelper.Pi -rotation; } + } + rotation = MathHelper.WrapAngle(rotation); return rotation; } } @@ -350,6 +378,10 @@ namespace Barotrauma get { Vector2 bodyOffset = Prefab.BodyOffset; + if (rotationRad != 0f) + { + bodyOffset = MathUtils.RotatePoint(bodyOffset, -rotationRad); + } if (FlippedX) { bodyOffset.X = -bodyOffset.X; } if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; } return bodyOffset; @@ -567,9 +599,14 @@ namespace Barotrauma Body newBody = GameMain.World.CreateRectangle(bodyWidth, bodyHeight, 1.5f); + var rotationWithFlip = FlippedX ^ FlippedY ? -rotationRad : rotationRad; + newBody.BodyType = BodyType.Static; - Vector2 stairPos = new Vector2(Position.X, rect.Y - rect.Height + stairHeight / 2.0f); - newBody.Rotation = (StairDirection == Direction.Right) ? stairAngle : -stairAngle; + Vector2 stairRectHeightDiff = new Vector2(0f, stairHeight / 2.0f - rect.Height / 2.0f); + stairRectHeightDiff = MathUtils.RotatePoint(stairRectHeightDiff, -rotationWithFlip); + if (FlippedY) { stairRectHeightDiff = -stairRectHeightDiff; } + Vector2 stairPos = new Vector2(Position.X, rect.Y - rect.Height / 2.0f) + stairRectHeightDiff; + newBody.Rotation = ((StairDirection == Direction.Right) ? stairAngle : -stairAngle) - rotationWithFlip; newBody.CollisionCategories = Physics.CollisionStairs; newBody.Friction = 0.8f; newBody.UserData = this; @@ -696,16 +733,11 @@ namespace Barotrauma } } - private static Vector2[] CalculateExtremes(Rectangle sectionRect) - { - 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); - - return corners; - } + public override Quad2D GetTransformedQuad() + => Quad2D.FromSubmarineRectangle(rect).Rotated( + FlippedX != FlippedY + ? rotationRad + : -rotationRad); /// /// Checks if there's a structure items can be attached to at the given position and returns it. @@ -727,8 +759,6 @@ namespace Barotrauma public override bool IsMouseOn(Vector2 position) { - if (!base.IsMouseOn(position)) { return false; } - if (StairDirection == Direction.None) { Vector2 rectSize = rect.Size.ToVector2(); @@ -745,14 +775,19 @@ namespace Barotrauma } else { + Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget( + position, + WorldRect.Location.ToVector2() + WorldRect.Size.ToVector2().FlipY() * 0.5f, + BodyRotation); + if (!Submarine.RectContains(WorldRect, position)) { return false; } if (StairDirection == Direction.Left) { - return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y), new Vector2(WorldRect.Right, WorldRect.Y - WorldRect.Height), position) < 1600.0f; + return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y), new Vector2(WorldRect.Right, WorldRect.Y - WorldRect.Height), transformedMousePos) < 1600.0f; } else { - return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y - rect.Height), new Vector2(WorldRect.Right, WorldRect.Y), position) < 1600.0f; + return MathUtils.LineToPointDistanceSquared(new Vector2(WorldRect.X, WorldRect.Y - rect.Height), new Vector2(WorldRect.Right, WorldRect.Y), transformedMousePos) < 1600.0f; } } } @@ -883,6 +918,12 @@ namespace Barotrauma return Sections[sectionIndex].damage >= MaxHealth * LeakThreshold; } + public bool SectionIsLeakingFromOutside(int sectionIndex) + { + if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return false; } + return SectionIsLeaking(sectionIndex) && !Sections[sectionIndex].gap.IsRoomToRoom; + } + public int SectionLength(int sectionIndex) { if (sectionIndex < 0 || sectionIndex >= Sections.Length) return 0; @@ -934,11 +975,19 @@ namespace Barotrauma for (int i = 1; i <= particleAmount; i++) { var worldRect = section.WorldRect; + var directionUnitX = MathUtils.RotatedUnitXRadians(BodyRotation); + var directionUnitY = directionUnitX.YX().FlipX(); Vector2 particlePos = new Vector2( - Rand.Range(worldRect.X, worldRect.Right + 1), - Rand.Range(worldRect.Y - worldRect.Height, worldRect.Y + 1)); + Rand.Range(0, worldRect.Width + 1), + Rand.Range(-worldRect.Height, 1)); + particlePos -= worldRect.Size.ToVector2().FlipY() * 0.5f; - var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle, particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); + var particlePosFinal = SectionPosition(sectionIndex, world: true); + particlePosFinal += particlePos.X * directionUnitX + particlePos.Y * directionUnitY; + + var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle, + position: particlePosFinal, + velocity: Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); if (particle == null) break; } } @@ -960,14 +1009,23 @@ namespace Barotrauma //if the sub has been flipped horizontally, the first section may be smaller than wallSectionSize //and we need to adjust the position accordingly - if (Sections[0].rect.Width < WallSectionSize) + if (IsHorizontal) { - displayPos.X += WallSectionSize - Sections[0].rect.Width; + if (Sections[0].rect.Width < WallSectionSize) + { + displayPos += DirectionUnit * (WallSectionSize - Sections[0].rect.Width); + } + } + else + { + if (Sections[0].rect.Height < WallSectionSize) + { + displayPos += DirectionUnit * (WallSectionSize - Sections[0].rect.Height); + } } - int index = IsHorizontal ? - (int)Math.Floor((displayPos.X - rect.X) / WallSectionSize) : - (int)Math.Floor((rect.Y - displayPos.Y) / WallSectionSize); + var leftmostPos = Position - DirectionUnit * (IsHorizontal ? Rect.Width : Rect.Height) * 0.5f; + int index = (int)Math.Floor(Vector2.Dot(DirectionUnit, displayPos - leftmostPos) / WallSectionSize); if (clamp) { @@ -987,6 +1045,17 @@ namespace Barotrauma return Sections[sectionIndex].damage; } + protected Vector2 DirectionUnit + { + get + { + var rotation = IsHorizontal ? -BodyRotation : -MathHelper.PiOver2 - BodyRotation; + if (IsHorizontal && FlippedX) { rotation += MathF.PI; } + if (!IsHorizontal && FlippedY) { rotation += MathF.PI; } + return MathUtils.RotatedUnitXRadians(rotation); + } + } + public Vector2 SectionPosition(int sectionIndex, bool world = false) { if (sectionIndex < 0 || sectionIndex >= Sections.Length) @@ -994,7 +1063,7 @@ namespace Barotrauma return Vector2.Zero; } - if (Prefab.BodyRotation == 0.0f) + if (MathUtils.NearlyEqual(BodyRotation, 0f)) { Vector2 sectionPos = new Vector2( Sections[sectionIndex].rect.X + Sections[sectionIndex].rect.Width / 2.0f, @@ -1017,15 +1086,10 @@ namespace Barotrauma else { diffFromCenter = ((sectionRect.Y - sectionRect.Height / 2) - (rect.Y - rect.Height / 2)) / (float)rect.Height * BodyHeight; - } - if (FlippedX) - { diffFromCenter = -diffFromCenter; } - Vector2 sectionPos = Position + new Vector2( - (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), - (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter; + Vector2 sectionPos = Position + DirectionUnit * diffFromCenter; if (world && Submarine != null) { @@ -1035,13 +1099,22 @@ namespace Barotrauma } } - public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false) + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = false) { if (Submarine != null && Submarine.GodMode) { return new AttackResult(0.0f, null); } if (!Prefab.Body || Prefab.Platform || Indestructible) { return new AttackResult(0.0f, null); } Vector2 transformedPos = worldPosition; - if (Submarine != null) transformedPos -= Submarine.Position; + if (Submarine != null) { transformedPos -= Submarine.Position; } + + if (!MathUtils.NearlyEqual(BodyRotation, 0f)) + { + var center = Rect.Location.ToVector2() + Rect.Size.ToVector2().FlipY() * 0.5f; + var rotation = BodyRotation; + if (IsHorizontal && FlippedX) { rotation += MathF.PI; } + if (!IsHorizontal && FlippedY) { rotation += MathF.PI; } + transformedPos = MathUtils.RotatePointAroundTarget(transformedPos, center, rotation); + } float damageAmount = 0.0f; for (int i = 0; i < SectionCount; i++) @@ -1143,6 +1216,7 @@ namespace Barotrauma gapRect.Y = (gapRect.Y - gapRect.Height / 2) + (int)(BodyHeight / 2 + BodyOffset.Y * scale); gapRect.Height = (int)BodyHeight; } + if (FlippedX) { diffFromCenter = -diffFromCenter; } } else { @@ -1153,8 +1227,8 @@ namespace Barotrauma gapRect.Width = (int)BodyWidth; } if (BodyHeight > 0.0f) { gapRect.Height = (int)(BodyHeight * (gapRect.Height / (float)this.rect.Height)); } + if (FlippedY) { diffFromCenter = -diffFromCenter; } } - if (FlippedX) { diffFromCenter = -diffFromCenter; } if (Math.Abs(BodyRotation) > 0.01f) { @@ -1170,14 +1244,26 @@ namespace Barotrauma gapRect.Width += 20; gapRect.Height += 20; - bool horizontalGap = !IsHorizontal; + bool rotatedEnoughToChangeOrientation = (MathUtils.WrapAngleTwoPi(rotationRad - MathHelper.PiOver4) % MathHelper.Pi < MathHelper.PiOver2); + if (rotatedEnoughToChangeOrientation) + { + var center = gapRect.Location + gapRect.Size.FlipY() / new Point(2); + var topLeft = gapRect.Location; + var diff = topLeft - center; + diff = diff.FlipY().YX().FlipY(); + var newTopLeft = diff + center; + gapRect = new Rectangle(newTopLeft, gapRect.Size.YX()); + } + bool horizontalGap = rotatedEnoughToChangeOrientation + ? IsHorizontal + : !IsHorizontal; bool diagonalGap = false; - if (Prefab.BodyRotation != 0.0f) + if (!MathUtils.NearlyEqual(BodyRotation, 0f)) { //rotation within a 90 deg sector (e.g. 100 -> 10, 190 -> 10, -10 -> 80) float sectorizedRotation = MathUtils.WrapAngleTwoPi(BodyRotation) % MathHelper.PiOver2; //diagonal if 30 < angle < 60 - diagonalGap = sectorizedRotation > MathHelper.Pi / 6 && sectorizedRotation < MathHelper.Pi / 3; + diagonalGap = sectorizedRotation is > MathHelper.Pi / 6 and < MathHelper.Pi / 3; //gaps on the lower half of a diagonal wall are horizontal, ones on the upper half are vertical if (diagonalGap) { @@ -1230,8 +1316,8 @@ namespace Barotrauma { if (damageDiff < 0.0f) { - attacker.Info?.IncreaseSkillLevel("mechanical".ToIdentifier(), - -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f)); + attacker.Info?.ApplySkillGain(Barotrauma.Tags.MechanicalSkill, + -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage); } } } @@ -1337,7 +1423,7 @@ namespace Barotrauma { hasHoles = true; - if (!mergedSections.Any()) continue; + if (!mergedSections.Any()) { continue; } var mergedRect = GenerateMergedRect(mergedSections); mergedSections.Clear(); CreateRectBody(mergedRect, createConvexHull: true); @@ -1373,18 +1459,17 @@ namespace Barotrauma diffFromCenter = (rect.Center.X - this.rect.Center.X) / (float)this.rect.Width * BodyWidth; if (BodyWidth > 0.0f) rect.Width = Math.Max((int)Math.Round(BodyWidth * (rect.Width / (float)this.rect.Width)), 1); if (BodyHeight > 0.0f) rect.Height = (int)BodyHeight; + if (FlippedX) { diffFromCenter = -diffFromCenter; } } else { diffFromCenter = ((rect.Y - rect.Height / 2) - (this.rect.Y - this.rect.Height / 2)) / (float)this.rect.Height * BodyHeight; if (BodyWidth > 0.0f) rect.Width = (int)BodyWidth; if (BodyHeight > 0.0f) rect.Height = Math.Max((int)Math.Round(BodyHeight * (rect.Height / (float)this.rect.Height)), 1); + if (FlippedY) { diffFromCenter = -diffFromCenter; } } - if (FlippedX) { diffFromCenter = -diffFromCenter; } - Vector2 bodyOffset = ConvertUnits.ToSimUnits(Prefab.BodyOffset) * scale; - if (FlippedX) { bodyOffset.X = -bodyOffset.X; } - if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; } + Vector2 bodyOffset = ConvertUnits.ToSimUnits(BodyOffset) * scale; Body newBody = GameMain.World.CreateRectangle( ConvertUnits.ToSimUnits(rect.Width), @@ -1398,7 +1483,7 @@ namespace Barotrauma newBody.UserData = this; Vector2 structureCenter = ConvertUnits.ToSimUnits(Position); - if (BodyRotation != 0.0f) + if (!MathUtils.NearlyEqual(BodyRotation, 0f)) { Vector2 pos = structureCenter + bodyOffset + new Vector2( (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 941ddab07..a089421ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -67,6 +67,9 @@ namespace Barotrauma [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(true, IsPropertySaveable.No, description: "Can the structure be rotated in the submarine editor?")] + public bool AllowRotatingInEditor { get; set; } + [Serialize(0.0f, IsPropertySaveable.No)] public float MinHealth { get; private set; } @@ -297,14 +300,16 @@ namespace Barotrauma if (Identifier == Identifier.Empty) { DebugConsole.ThrowError( - "Structure prefab \"" + Name + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); + "Structure prefab \"" + Name.Value + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading.", + contentPackage: ContentPackage); } #if DEBUG if (!Category.HasFlag(MapEntityCategory.Legacy) && !HideInMenus) { if (!string.IsNullOrEmpty(OriginalName)) { - DebugConsole.AddWarning($"Structure \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages."); + DebugConsole.AddWarning($"Structure \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages.", + ContentPackage); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index fad619258..edce76390 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -505,51 +505,58 @@ namespace Barotrauma minWidth += padding; minHeight += padding; - Vector2 limits = GetHorizontalLimits(spawnPos, minWidth, minHeight, 0); - if (verticalMoveDir != 0) + int iterations = 0; + const int maxIterations = 5; + do { - verticalMoveDir = Math.Sign(verticalMoveDir); - //do a raycast towards the top/bottom of the level depending on direction - Vector2 potentialPos = new Vector2(spawnPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0); - - //3 raycasts (left, middle and right side of the sub, so we don't accidentally raycast up a passage too narrow for the sub) - for (int x = -1; x <= 1; x++) + Vector2 potentialPos = spawnPos; + if (verticalMoveDir != 0) { - Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x; - if (PickBody( - ConvertUnits.ToSimUnits(spawnPos + xOffset), - ConvertUnits.ToSimUnits(potentialPos + xOffset), - collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) + verticalMoveDir = Math.Sign(verticalMoveDir); + //do a raycast towards the top/bottom of the level depending on direction + Vector2 rayEnd = new Vector2(potentialPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0); + + Vector2 closestPickedPos = rayEnd; + //multiple raycast across the width of the sub (so we don't accidentally raycast up a passage too narrow for the sub) + for (float x = -1; x <= 1; x += 0.2f) { - int offsetFromWall = 10 * -verticalMoveDir; - //if the raycast hit a wall, attempt to place the spawnpos there - if (verticalMoveDir > 0) + Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x; + xOffset.X += subDockingPortOffset; + if (PickBody( + ConvertUnits.ToSimUnits(potentialPos + xOffset), + ConvertUnits.ToSimUnits(rayEnd + xOffset), + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall, + customPredicate: (Fixture f) => + { + return f.UserData is not VoronoiCell { IsDestructible: true }; + }) != null) { - potentialPos.Y = Math.Min(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall); - } - else - { - potentialPos.Y = Math.Max(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall); + //if the raycast hit a wall, attempt to place the spawnpos there + int offsetFromWall = 10 * -verticalMoveDir; + float pickedPos = ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall; + closestPickedPos.Y = + verticalMoveDir > 0 ? + Math.Min(closestPickedPos.Y, pickedPos) : + Math.Max(closestPickedPos.Y, pickedPos); } } + potentialPos.Y = closestPickedPos.Y; } - //step away from the top/bottom of the level, or from whatever wall the raycast hit, - //until we found a spot where there's enough room to place the sub - float dist = Math.Abs(potentialPos.Y - spawnPos.Y); - for (float d = dist; d > 0; d -= 100.0f) + Vector2 limits = GetHorizontalLimits(new Vector2(potentialPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir)), + maxHorizontalMoveAmount: minWidth, minHeight, verticalMoveDir, padding); + if (limits.Y - limits.X >= minWidth) { - float y = spawnPos.Y + verticalMoveDir * d; - limits = GetHorizontalLimits(new Vector2(spawnPos.X, y), minWidth, minHeight, verticalMoveDir); - if (limits.Y - limits.X > minWidth) - { - spawnPos = new Vector2(spawnPos.X, y - (dockedBorders.Height * 0.5f * verticalMoveDir)); - break; - } - } - } + Vector2 newSpawnPos = new Vector2(spawnPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir)); + bool couldMoveInVerticalMoveDir = Math.Sign(newSpawnPos.Y - spawnPos.Y) == Math.Sign(verticalMoveDir); + if (!couldMoveInVerticalMoveDir) { break; } + spawnPos = ClampToHorizontalLimits(newSpawnPos, limits); + } - static Vector2 GetHorizontalLimits(Vector2 spawnPos, float minWidth, float minHeight, int verticalMoveDir) + iterations++; + } while (iterations < maxIterations); + + Vector2 GetHorizontalLimits(Vector2 spawnPos, float maxHorizontalMoveAmount, float minHeight, int verticalMoveDir, int padding) { Vector2 refPos = spawnPos - Vector2.UnitY * minHeight * 0.5f * Math.Sign(verticalMoveDir); @@ -580,34 +587,44 @@ namespace Barotrauma if (Math.Abs(ruin.Area.Center.Y - refPos.Y) > (minHeight + ruin.Area.Height) * 0.5f) { continue; } if (ruin.Area.Center.X < refPos.X) { - minX = Math.Max(minX, ruin.Area.Right + 100.0f); + minX = Math.Max(minX, ruin.Area.Right + padding); } else { - maxX = Math.Min(maxX, ruin.Area.X - 100.0f); + maxX = Math.Min(maxX, ruin.Area.X - padding); } } - return new Vector2(Math.Max(minX, spawnPos.X - minWidth), Math.Min(maxX, spawnPos.X + minWidth)); + + minX += subDockingPortOffset; + maxX += subDockingPortOffset; + + return new Vector2( + Math.Max(Math.Max(minX, spawnPos.X - maxHorizontalMoveAmount - padding), 0), + Math.Min(Math.Min(maxX, spawnPos.X + maxHorizontalMoveAmount + padding), Level.Loaded.Size.X)); } - if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X) + Vector2 ClampToHorizontalLimits(Vector2 spawnPos, Vector2 limits) { - //no walls found at either side, just use the initial spawnpos and hope for the best - } - else if (limits.X < 0) - { - //no wall found at the left side, spawn to the left from the right-side wall - spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset; - } - else if (limits.Y > Level.Loaded.Size.X) - { - //no wall found at right side, spawn to the right from the left-side wall - spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset; - } - else - { - //walls found at both sides, use their midpoint - spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset; + if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X) + { + //no walls found at either side, just use the initial spawnpos and hope for the best + } + else if (limits.X < 0) + { + //no wall found at the left side, spawn to the left from the right-side wall + spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset; + } + else if (limits.Y > Level.Loaded.Size.X) + { + //no wall found at right side, spawn to the right from the left-side wall + spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset; + } + else + { + //walls found at both sides, use their midpoint + spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset; + } + return spawnPos; } spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - padding * 2); @@ -624,7 +641,7 @@ namespace Barotrauma //math/physics stuff ---------------------------------------------------- - public static Vector2 VectorToWorldGrid(Vector2 position, bool round = false) + public static Vector2 VectorToWorldGrid(Vector2 position, Submarine sub = null, bool round = false) { if (round) { @@ -636,6 +653,12 @@ namespace Barotrauma position.X = MathF.Floor(position.X / GridSize.X) * GridSize.X; position.Y = MathF.Ceiling(position.Y / GridSize.Y) * GridSize.Y; } + + if (sub != null) + { + position.X += sub.Position.X % GridSize.X; + position.Y += sub.Position.Y % GridSize.Y; + } return position; } @@ -923,11 +946,17 @@ namespace Barotrauma return true; } + /// - /// check visibility between two points (in sim units) + /// Check visibility between two points (in sim units). /// - /// a physics body that was between the points (or null) - public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true) + /// + + /// Should plants' branches be ignored? + /// If the predicate returns false, the fixture is ignored even if it would normally block visibility. + /// A physics body that was between the points (or null) + public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true, + Predicate blocksVisibilityPredicate = null) { Body closestBody = null; float closestFraction = 1.0f; @@ -962,7 +991,10 @@ namespace Barotrauma if (sectionIndex > -1 && structure.SectionBodyDisabled(sectionIndex)) { return -1; } } } - + if (blocksVisibilityPredicate != null && !blocksVisibilityPredicate(fixture)) + { + return -1; + } if (fraction < closestFraction) { closestBody = fixture.Body; @@ -1840,6 +1872,7 @@ namespace Barotrauma FilePath = filePath, OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null, BeaconStationInfo = Info.BeaconStationInfo != null ? new BeaconStationInfo(Info.BeaconStationInfo) : null, + WreckInfo = Info.WreckInfo != null ? new WreckInfo(Info.WreckInfo) : null, Name = Path.GetFileNameWithoutExtension(filePath) }; #if CLIENT @@ -1878,12 +1911,11 @@ namespace Barotrauma Unloading = true; try { - #if CLIENT RoundSound.RemoveAllRoundSounds(); GameMain.LightManager?.ClearLights(); + depthSortedDamageable.Clear(); #endif - var _loaded = new List(loaded); foreach (Submarine sub in _loaded) { @@ -1918,9 +1950,11 @@ namespace Barotrauma Ragdoll.RemoveAll(); PhysicsBody.RemoveAll(); + StatusEffect.StopAll(); GameMain.World = null; Powered.Grids.Clear(); + Powered.ChangedConnections.Clear(); GC.Collect(); @@ -1940,6 +1974,7 @@ namespace Barotrauma outdoorNodes?.Clear(); outdoorNodes = null; + obstructedNodes.Clear(); GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged?.TryDeregister(upgradeEventIdentifier); @@ -1951,11 +1986,17 @@ namespace Barotrauma visibleEntities = null; + bodyDist.Clear(); + bodies.Clear(); + if (MainSub == this) { MainSub = null; } if (MainSubs[1] == this) { MainSubs[1] = null; } ConnectedDockingPorts?.Clear(); + Powered.ChangedConnections.Clear(); + Powered.Grids.Clear(); + loaded.Remove(this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index e0d060c04..c4948fc15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -169,7 +169,12 @@ namespace Barotrauma bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None; Rectangle rect = wall.Rect; - SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider); + + var transformedQuad = wall.GetTransformedQuad(); + AddPointToExtents(transformedQuad.A, hasCollider: hasCollider); + AddPointToExtents(transformedQuad.B, hasCollider: hasCollider); + AddPointToExtents(transformedQuad.C, hasCollider: hasCollider); + AddPointToExtents(transformedQuad.D, hasCollider: hasCollider); if (hasCollider) { farseerBody.CreateRectangle( @@ -188,7 +193,8 @@ namespace Barotrauma if (hull.Submarine != submarine || hull.IdFreed) { continue; } Rectangle rect = hull.Rect; - SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider: true); + AddPointToExtents(new Vector2(rect.X, rect.Y - rect.Height), hasCollider: true); + AddPointToExtents(new Vector2(rect.Right, rect.Y), hasCollider: true); farseerBody.CreateRectangle( ConvertUnits.ToSimUnits(rect.Width), @@ -221,33 +227,42 @@ namespace Barotrauma float simWidth = ConvertUnits.ToSimUnits(width); float simHeight = ConvertUnits.ToSimUnits(height); + if (radius > 0f || (width > 0f && height > 0f)) + { + var transformedQuad = item.GetTransformedQuad(); + AddPointToExtents(transformedQuad.A, hasCollider: true); + AddPointToExtents(transformedQuad.B, hasCollider: true); + AddPointToExtents(transformedQuad.C, hasCollider: true); + AddPointToExtents(transformedQuad.D, hasCollider: true); + } + if (width > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos, collisionCategory, collidesWith)); - SetExtents(item.Position - new Vector2(width, height) / 2, item.Position + new Vector2(width, height) / 2, hasCollider: true); + AddPointToExtents(item.Position - new Vector2(width, height) / 2, hasCollider: true); + AddPointToExtents(item.Position + new Vector2(width, height) / 2, hasCollider: true); } else if (radius > 0.0f && width > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos, collisionCategory, collidesWith)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith)); - SetExtents(item.Position - new Vector2(width / 2 + radius, height / 2), item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true); + AddPointToExtents(item.Position - new Vector2(width / 2 + radius, height / 2), hasCollider: true); + AddPointToExtents(item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true); } else if (radius > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos, collisionCategory, collidesWith)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith)); - SetExtents(item.Position - new Vector2(width / 2, height / 2 + radius), item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true); + AddPointToExtents(item.Position - new Vector2(width / 2, height / 2 + radius), hasCollider: true); + AddPointToExtents(item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true); } else if (radius > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos, collisionCategory, collidesWith)); - visibleMinExtents.X = Math.Min(item.Position.X - radius, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y); - SetExtents(item.Position - new Vector2(radius, radius), item.Position + new Vector2(radius, radius), hasCollider: true); + AddPointToExtents(item.Position - new Vector2(radius, radius), hasCollider: true); + AddPointToExtents(item.Position + new Vector2(radius, radius), hasCollider: true); } item.StaticFixtures.ForEach(f => f.UserData = item); } @@ -268,18 +283,18 @@ namespace Barotrauma Body = new PhysicsBody(farseerBody); - void SetExtents(Vector2 min, Vector2 max, bool hasCollider) + void AddPointToExtents(Vector2 point, bool hasCollider) { - visibleMinExtents.X = Math.Min(min.X, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(min.Y, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(max.X, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(max.Y, visibleMaxExtents.Y); + visibleMinExtents.X = Math.Min(point.X, visibleMinExtents.X); + visibleMinExtents.Y = Math.Min(point.Y, visibleMinExtents.Y); + visibleMaxExtents.X = Math.Max(point.X, visibleMaxExtents.X); + visibleMaxExtents.Y = Math.Max(point.Y, visibleMaxExtents.Y); if (hasCollider) { - minExtents.X = Math.Min(min.X, minExtents.X); - minExtents.Y = Math.Min(min.Y, minExtents.Y); - maxExtents.X = Math.Max(max.X, maxExtents.X); - maxExtents.Y = Math.Max(max.Y, maxExtents.Y); + minExtents.X = Math.Min(point.X, minExtents.X); + minExtents.Y = Math.Min(point.Y, minExtents.Y); + maxExtents.X = Math.Max(point.X, maxExtents.X); + maxExtents.Y = Math.Max(point.Y, maxExtents.Y); } } } @@ -800,7 +815,10 @@ namespace Barotrauma float damageAmount = contactDot * Body.Mass / limb.character.Mass; limb.character.LastDamageSource = submarine; limb.character.DamageLimb(ConvertUnits.ToDisplayUnits(collision.ImpactPos), limb, - AfflictionPrefab.ImpactDamage.Instantiate(damageAmount).ToEnumerable(), 0.0f, true, 0.0f); + AfflictionPrefab.ImpactDamage.Instantiate(damageAmount).ToEnumerable(), + stun: 0.0f, + playSound: true, + attackImpulse: Vector2.Zero); if (limb.character.IsDead) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index a83578cfa..a71c4f3fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -122,6 +122,9 @@ namespace Barotrauma public OutpostModuleInfo OutpostModuleInfo { get; set; } public BeaconStationInfo BeaconStationInfo { get; set; } + public WreckInfo WreckInfo { get; set; } + + public ExtraSubmarineInfo GetExtraSubmarineInfo => BeaconStationInfo ?? WreckInfo as ExtraSubmarineInfo; public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule; @@ -320,10 +323,14 @@ namespace Barotrauma { OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo); } - if (original.BeaconStationInfo != null) + else if (original.BeaconStationInfo != null) { BeaconStationInfo = new BeaconStationInfo(original.BeaconStationInfo); } + else if (original.WreckInfo != null) + { + WreckInfo = new WreckInfo(original.WreckInfo); + } #if CLIENT PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage) : null; #endif @@ -410,6 +417,10 @@ namespace Barotrauma { BeaconStationInfo = new BeaconStationInfo(this, SubmarineElement); } + else if (Type == SubmarineType.Wreck) + { + WreckInfo = new WreckInfo(this, SubmarineElement); + } } } @@ -589,6 +600,11 @@ namespace Barotrauma BeaconStationInfo.Save(newElement); BeaconStationInfo = new BeaconStationInfo(this, newElement); } + else if (Type == SubmarineType.Wreck) + { + WreckInfo.Save(newElement); + WreckInfo = new WreckInfo(this, newElement); + } XDocument doc = new XDocument(newElement); doc.Root.Add(new XAttribute("name", Name)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs index dbb4c9276..c19ff9637 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/BanList.cs @@ -14,6 +14,16 @@ namespace Barotrauma.Networking public readonly string Reason; public Option ExpirationTime; public readonly UInt32 UniqueIdentifier; + + public bool MatchesClient(Client client) + { + if (client == null) { return false; } + if (AddressOrAccountId.TryGet(out AccountId bannedAccountId) && client.AccountId.TryUnwrap(out AccountId? accountId)) + { + return bannedAccountId.Equals(accountId); + } + return false; + } } partial class BanList diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 4f8157b45..9f7dafe82 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -131,6 +131,7 @@ namespace Barotrauma.Networking catch (AggregateException aggregateException) { if (aggregateException.InnerException is OperationCanceledException) { return Option.None(); } + CheckPipeConnected(nameof(readStream), readStream); throw; } catch (OperationCanceledException) @@ -161,7 +162,18 @@ namespace Barotrauma.Networking { if (status is StatusEnum.Active && pipe is not { IsConnected: true }) { - throw new Exception($"{name} was disconnected unexpectedly"); + string exceptionMsg = $"{name} was disconnected unexpectedly."; +#if CLIENT + if (Process is { HasExited: true, ExitCode: var exitCode }) + { + exceptionMsg += $" Child process exit code was {(uint)exitCode:X8}."; + } + else if (Process is { HasExited: false }) + { + exceptionMsg += " Child process has not exited."; + } +#endif + throw new Exception(exceptionMsg); } } @@ -256,7 +268,11 @@ namespace Barotrauma.Networking { case ObjectDisposedException _: case System.IO.IOException _: - if (!HasShutDown) { throw; } + if (!HasShutDown) + { + CheckPipeConnected(nameof(writeStream), writeStream); + throw; + } break; default: throw; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index b3b47676e..34af27022 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -218,7 +218,7 @@ namespace Barotrauma.Networking msg.WriteUInt16((UInt16)PermittedConsoleCommands.Count); foreach (DebugConsole.Command command in PermittedConsoleCommands) { - msg.WriteString(command.names[0]); + msg.WriteIdentifier(command.Names[0]); } } } @@ -240,8 +240,8 @@ namespace Barotrauma.Networking UInt16 commandCount = inc.ReadUInt16(); for (int i = 0; i < commandCount; i++) { - string commandName = inc.ReadString(); - var consoleCommand = DebugConsole.Commands.Find(c => c.names.Contains(commandName)); + Identifier commandName = inc.ReadIdentifier(); + var consoleCommand = DebugConsole.Commands.Find(c => c.Names.Contains(commandName)); if (consoleCommand != null) { permittedCommands.Add(consoleCommand); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index a5067d7c6..1e45b7d94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -1,11 +1,10 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Steamworks.ServerList; namespace Barotrauma { @@ -111,7 +110,7 @@ namespace Barotrauma public void OnSpawned(Entity spawnedItem) { - if (!(spawnedItem is Item item)) { throw new ArgumentException($"The entity passed to ItemSpawnInfo.OnSpawned must be an Item (value was {spawnedItem?.ToString() ?? "null"})."); } + if (spawnedItem is not Item item) { throw new ArgumentException($"The entity passed to ItemSpawnInfo.OnSpawned must be an Item (value was {spawnedItem?.ToString() ?? "null"})."); } onSpawned?.Invoke(item); } } @@ -443,6 +442,7 @@ namespace Barotrauma CreateNetworkEventProjSpecific(new SpawnEntity(spawnedEntity)); } spawnInfo.OnSpawned(spawnedEntity); + GameMain.GameSession?.EventManager?.EntitySpawned(spawnedEntity); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index 0f33d9599..fd647fcab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -195,7 +195,7 @@ namespace Barotrauma.Networking int orderPriority = msg.ReadByte(); OrderTarget orderTargetPosition = null; Order.OrderTargetType orderTargetType = (Order.OrderTargetType)msg.ReadByte(); - int wallSectionIndex = 0; + int? wallSectionIndex = null; if (msg.ReadBoolean()) { float x = msg.ReadSingle(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 658fee626..b8e8965dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -252,6 +252,13 @@ namespace Barotrauma.Networking continue; } +#if CLIENT + foreach (var itemComponent in item.Components) + { + itemComponent.StopLoopingSound(); + } +#endif + //restore other items to full condition and recharge batteries item.Condition = item.MaxCondition; item.GetComponent()?.ResetDeterioration(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index b6dfb0840..c105f27de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -644,6 +644,13 @@ namespace Barotrauma.Networking set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool AllowImmediateItemDelivery + { + get; + set; + } + [Serialize(false, IsPropertySaveable.Yes)] public bool LockAllDefaultWires { @@ -841,6 +848,13 @@ namespace Barotrauma.Networking private set; } + [Serialize(10.0f, IsPropertySaveable.Yes)] + public float MinimumMidRoundSyncTimeout + { + get; + private set; + } + private bool karmaEnabled; [Serialize(false, IsPropertySaveable.Yes)] public bool KarmaEnabled diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs new file mode 100644 index 000000000..5229bf5b5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/ConditionallyEditable.cs @@ -0,0 +1,66 @@ +using System; +using Barotrauma.Items.Components; + +namespace Barotrauma; + +[AttributeUsage(AttributeTargets.Property)] +sealed class ConditionallyEditable : Editable +{ + public ConditionallyEditable(ConditionType conditionType, bool onlyInEditors = true) + { + this.conditionType = conditionType; + this.onlyInEditors = onlyInEditors; + } + private readonly ConditionType conditionType; + + private readonly bool onlyInEditors; + + public enum ConditionType + { + //These need to exist at compile time, so it is a little awkward + //I would love to see a better way to do this + AllowLinkingWifiToChat, + IsSwappableItem, + AllowRotating, + Attachable, + HasBody, + Pickable, + OnlyByStatusEffectsAndNetwork, + HasIntegratedButtons, + IsToggleableController, + HasConnectionPanel + } + + public bool IsEditable(ISerializableEntity entity) + { + if (onlyInEditors && Screen.Selected is { IsEditor: false }) { return false; } + + return conditionType switch + { + ConditionType.AllowLinkingWifiToChat + => GameMain.NetworkMember is not { ServerSettings.AllowLinkingWifiToChat: false }, + ConditionType.IsSwappableItem + => entity is Item item && item.Prefab.SwappableItem != null, + ConditionType.AllowRotating + => (entity is Item { body: null } item && item.Prefab.AllowRotatingInEditor) + || (entity is Structure structure && structure.Prefab.AllowRotatingInEditor), + ConditionType.Attachable + => entity is Holdable { Attachable: true }, + ConditionType.HasBody + => entity is Structure { HasBody: true } or Item { body: not null }, + ConditionType.Pickable + => entity is Item item && item.GetComponent() != null, + ConditionType.OnlyByStatusEffectsAndNetwork + => GameMain.NetworkMember is { IsServer: true }, + ConditionType.HasIntegratedButtons + => entity is Door { HasIntegratedButtons: true }, + ConditionType.IsToggleableController + => entity is Controller { IsToggle: true } controller && controller.Item.GetComponent() != null, + ConditionType.HasConnectionPanel + => (entity is Item item && item.GetComponent() != null) + || (entity is ItemComponent ic && ic.Item.GetComponent() != null), + _ + => false + }; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs new file mode 100644 index 000000000..5786df7b1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/Editable/Editable.cs @@ -0,0 +1,55 @@ +using System; +using Barotrauma.Items.Components; + +namespace Barotrauma; + +[AttributeUsage(AttributeTargets.Property)] +class Editable : Attribute +{ + public int MaxLength; + public int DecimalCount = 1; + + public int MinValueInt = int.MinValue, MaxValueInt = int.MaxValue; + public float MinValueFloat = float.MinValue, MaxValueFloat = float.MaxValue; + public bool ForceShowPlusMinusButtons = false; + public float ValueStep; + + /// + /// Labels of the components of a vector property (defaults to x,y,z,w) + /// + public string[] VectorComponentLabels; + + /// + /// If a translation can't be found for the property name, this tag is used instead + /// + public string FallBackTextTag; + + /// + /// Currently implemented only for int and bool fields. TODO: implement the remaining types (SerializableEntityEditor) + /// + public bool ReadOnly; + + public Editable(int maxLength = 20) + { + MaxLength = maxLength; + } + + public Editable(int minValue, int maxValue) + { + MinValueInt = minValue; + MaxValueInt = maxValue; + } + + public Editable(float minValue, float maxValue, int decimals = 1) + { + MinValueFloat = minValue; + MaxValueFloat = maxValue; + DecimalCount = decimals; + } +} + +[AttributeUsage(AttributeTargets.Property)] +sealed class InGameEditable : Editable +{ +} + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs similarity index 91% rename from Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs rename to Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs index 3e477e0e2..1c26b59df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs @@ -15,135 +15,6 @@ using Barotrauma.Networking; namespace Barotrauma { - [AttributeUsage(AttributeTargets.Property)] - class Editable : Attribute - { - public int MaxLength; - public int DecimalCount = 1; - - public int MinValueInt = int.MinValue, MaxValueInt = int.MaxValue; - public float MinValueFloat = float.MinValue, MaxValueFloat = float.MaxValue; - public float ValueStep; - - /// - /// Labels of the components of a vector property (defaults to x,y,z,w) - /// - public string[] VectorComponentLabels; - - /// - /// If a translation can't be found for the property name, this tag is used instead - /// - public string FallBackTextTag; - - /// - /// Currently implemented only for int and bool fields. TODO: implement the remaining types (SerializableEntityEditor) - /// - public bool ReadOnly; - - public Editable(int maxLength = 20) - { - MaxLength = maxLength; - } - - public Editable(int minValue, int maxValue) - { - MinValueInt = minValue; - MaxValueInt = maxValue; - } - - public Editable(float minValue, float maxValue, int decimals = 1) - { - MinValueFloat = minValue; - MaxValueFloat = maxValue; - DecimalCount = decimals; - } - } - - [AttributeUsage(AttributeTargets.Property)] - class InGameEditable : Editable - { - } - - [AttributeUsage(AttributeTargets.Property)] - class ConditionallyEditable : Editable - { - public ConditionallyEditable(ConditionType conditionType, bool onlyInEditors = true) - { - this.conditionType = conditionType; - this.onlyInEditors = onlyInEditors; - } - private readonly ConditionType conditionType; - - private readonly bool onlyInEditors; - - public enum ConditionType - { - //These need to exist at compile time, so it is a little awkward - //I would love to see a better way to do this - AllowLinkingWifiToChat, - IsSwappableItem, - AllowRotating, - Attachable, - HasBody, - Pickable, - OnlyByStatusEffectsAndNetwork, - HasIntegratedButtons, - IsToggleableController, - HasConnectionPanel - } - - public bool IsEditable(ISerializableEntity entity) - { - if (onlyInEditors && Screen.Selected is { IsEditor: false }) { return false; } - switch (conditionType) - { - case ConditionType.AllowLinkingWifiToChat: - return GameMain.NetworkMember?.ServerSettings?.AllowLinkingWifiToChat ?? true; - case ConditionType.IsSwappableItem: - { - return entity is Item item && item.Prefab.SwappableItem != null; - } - case ConditionType.AllowRotating: - { - return entity is Item item && item.body == null && item.Prefab.AllowRotatingInEditor; - } - case ConditionType.Attachable: - { - return entity is Holdable holdable && holdable.Attachable; - } - case ConditionType.HasBody: - { - return entity is Structure { HasBody: true } || entity is Item { body: not null }; - } - case ConditionType.Pickable: - { - return entity is Item item && item.GetComponent() != null; - } - case ConditionType.HasIntegratedButtons: - { - return entity is Door door && door.HasIntegratedButtons; - } - case ConditionType.OnlyByStatusEffectsAndNetwork: -#if SERVER - return true; -#else - return false; -#endif - case ConditionType.IsToggleableController: - { - return entity is Controller controller && controller.IsToggle && controller.Item.GetComponent() != null; - } - case ConditionType.HasConnectionPanel: - { - return - (entity is Item item && item.GetComponent() != null) || - (entity is ItemComponent ic && ic.Item.GetComponent() != null); - } - } - return false; - } - } - public enum IsPropertySaveable { Yes, @@ -151,7 +22,7 @@ namespace Barotrauma } [AttributeUsage(AttributeTargets.Property)] - public class Serialize : Attribute + public sealed class Serialize : Attribute { public readonly object DefaultValue; public readonly IsPropertySaveable IsSaveable; @@ -182,9 +53,9 @@ namespace Barotrauma } } - public class SerializableProperty + public sealed class SerializableProperty { - private readonly static ImmutableDictionary supportedTypes = new Dictionary + private static readonly ImmutableDictionary supportedTypes = new Dictionary { { typeof(bool), "bool" }, { typeof(int), "int" }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs index 409e5d5b1..1121e33d9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs @@ -114,7 +114,7 @@ namespace Barotrauma self = (T)boxedSelf; } - public static void TryDeserialize(this object boxedSelf, FieldInfo field, XElement element) + private static void TryDeserialize(this object boxedSelf, FieldInfo field, XElement element) { string fieldName = field.Name.ToLowerInvariant(); string valueStr = element.GetAttributeString(fieldName, field.GetValue(boxedSelf)?.ToString() ?? ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 8c044c67d..0ad15815d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -84,6 +84,7 @@ namespace Barotrauma Graphics = GraphicsSettings.GetDefault(), Audio = AudioSettings.GetDefault(), #if CLIENT + DisableGlobalSpamList = false, KeyMap = KeyMapping.GetDefault(), InventoryKeyMap = InventoryKeyMapping.GetDefault() #endif @@ -156,6 +157,7 @@ namespace Barotrauma public string RemoteMainMenuContentUrl; #if CLIENT public XElement SavedCampaignSettings; + public bool DisableGlobalSpamList; #endif #if DEBUG public bool UseSteamMatchmaking; @@ -548,6 +550,19 @@ namespace Barotrauma currentConfig.Graphics.VSync != newConfig.Graphics.VSync || currentConfig.Graphics.DisplayMode != newConfig.Graphics.DisplayMode; +#if CLIENT + bool keybindsChanged = false; + foreach (var kvp in newConfig.KeyMap.Bindings) + { + if (!currentConfig.KeyMap.Bindings.TryGetValue(kvp.Key, out var existingBinding) || + existingBinding != kvp.Value) + { + keybindsChanged = true; + break; + } + } +#endif + currentConfig = newConfig; #if CLIENT @@ -575,7 +590,19 @@ namespace Barotrauma HUDLayoutSettings.CreateAreas(); GameMain.GameSession?.HUDScaleChanged(); } - + + if (keybindsChanged) + { + foreach (var item in Item.ItemList) + { + foreach (var ic in item.Components) + { + //parse messages because they may contain keybind texts + ic.ParseMsg(); + } + } + } + GameMain.SoundManager?.ApplySettings(); #endif if (languageChanged) { TextManager.ClearCache(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index bb39a43b5..8d3d19ab4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -306,7 +306,8 @@ namespace Barotrauma } if (file == "") { - DebugConsole.ThrowError("Sprite " + SourceElement + " doesn't have a texture specified!"); + DebugConsole.ThrowError("Sprite " + SourceElement.Element + " doesn't have a texture specified!", + contentPackage: SourceElement.ContentPackage); return false; } if (!string.IsNullOrEmpty(path)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 8d14f4ce7..3c34fd89a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -196,7 +196,7 @@ namespace Barotrauma /// public readonly bool TargetContainedItem; - public static IEnumerable FromXElement(XElement element, Predicate? predicate = null) + public static IEnumerable FromXElement(ContentXElement element, Predicate? predicate = null) { var targetItemComponent = element.GetAttributeString(nameof(TargetItemComponent), ""); var targetContainer = element.GetAttributeBool(nameof(TargetContainer), false); @@ -218,7 +218,7 @@ namespace Barotrauma var (comparisonOperator, attributeValueString) = ExtractComparisonOperatorFromConditionString(attribute.Value); if (string.IsNullOrWhiteSpace(attributeValueString)) { - DebugConsole.ThrowError($"Conditional attribute value is empty: {element}"); + DebugConsole.ThrowError($"Conditional attribute value is empty: {element}", contentPackage: element.ContentPackage); continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 3af1f3aca..67226d137 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -227,17 +227,17 @@ namespace Barotrauma public bool InheritEventTags { get; private set; } - public ItemSpawnInfo(XElement element, string parentDebugName) + public ItemSpawnInfo(ContentXElement element, string parentDebugName) { - if (element.Attribute("name") != null) + if (element.GetAttribute("name") != null) { //backwards compatibility - DebugConsole.ThrowError("Error in StatusEffect config (" + element.ToString() + ") - use item identifier instead of the name."); + DebugConsole.ThrowError("Error in StatusEffect config (" + element.ToString() + ") - use item identifier instead of the name.", contentPackage: element.ContentPackage); string itemPrefabName = element.GetAttributeString("name", ""); ItemPrefab = ItemPrefab.Prefabs.Find(m => m.NameMatches(itemPrefabName, StringComparison.InvariantCultureIgnoreCase) || m.Tags.Contains(itemPrefabName)); if (ItemPrefab == null) { - DebugConsole.ThrowError("Error in StatusEffect \"" + parentDebugName + "\" - item prefab \"" + itemPrefabName + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect \"" + parentDebugName + "\" - item prefab \"" + itemPrefabName + "\" not found.", contentPackage: element.ContentPackage); } } else @@ -246,12 +246,12 @@ namespace Barotrauma if (string.IsNullOrEmpty(itemPrefabIdentifier)) itemPrefabIdentifier = element.GetAttributeString("identifiers", ""); if (string.IsNullOrEmpty(itemPrefabIdentifier)) { - DebugConsole.ThrowError("Invalid item spawn in StatusEffect \"" + parentDebugName + "\" - identifier not found in the element \"" + element.ToString() + "\""); + DebugConsole.ThrowError("Invalid item spawn in StatusEffect \"" + parentDebugName + "\" - identifier not found in the element \"" + element.ToString() + "\".", contentPackage: element.ContentPackage); } ItemPrefab = ItemPrefab.Prefabs.Find(m => m.Identifier == itemPrefabIdentifier); if (ItemPrefab == null) { - DebugConsole.ThrowError("Error in StatusEffect config - item prefab with the identifier \"" + itemPrefabIdentifier + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect config - item prefab with the identifier \"" + itemPrefabIdentifier + "\" not found.", contentPackage: element.ContentPackage); return; } } @@ -332,7 +332,7 @@ namespace Barotrauma /// public readonly bool TriggerTalents; - public GiveSkill(XElement element, string parentDebugName) + public GiveSkill(ContentXElement element, string parentDebugName) { SkillIdentifier = element.GetAttributeIdentifier(nameof(SkillIdentifier), Identifier.Empty); Amount = element.GetAttributeFloat(nameof(Amount), 0); @@ -340,7 +340,7 @@ namespace Barotrauma if (SkillIdentifier == Identifier.Empty) { - DebugConsole.ThrowError($"GiveSkill StatusEffect did not have a skill identifier defined in {parentDebugName}!"); + DebugConsole.ThrowError($"GiveSkill StatusEffect did not have a skill identifier defined in {parentDebugName}!", contentPackage: element.ContentPackage); } } } @@ -410,12 +410,12 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool InheritEventTags { get; private set; } - public CharacterSpawnInfo(XElement element, string parentDebugName) + public CharacterSpawnInfo(ContentXElement element, string parentDebugName) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); if (SpeciesName.IsEmpty) { - DebugConsole.ThrowError($"Invalid character spawn ({Name}) in StatusEffect \"{parentDebugName}\" - identifier not found in the element \"{element}\""); + DebugConsole.ThrowError($"Invalid character spawn ({Name}) in StatusEffect \"{parentDebugName}\" - identifier not found in the element \"{element}\".", contentPackage: element.ContentPackage); } } } @@ -798,7 +798,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})", contentPackage: element.ContentPackage); } else { @@ -837,7 +837,7 @@ namespace Barotrauma 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})", contentPackage: element.ContentPackage); } break; case "targettype": @@ -866,11 +866,11 @@ namespace Barotrauma case "comparison": 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})", contentPackage: element.ContentPackage); } break; case "sound": - DebugConsole.ThrowError($"Error in StatusEffect ({parentDebugName}): sounds should be defined as child elements of the StatusEffect, not as attributes."); + DebugConsole.ThrowError($"Error in StatusEffect ({parentDebugName}): sounds should be defined as child elements of the StatusEffect, not as attributes.", contentPackage: element.ContentPackage); break; case "range": if (!HasTargetType(TargetType.NearbyCharacters) && !HasTargetType(TargetType.NearbyItems)) @@ -951,7 +951,7 @@ namespace Barotrauma RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: parentDebugName); if (newRequiredItem == null) { - DebugConsole.ThrowError("Error in StatusEffect config - requires an item with no identifiers."); + DebugConsole.ThrowError("Error in StatusEffect config - requires an item with no identifiers.", contentPackage: element.ContentPackage); continue; } requiredItems.Add(newRequiredItem); @@ -973,12 +973,12 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab; if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers instead of names."); + DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers instead of names.", contentPackage: element.ContentPackage); string afflictionName = subElement.GetAttributeString("name", ""); afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { - DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.", contentPackage: element.ContentPackage); continue; } } @@ -988,7 +988,7 @@ namespace Barotrauma afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier); if (afflictionPrefab == null) { - DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found.", contentPackage: element.ContentPackage); continue; } } @@ -1001,7 +1001,7 @@ namespace Barotrauma case "reduceaffliction": if (subElement.GetAttribute("name") != null) { - DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); + DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.", contentPackage: element.ContentPackage); ReduceAffliction.Add(( subElement.GetAttributeIdentifier("name", ""), subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); @@ -1016,7 +1016,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier or type \"" + name + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier or type \"" + name + "\" not found.", contentPackage: element.ContentPackage); } } break; @@ -1374,6 +1374,10 @@ namespace Barotrauma if (OnlyOutside && character.CurrentHull != null) { return false; } if (TargetIdentifiers == null) { return true; } if (TargetIdentifiers.Contains("character")) { return true; } + if (TargetIdentifiers.Contains("monster")) + { + return !character.IsHuman && character.Group != CharacterPrefab.HumanSpeciesName; + } return TargetIdentifiers.Contains(character.SpeciesName); } @@ -1697,7 +1701,7 @@ namespace Barotrauma if (limb.Removed) { continue; } if (limb.IsSevered) { continue; } if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } - AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); + AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); RegisterTreatmentResults(user, entity as Item, limb, affliction, result); //only apply non-limb-specific afflictions to the first limb @@ -1710,7 +1714,7 @@ namespace Barotrauma if (limb.character.Removed || limb.Removed) { continue; } if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality); - AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); + AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); RegisterTreatmentResults(user, entity as Item, limb, affliction, result); } @@ -1878,7 +1882,7 @@ namespace Barotrauma if (FireSize > 0.0f && entity != null) { - var fire = new FireSource(position, hull); + var fire = new FireSource(position, hull, sourceCharacter: user); fire.Size = new Vector2(FireSize, fire.Size.Y); } @@ -2245,8 +2249,8 @@ namespace Barotrauma { OnItemSpawned(newItem, chosenItemSpawnInfo); }); + break; } - break; } } } @@ -2409,7 +2413,7 @@ namespace Barotrauma { if (limb.character.Removed || limb.Removed) { continue; } newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, limb.character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality); - var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User); + var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: Vector2.Zero, attacker: element.User); element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, limb, affliction, result); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index c42374a47..37064ac10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -481,6 +481,7 @@ namespace Barotrauma } pathFinder = null; + roundData = null; } private static void UnlockAchievement(Character recipient, Identifier identifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs index 282c7d503..830c43168 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs @@ -45,7 +45,7 @@ public static class Tags public static readonly Identifier ToolItem = "tool".ToIdentifier(); public static readonly Identifier LogicItem = "logic".ToIdentifier(); public static readonly Identifier NavTerminal = "navterminal".ToIdentifier(); - public static readonly Identifier IdCard = "identitycard".ToIdentifier(); + public static readonly Identifier IdCardTag = "identitycard".ToIdentifier(); public static readonly Identifier WireItem = "wire".ToIdentifier(); public static readonly Identifier ChairItem = "chair".ToIdentifier(); public static readonly Identifier ArtifactHolder = "artifactholder".ToIdentifier(); @@ -55,6 +55,8 @@ public static class Tags public static readonly Identifier DontSellItems = "dontsellitems".ToIdentifier(); public static readonly Identifier CargoContainer = "cargocontainer".ToIdentifier(); + public static readonly Identifier CargoMissionItem = "cargomission".ToIdentifier(); + public static readonly Identifier ItemIgnoredByAI = "ignorebyai".ToIdentifier(); public static readonly Identifier GuardianShelter = "guardianshelter".ToIdentifier(); @@ -98,5 +100,15 @@ public static class Tags /// public static readonly Identifier DespawnContainer = "despawncontainer".ToIdentifier(); + /// + /// Used by talents to target all stat identifiers + /// + public static readonly Identifier StatIdentifierTargetAll = "all".ToIdentifier(); + + public static readonly Identifier HelmSkill = "helm".ToIdentifier(); + public static readonly Identifier WeaponsSkill = "weapons".ToIdentifier(); + public static readonly Identifier ElectricalSkill = "electrical".ToIdentifier(); + public static readonly Identifier MechanicalSkill = "mechanical".ToIdentifier(); + public static readonly Identifier MedicalSkill = "medical".ToIdentifier(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs index 7c8ede811..2d978ed4e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using Barotrauma.Extensions; namespace Barotrauma @@ -32,7 +33,7 @@ namespace Barotrauma (string value, bool loaded) tryLoad(LanguageIdentifier lang) { - IReadOnlyList candidates = Array.Empty(); + IReadOnlyList candidates = Array.Empty(); int tagIndex = 0; if (TextManager.TextPacks.TryGetValue(lang, out var packs)) @@ -50,8 +51,17 @@ namespace Barotrauma } } - bool loaded = candidates.Count > 0; - return (loaded ? candidates.GetRandomUnsynced() : "", loaded); + if (candidates.Count == 0) { return (string.Empty, loaded: false); } + var firstOverride = candidates.FirstOrDefault(c => c.IsOverride); + if (firstOverride != default) + { + //if there's overrides defined, choose from the first pack that defines overrides + return (candidates.Where(static c => c.IsOverride).Where(c => c.TextPack == firstOverride.TextPack).GetRandomUnsynced().String, loaded: true); + } + else + { + return (candidates.GetRandomUnsynced().String, loaded: true); + } } var (value, loaded) = tryLoad(Language); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs index f370ddd71..7e8300c5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs @@ -9,6 +9,7 @@ namespace Barotrauma { protected bool loaded = false; protected LanguageIdentifier language = LanguageIdentifier.None; + private int languageVersion = 0; protected string cachedSanitizedValue = ""; public string SanitizedValue @@ -32,9 +33,9 @@ namespace Barotrauma #if CLIENT private readonly GUIFont? font; private readonly GUIComponentStyle? componentStyle; - private readonly bool forceUpperCase = false; + private bool forceUpperCase = false; - private bool fontOrStyleForceUpperCase + private bool FontOrStyleForceUpperCase => font is { ForceUpperCase: true } || componentStyle is { ForceUpperCase: true }; #endif @@ -91,8 +92,9 @@ namespace Barotrauma { return NestedStr.Loaded != loaded || language != GameSettings.CurrentConfig.Language + || languageVersion != TextManager.LanguageVersion #if CLIENT - || (fontOrStyleForceUpperCase != forceUpperCase) + || (FontOrStyleForceUpperCase != forceUpperCase) #endif ; } @@ -100,9 +102,9 @@ namespace Barotrauma public void RetrieveValue() { #if CLIENT - NestedStr = fontOrStyleForceUpperCase ? originalStr.ToUpper() : originalStr; + NestedStr = FontOrStyleForceUpperCase ? originalStr.ToUpper() : originalStr; + forceUpperCase = FontOrStyleForceUpperCase; #endif - if (shouldParseRichTextData) { RichTextData = Barotrauma.RichTextData.GetRichTextData(NestedStr.Value, out cachedSanitizedValue); @@ -113,6 +115,7 @@ namespace Barotrauma } if (postProcess != null) { cachedSanitizedValue = postProcess(cachedSanitizedValue); } language = GameSettings.CurrentConfig.Language; + languageVersion = TextManager.LanguageVersion; loaded = NestedStr.Loaded; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 8fb92ae72..f8b5849f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -23,7 +23,7 @@ namespace Barotrauma public static bool DebugDraw; public readonly static LanguageIdentifier DefaultLanguage = "English".ToLanguageIdentifier(); - public readonly static ConcurrentDictionary> TextPacks = new ConcurrentDictionary>(); + public readonly static ConcurrentDictionary> TextPacks = new ConcurrentDictionary>(); public static IEnumerable AvailableLanguages => TextPacks.Keys; private readonly static Dictionary> cachedStrings = @@ -160,22 +160,48 @@ namespace Barotrauma return TextPacks[GameSettings.CurrentConfig.Language].Any(p => p.Texts.ContainsKey(tag)); } + public static bool ContainsTag(Identifier tag, LanguageIdentifier language) + { + return TextPacks[language].Any(p => p.Texts.ContainsKey(tag)); + } public static IEnumerable GetAll(string tag) => GetAll(tag.ToIdentifier()); public static IEnumerable GetAll(Identifier tag) { - return TextPacks[GameSettings.CurrentConfig.Language] + var allTexts = TextPacks[GameSettings.CurrentConfig.Language] .SelectMany(p => p.Texts.TryGetValue(tag, out var value) - ? (IEnumerable)value - : Array.Empty()); + ? (IEnumerable)value + : Array.Empty()); + + var firstOverride = allTexts.FirstOrDefault(t => t.IsOverride); + if (firstOverride != default) + { + return allTexts.Where(t => t.IsOverride && t.TextPack == firstOverride.TextPack).Select(t => t.String); + } + else + { + return allTexts.Select(t => t.String); + } } - + public static IEnumerable> GetAllTagTextPairs() { - return TextPacks[GameSettings.CurrentConfig.Language] - .SelectMany(p => p.Texts) - .SelectMany(kvp => kvp.Value.Select(v => new KeyValuePair(kvp.Key, v))); + var allTexts = TextPacks[GameSettings.CurrentConfig.Language] + .SelectMany(p => p.Texts); + + var firstOverride = allTexts.SelectMany(kvp => kvp.Value).FirstOrDefault(t => t.IsOverride); + if (firstOverride != default) + { + return allTexts + .Where(kvp => kvp.Value.Any(t => t.IsOverride && t.TextPack == firstOverride.TextPack)) + .SelectMany(kvp => kvp.Value.Select(v => new KeyValuePair(kvp.Key, v.String))); + } + else + { + return allTexts + .SelectMany(kvp => kvp.Value.Select(v => new KeyValuePair(kvp.Key, v.String))); + } } public static IEnumerable GetTextFiles() @@ -213,13 +239,21 @@ namespace Barotrauma } public static LocalizedString Get(params Identifier[] tags) + { + if (tags.Length == 1) + { + return Get(tags[0]); + } + return new TagLString(tags); + } + + public static LocalizedString Get(Identifier tag) { TagLString? str = null; lock (cachedStrings) { - if (tags.Length == 1 && !nonCacheableTags.Contains(tags[0])) + if (!nonCacheableTags.Contains(tag)) { - var tag = tags[0]; if (cachedStrings.TryGetValue(tag, out var strRef)) { if (!strRef.TryGetTarget(out str)) @@ -246,15 +280,18 @@ namespace Barotrauma } else { - str = new TagLString(tags); + str = new TagLString(tag); cachedStrings.Add(tag, new WeakReference(str)); } } } } - return str ?? new TagLString(tags); + return str ?? new TagLString(tag); } - + + public static LocalizedString Get(string tag) + => Get(tag.ToIdentifier()); + public static LocalizedString Get(params string[] tags) => Get(tags.ToIdentifiers()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs index 05ead3562..39d447e24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs @@ -48,7 +48,13 @@ namespace Barotrauma public readonly LanguageIdentifier Language; - public readonly ImmutableDictionary> Texts; + + public readonly record struct Text( + string String, + bool IsOverride, + TextPack TextPack); + + public readonly ImmutableDictionary> Texts; public readonly string TranslatedName; public readonly bool NoWhitespace; @@ -56,24 +62,47 @@ namespace Barotrauma { ContentFile = file; - var languageName = mainElement.GetAttributeIdentifier("language", TextManager.DefaultLanguage.Value); + var languageName = mainElement.GetAttributeIdentifier("language", Identifier.Empty); + if (languageName.IsEmpty) + { + DebugConsole.AddWarning($"Language not defined in text file \"{file.Path}\". Setting the language as {TextManager.DefaultLanguage}.", + mainElement.ContentPackage); + languageName = TextManager.DefaultLanguage.Value; + } Language = language; TranslatedName = mainElement.GetAttributeString("translatedname", languageName.Value); NoWhitespace = mainElement.GetAttributeBool("nowhitespace", false); - Dictionary> texts = new Dictionary>(); - foreach (var element in mainElement.Elements()) + Dictionary> texts = new Dictionary>(); + LoadElements(mainElement, isOverride: mainElement.IsOverride()); + + void LoadElements(XElement parentElement, bool isOverride) { - Identifier elemName = element.NameAsIdentifier(); - if (!texts.ContainsKey(elemName)) { texts.Add(elemName, new List()); } - texts[elemName].Add(element.ElementInnerText() - .Replace(@"\n", "\n") - .Replace("&", "&") - .Replace("<", "<") - .Replace(">", ">") - .Replace(""", "\"") - .Replace("'", "'")); + foreach (var element in parentElement.Elements()) + { + Identifier elemName = element.NameAsIdentifier(); + + if (element.IsOverride()) + { + LoadElements(element, isOverride: true); + } + else + { + if (!texts.ContainsKey(elemName)) { texts.Add(elemName, new List()); } + + string str = element.ElementInnerText() + .Replace(@"\n", "\n") + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace(""", "\"") + .Replace("'", "'"); + + texts[elemName].Add(new Text(str, isOverride, this)); + } + } } + Texts = texts.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs index 556ad87e5..99ba1035e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs @@ -96,8 +96,8 @@ namespace Barotrauma traitor.Character.IsTraitor = true; AddTarget(Tags.Traitor, traitor.Character); AddTarget(Tags.AnyTraitor, traitor.Character); - AddTargetPredicate(Tags.NonTraitor, e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.TeamID == traitor.TeamID && !c.IsIncapacitated); - AddTargetPredicate(Tags.NonTraitorPlayer, e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); + AddTargetPredicate(Tags.NonTraitor, TargetPredicate.EntityType.Character, e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.TeamID == traitor.TeamID && !c.IsIncapacitated); + AddTargetPredicate(Tags.NonTraitorPlayer, TargetPredicate.EntityType.Character, e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated); } public void SetSecondaryTraitors(IEnumerable traitors) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs index e941208e9..1353c1bbf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEventPrefab.cs @@ -25,7 +25,8 @@ namespace Barotrauma MissionType = element.GetAttributeEnum(nameof(MissionType), MissionType.None); if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionType == MissionType.None) { - DebugConsole.ThrowError($"Error in traitor event \"{prefab.Identifier}\". Mission requirement with no {nameof(MissionIdentifier)}, {nameof(MissionTag)} or {nameof(MissionType)}."); + DebugConsole.ThrowError($"Error in traitor event \"{prefab.Identifier}\". Mission requirement with no {nameof(MissionIdentifier)}, {nameof(MissionTag)} or {nameof(MissionType)}.", + contentPackage: prefab.ContentPackage); } } @@ -72,7 +73,7 @@ namespace Barotrauma //feels a little weird to have something this specific here, but couldn't think of a better way to implement this public ImmutableArray RequiredItemConditionals; - public LevelRequirement(XElement element, TraitorEventPrefab prefab) + public LevelRequirement(ContentXElement element, TraitorEventPrefab prefab) { levelType = element.GetAttributeEnum(nameof(LevelType), LevelType.Any); LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index 88731d001..920dbb269 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -333,7 +333,8 @@ namespace Barotrauma { DebugConsole.AddWarning($"Failed to save upgrade \"{Prefab.Name}\" on {TargetEntity.Name} because property reference \"{propertyRef.Name}\" is missing original values. \n" + "Upgrades should always call Upgrade.ApplyUpgrade() or manually set the original value in a property reference after they have been added. \n" + - "If you are not a developer submit a bug report at https://github.com/Regalis11/Barotrauma/issues/."); + "If you are not a developer submit a bug report at https://github.com/Regalis11/Barotrauma/issues/.", + Prefab.ContentPackage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 9114c6f6a..8365930bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -18,12 +18,8 @@ namespace Barotrauma public readonly int IncreaseHigh; - public readonly UpgradePrefab Prefab; - public UpgradePrice(UpgradePrefab prefab, ContentXElement element) { - Prefab = prefab; - IncreaseLow = UpgradePrefab.ParsePercentage(element.GetAttributeString("increaselow", string.Empty)!, "IncreaseLow".ToIdentifier(), element, suppressWarnings: prefab.SuppressWarnings); @@ -34,20 +30,21 @@ namespace Barotrauma if (BasePrice == -1) { - if (prefab.SuppressWarnings) + if (!prefab.SuppressWarnings) { DebugConsole.AddWarning($"Price attribute \"baseprice\" is not defined for {prefab?.Identifier}.\n " + - "The value has been assumed to be '1000'."); + "The value has been assumed to be '1000'.", + prefab!.ContentPackage); BasePrice = 1000; } } } - public int GetBuyPrice(int level, Location? location = null, ImmutableHashSet? characterList = null) + public int GetBuyPrice(UpgradePrefab prefab, int level, Location? location = null, ImmutableHashSet? characterList = null) { float price = BasePrice; - int maxLevel = Prefab.MaxLevel; + int maxLevel = prefab.MaxLevel; float lerpAmount = maxLevel is 0 ? level // avoid division by 0 @@ -342,7 +339,8 @@ namespace Barotrauma { DebugConsole.AddWarning($"Upgrade \"{prefab.Identifier}\" is affecting a property that is also being affected by \"{matchingPrefab.Identifier}\".\n" + "This is unsupported and might yield unexpected results if both upgrades are applied at the same time to the same item.\n" + - "Add the attribute suppresswarnings=\"true\" to your XML element to disable this warning if you know what you're doing."); + "Add the attribute suppresswarnings=\"true\" to your XML element to disable this warning if you know what you're doing.", + prefab.ContentPackage); } } } @@ -398,11 +396,11 @@ namespace Barotrauma public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file) { - Name = element.GetAttributeString("name", string.Empty)!; - Description = element.GetAttributeString("description", string.Empty)!; - MaxLevel = element.GetAttributeInt("maxlevel", 1); - SuppressWarnings = element.GetAttributeBool("supresswarnings", false); - HideInMenus = element.GetAttributeBool("hideinmenus", false); + Name = element.GetAttributeString(nameof(Name), string.Empty)!; + Description = element.GetAttributeString(nameof(Description), string.Empty)!; + MaxLevel = element.GetAttributeInt(nameof(MaxLevel), 1); + SuppressWarnings = element.GetAttributeBool(nameof(SuppressWarnings), false); + HideInMenus = element.GetAttributeBool(nameof(HideInMenus), false); SourceElement = element; var targetProperties = new Dictionary(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 195cba7fc..1f116b00f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -139,17 +139,17 @@ namespace Barotrauma return new Rectangle(rect.X - amount, rect.Y + amount, rect.Width + amount * 2, rect.Height + amount * 2); } - public static int VectorOrientation(Vector2 p1, Vector2 p2, Vector2 p) + /// + /// Given three points A, B and C, + /// returns 1 if AC is oriented clockwise relative to AB, + /// -1 if AC is oriented counter-clockwise relative to AB, + /// or 0 if A, B and C are collinear. + /// + public static int VectorOrientation(Vector2 pointA, Vector2 pointB, Vector2 pointC) { - // Determinant - float Orin = (p2.X - p1.X) * (p.Y - p1.Y) - (p.X - p1.X) * (p2.Y - p1.Y); + float determinant = (pointB.X - pointA.X) * (pointC.Y - pointA.Y) - (pointC.X - pointA.X) * (pointB.Y - pointA.Y); - if (Orin > 0) - return -1; // (* Orientation is to the left-hand side *) - if (Orin < 0) - return 1; // (* Orientation is to the right-hand side *) - - return 0; // (* Orientation is neutral aka collinear *) + return -Math.Sign(determinant); } @@ -412,41 +412,29 @@ namespace Barotrauma return false; } - /*public static List GetLineRectangleIntersections(Vector2 a1, Vector2 a2, Rectangle rect) - { - List intersections = new List(); + public static Vector2 FlipX(this Vector2 vector) + => new Vector2(-vector.X, vector.Y); - Vector2? intersection = GetAxisAlignedLineIntersection(a1, a2, - new Vector2(rect.X, rect.Y), - new Vector2(rect.Right, rect.Y), - true); + public static Vector2 FlipY(this Vector2 vector) + => new Vector2(vector.X, -vector.Y); - if (intersection != null) intersections.Add((Vector2)intersection); + public static Vector2 YX(this Vector2 vector) + => new Vector2(x: vector.Y, y: vector.X); + + public static Point FlipY(this Point point) + => new Point(point.X, -point.Y); - intersection = GetAxisAlignedLineIntersection(a1, a2, - new Vector2(rect.X, rect.Y - rect.Height), - new Vector2(rect.Right, rect.Y - rect.Height), - true); + public static Point YX(this Point point) + => new Point(x: point.Y, y: point.X); + + public static Vector2 RotatedUnitXRadians(float radians) + => new Vector2(MathF.Cos(radians), MathF.Sin(radians)); - if (intersection != null) intersections.Add((Vector2)intersection); - - intersection = GetAxisAlignedLineIntersection(a1, a2, - new Vector2(rect.X, rect.Y), - new Vector2(rect.X, rect.Y - rect.Height), - false); - - if (intersection != null) intersections.Add((Vector2)intersection); - - intersection = GetAxisAlignedLineIntersection(a1, a2, - new Vector2(rect.Right, rect.Y), - new Vector2(rect.Right, rect.Y - rect.Height), - false); - - if (intersection != null) intersections.Add((Vector2)intersection); - - return intersections; - }*/ + public static Vector2 RotatedUnitYRadians(float radians) + => RotatedUnitXRadians(radians).YX().FlipX(); + public static Vector2 Round(this Vector2 vector) + => new Vector2((int)MathF.Round(vector.X), (int)MathF.Round(vector.Y)); /// /// Get the intersections between a line (either infinite or a line segment) and a circle @@ -1103,11 +1091,11 @@ namespace Barotrauma } } - class CompareCCW : IComparer + class CompareCW : IComparer { private Vector2 center; - public CompareCCW(Vector2 center) + public CompareCW(Vector2 center) { this.center = center; } @@ -1118,25 +1106,43 @@ namespace Barotrauma public static int Compare(Vector2 a, Vector2 b, Vector2 center) { - if (a == b) return 0; - if (a.X - center.X >= 0 && b.X - center.X < 0) return -1; - if (a.X - center.X < 0 && b.X - center.X >= 0) return 1; + if (a == b) { return 0; } + if (a.X - center.X >= 0 && b.X - center.X < 0) { return 1; } + if (a.X - center.X < 0 && b.X - center.X >= 0) { return -1; } if (a.X - center.X == 0 && b.X - center.X == 0) { - if (a.Y - center.Y >= 0 || b.Y - center.Y >= 0) return Math.Sign(b.Y - a.Y); + if (a.Y - center.Y >= 0 || b.Y - center.Y >= 0) { return Math.Sign(a.Y - b.Y); } return Math.Sign(a.Y - b.Y); } // compute the cross product of vectors (center -> a) x (center -> b) float det = (a.X - center.X) * (b.Y - center.Y) - (b.X - center.X) * (a.Y - center.Y); - if (det < 0) return -1; - if (det > 0) return 1; + if (det < 0) { return 1; } + if (det > 0) { return -1; } // points a and b are on the same line from the center // check which point is closer to the center float d1 = (a.X - center.X) * (a.X - center.X) + (a.Y - center.Y) * (a.Y - center.Y); float d2 = (b.X - center.X) * (b.X - center.X) + (b.Y - center.Y) * (b.Y - center.Y); - return Math.Sign(d2 - d1); + return Math.Sign(d1 - d2); + } + } + + class CompareCCW : IComparer + { + private Vector2 center; + + public CompareCCW(Vector2 center) + { + this.center = center; + } + public int Compare(Vector2 a, Vector2 b) + { + return -CompareCW.Compare(a, b, center); + } + public static int Compare(Vector2 a, Vector2 b, Vector2 center) + { + return -CompareCW.Compare(a, b, center); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 7d2a8d983..bd3f78492 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -160,7 +160,14 @@ namespace Barotrauma { string subName = subElement.GetAttributeString("name", ""); string ownedSubPath = Path.Combine(TempPath, subName + ".sub"); - ownedSubmarines.Add(new SubmarineInfo(ownedSubPath)); + if (!File.Exists(ownedSubPath)) + { + DebugConsole.ThrowError($"Could not find the submarine \"{subName}\" ({ownedSubPath})! The save file may be corrupted. Removing the submarine from owned submarines..."); + } + else + { + ownedSubmarines.Add(new SubmarineInfo(ownedSubPath)); + } } return ownedSubmarines; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Quad2D.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Quad2D.cs new file mode 100644 index 000000000..af6284bc9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Quad2D.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma; + +readonly record struct Quad2D(Vector2 A, Vector2 B, Vector2 C, Vector2 D) +{ + public Vector2 Centroid => (A + B + C + D) / 4; + + public static Quad2D FromRectangle(RectangleF rectangle) + { + return new Quad2D( + A: (rectangle.Left, rectangle.Top), + B: (rectangle.Right, rectangle.Top), + C: (rectangle.Right, rectangle.Bottom), + D: (rectangle.Left, rectangle.Bottom)); + } + + public static Quad2D FromSubmarineRectangle(RectangleF rectangle) + { + return new Quad2D( + A: (rectangle.X, rectangle.Y), + B: (rectangle.X + rectangle.Width, rectangle.Y), + C: (rectangle.X + rectangle.Width, rectangle.Y - rectangle.Height), + D: (rectangle.X, rectangle.Y - rectangle.Height)); + } + + public Quad2D Rotated(float radians) + { + return new Quad2D( + A: MathUtils.RotatePointAroundTarget(point: A, target: Centroid, radians: radians), + B: MathUtils.RotatePointAroundTarget(point: B, target: Centroid, radians: radians), + C: MathUtils.RotatePointAroundTarget(point: C, target: Centroid, radians: radians), + D: MathUtils.RotatePointAroundTarget(point: D, target: Centroid, radians: radians)); + } + + public RectangleF BoundingAxisAlignedRectangle + { + get + { + Vector2 min = ( + X: Math.Min(A.X, Math.Min(B.X, Math.Min(C.X, D.X))), + Y: Math.Min(A.Y, Math.Min(B.Y, Math.Min(C.Y, D.Y)))); + Vector2 max = ( + X: Math.Max(A.X, Math.Max(B.X, Math.Max(C.X, D.X))), + Y: Math.Max(A.Y, Math.Max(B.Y, Math.Max(C.Y, D.Y)))); + return new RectangleF(location: min, size: max - min); + } + } + + public bool TryGetEdges(Span<(Vector2 A, Vector2 B)> outputSpan) + { + if (outputSpan.Length < 4) { return false; } + + outputSpan[0] = (A, B); + outputSpan[1] = (B, C); + outputSpan[2] = (C, D); + outputSpan[3] = (D, A); + return true; + } + + public bool Contains(Vector2 point) + { + // Break up the quad into two triangles and then see if the point is in either triangle. + // Since quads can be concave, care needs to be taken when splitting in two. + + (Triangle2D triangle1, Triangle2D triangle2) + = (new Triangle2D(A, B, C), new Triangle2D(A, D, C)); + + // If D is inside of the triangle ABC, or B is inside of the triangle ADC, + // then the quad is concave and we split at the wrong diagonal. + // Splitting at the other diagonal should be fine. + if (triangle1.Contains(D) || triangle2.Contains(B)) + { + (triangle1, triangle2) = (new Triangle2D(B, C, D), new Triangle2D(B, A, D)); + } + + return triangle1.Contains(point) || triangle2.Contains(point); + } + + public bool Intersects(Quad2D other) + { + if (!BoundingAxisAlignedRectangle.Intersects(other.BoundingAxisAlignedRectangle)) + { + return false; + } + + if (Contains(other.A)) { return true; } + if (Contains(other.B)) { return true; } + if (Contains(other.C)) { return true; } + if (Contains(other.D)) { return true; } + + if (other.Contains(A)) { return true; } + if (other.Contains(B)) { return true; } + if (other.Contains(C)) { return true; } + if (other.Contains(D)) { return true; } + + Span<(Vector2 A, Vector2 B)> myEdges = stackalloc (Vector2 A, Vector2 B)[4]; + TryGetEdges(myEdges); + Span<(Vector2 A, Vector2 B)> otherEdges = stackalloc (Vector2 A, Vector2 B)[4]; + other.TryGetEdges(otherEdges); + foreach (var edge in myEdges) + { + foreach (var otherEdge in otherEdges) + { + if (MathUtils.LineSegmentsIntersect(edge.A, edge.B, otherEdge.A, otherEdge.B)) { return true; } + } + } + return false; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Triangle2D.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Triangle2D.cs new file mode 100644 index 000000000..ab5e4b33e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Shapes/Triangle2D.cs @@ -0,0 +1,21 @@ +using Microsoft.Xna.Framework; + +namespace Barotrauma; + +readonly record struct Triangle2D(Vector2 A, Vector2 B, Vector2 C) +{ + public bool Contains(Vector2 point) + { + // Get the half-plane that the point lands in, for each side of the triangle + int halfPlaneAb = MathUtils.VectorOrientation(A, B, point); + int halfPlaneBc = MathUtils.VectorOrientation(B, C, point); + int halfPlaneCa = MathUtils.VectorOrientation(C, A, point); + + // The intersection of three half-planes derived from the three sides of the triangle + // is the triangle itself, so check for the point being in those three half-planes + bool allNonNegative = halfPlaneAb >= 0 && halfPlaneBc >= 0 && halfPlaneCa >= 0; + bool allNonPositive = halfPlaneAb <= 0 && halfPlaneBc <= 0 && halfPlaneCa <= 0; + + return allNonNegative || allNonPositive; + } +} diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index f325714d8..73a4ca7f7 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,118 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.6.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Added some more logging to diagnose the mysterious "pipe was broken" crashes. These seem to happen when the server crashes in some way that prevents it from generating a crash report or communicating the reason of the crash to the clients. Now the "pipe is broken" crash report should include the exit code of the server process, giving us more clues for diagnosing the issue, and the server should create simplified crash report if there's some kind of an issue with creating the normal crash report. + +Changes: +- Structures can now be rotated in the sub editor! +- Outpost security is now better at catching thieves: they can do random inspections to check for stolen items (more frequently if your reputation is low or if they've caught someone in your crew stealing), and notice if you're visibly carrying or wearing any stolen items. +- Option to get the items you're buying from an outpost store in your inventory immediately, as opposed to them being delivered to the sub at the start of the round. There's also now an anti-griefing setting for disabling this, in case you want to make sure a griefer can't easily gain access to dangerous items in the outposts. Players with campaign management permissions aren't affected by the setting. +- Added new variants of the Monsters Nearby and Submarine Flooded tracks. +- Minor adjustments to level layouts: all the levels now slope down a bit to give the impression you're actually heading deeper. +- Clicking on a door that's set to not toggle when clicked (= door that just send out an activate_out signal when clicked) doesn't activate the door's "toggle cooldown". +- Bots operating turrets can now lead targets (i.e. aim ahead of a moving target). Bots with a higher weapon skill are better at estimating the future position of the target. +- Minor balance change to sub vs sub combat. AI-controlled subs get at least 2 security instead of 1, so more turrets will be manned. +- Bots can see through windowed walls (also making it easier for them to spot stealing). +- Added bilge pumps and duct blocks throughout outposts. Otherwise the outposts can never drain if the player causes a flood. +- Removed karma restrictions from jobs. This feature wasn't communicated anywhere, and it often just seemed like a bug when someone didn't get assign the job they wanted due to their karma being too low. +- Added bullet casing particles to firearms. +- Adjusted default throwing pose a bit: characters don't extend their arm as high up, because it could cause the throwable item to hit the ceiling in roughly door-height rooms. +- Most message boxes can be closed by pressing enter (more specifically, all boxes that are created with just the "ok" button). +- ID cards left in a duffel bag are automatically removed at the end of the round. +- Added a light to handheld sonar to indicate when it's on + made the ping a bit higher in pitch to differentiate it from the sub's sonar. +- Reduced the forces applied on characters by fractal guardian's melee attacks. The previous values were so high they often lead to characters getting stunlocked when the attacks threw them against the walls of the ruin. + +Traitors: +- Three new Danger Level 1 traitor events: Exhibitionism, Firing Blanks, The Original Honkero. +- Five new multi-traitor events: Gnawing Cold, The Clobbery Robbery, Husk Roulette, Duck Side of the Moon, Powered by Faith. +- Fixed "Insurgency" achievement not unlocking when completing a traitor event. +- Fixed traitor-specific items sometimes being requested by stores. +- Fixed visibility checks in some traitor events having infinite range. +- Don't allow the victim's body to despawn in the "dead or alive" event because it'd interfere with completing the event (others finding the body). + +Optimization: +- Significant optimization to situations where a bot is idling and has trouble finding a path to another hull. Caused performance issues in colonies in particular. +- Fixed a memory leak that caused the memory usage to increase every round. +- Fixed firing turrets (or doing any other action that spawns lots of new entities) causing an enormous performance drop in some situations. Had to do with specific scripted events re-checking their targets whenever a new entity spawned, which was needlessly heavy and was done unnecessarily often. +- Miscellaneous small optimizations. +- Optimized labels (large labels with a scaled down texture in particular were unnecessarily heavy). +- Optimized Watcher's status effects. + +Multiplayer: +- Option to hide servers from the server list. +- Option to report inappropriate servers on the server list. +- Added "MinimumMidRoundSyncTimeout" server setting. Determines how long the server waits for a mid-round joining client to get in sync before disconnecting them. +- Fixed workshop item downloads sometimes getting stuck when opting to install server mods from the workshop. Happened if the mod had already been installed, was in the process of being installed, or if it couldn't be installed for some reason. +- Fixed dedicated servers with lots of mods enabled not showing up in the server browser. +- Fixed Text and NPCConversation files being required to be in sync between clients and the server, meaning if you were using a mod that changes the game to a different language, it'd get disabled when you joined a server that's not using the mod. +- Fixed lack of unban option in the "manage client" menu and the client context menu. + +Talents: +- Five new assistant talents: Starter Quest, Mule, Jenga Master, Indentured Servitude, Tasty Target. +- Fixed "junction junkie" not working on sonar monitors. +- Fixed a couple of inaccurate talent and mission descriptions in Russian. + +Skill gain balancing: +- Now takes longer to reach max skill level, the Helm skill especially was leveling way too fast. +- Helm skill levels up based on the submarine's speed. +- Added an exponent factor to the skill increases for diminishing returns at higher skill level. +- Increased skill gains at lower levels to compensate. +- Welding gives more skill. +- Fabricating items gives less skill. + +Fixes: +- Fixed "camera zoom effect" taking a very long time to appear when pressure is increasing inside the sub, giving you very little time to react when the pressure gets to a lethal level. +- Fixed turret lights always being forced on at the start of the round. +- Fixed lighting artifacts that looked like thin slivers of light and strange jagged lighting that sometimes occurred in corners of rooms or places where multiple walls meet. +- Fixed lights shining through obstacles in some very specific situations (one common spot was the windowed door at the left side of Dugong's command room). +- Fixed turret lights sometimes disappearing from view when you're far from the turret (but still close enough to see the light). +- Fixed "bad vibrations 2" event getting stuck if you choose the option to sleep. +- Fixed components disappearing from a circuit box if you delete one in the sub editor and undo the deletion. +- Fixed a rare crash when you'd selected text in a textbox and changed the language of the game to one where the word in the textbox is shorter than in the previous language. +- Fixed handcuffs appearing to continuously drop from characters in certain situations in multiplayer (more specifically, if the character has been disabled and re-enabled during the round). +- Workaround to a rare mystery issue that seems to sometimes cause save files to get corrupted. It seems in some situations the submarine doesn't get included in the save: now the game checks whether the submarine is included before attempting to save the file, and refuses to save if it isn't. This does not solve the underlying issue, but should make it easier for us to diagnose why the submarine gets left out, and prevent corrupting the save when it occurs. +- Fixed "operate turret" orders showing the order marker on the turret instead of the periscope. +- Fixed respawn shuttle lights shining through the top of the level. +- Fixed flares (or other "provocative" items) not attracting monsters when you're wearing a diving suit, because the diving suit was also considered a "provocative" item. +- Fixed ability to put items into circuit boxes by dragging and dropping and with the inventory hotkeys. This caused the components to end up in an invalid state, leading to errors when loading the next round. +- Fixed custom ID card tags not getting applied to ID cards when switching subs. +- Fixed subinventories constantly opening and closing when you're hovering the cursor over the inventory slot with 2 different containers equipped (e.g. 2 storage containers). +- Fixed bots being able to take ammo out from non-interactable items or items whose inventory is set to be inaccessible. +- Fixed progress bars being visible through walls when someone uses a repair tool, damages an item with a projectile or a melee weapon, or crowbars a door open. +- Fixed crashing if you're in an outpost level with no outpost. Should normally never happen, but can occur with e.g. outdated save files or mods. +- Fixed ballast flora spores (or any other level object with no actual sprite) not being visible. +- Fixed bots "cleaning up" items from inside artifact transport cases. +- Fixed ability to sell components from inside circuit boxes in outposts. +- Fixed outpost NPCs not caring about the players starting fires. +- Fixed purchased items sometimes spawning in the cargo crates when you've got a cargo mission active, making them potentially very hard to find. +- Fixed inability to select a component you've just placed in the circuit box until you move the component for the first time. +- Fixed health scanner HUD texts overlapping on high resolutions. +- Fixed "maintain position" marker being misplaced on the navigation terminal on some resolutions. +- Fixed chance of tainting the genetic material when refining sometimes being displayed incorrectly on research stations. +- Disable the health interface of the patient in the "good samaritan" event. Prevents being able to heal the opiate overdose "normally" before triggering the event. +- Fixed item hover texts (e.g. "[E] Repair") not refreshing when changing keybinds. +- Fixed fabricator listbox scroll always resetting to the top when someone selects the fabricator. +- Fixed bandoliers not affecting pulse laser fire rate. + +Modding: +- When running an outdated executable (i.e. a mod that overrides the game executable, but hasn't been updated yet after a new vanilla update has been released), there's a notification in the main menu and the "host server" menu warning you about the potential issues caused by running an outdated executable with the other up-to-date vanilla files. +- Console errors and warnings caused by modded content include the name of the mod to make it easier to diagnose which mod is causing some issue or whether it's an issue in the vanilla game. +- Support for overriding texts. Works using elements, the same way as overriding any other content. +- Fixed decorative sprites not being visible on items placed in a container. Did not affect any vanilla content (there were no decorative sprites on any item that can be visible in a container). +- Fixed characters with no AI defined causing crashes. +- Fixed inability to make StatusEffectAction modify the properties of an ItemComponent (only worked on the actual Item). +- Option to have multiple conditionals in CheckConditionalAction. +- Option to limit how many times a GoTo action can be executed (i.e. to create loops that only repeat a given number of times). +- TutorialHighlightAction can now be used in multiplayer too, renamed it as HighlightAction. +- Fixed first matching Containable definition of an ItemContainer determining the hiding, position and rotation of a contained item, disregarding which subcontainer the item is actually in. E.g. if a weapon had 2 subcontainers for magazines, both with a different ItemPos, the ItemPos defined in the first subcontainer would be used for both magazines. +- Fixed stun batons not accepting modded batteries because the RequiredItem s were configured using identifiers instead of tags. +- Fixed Character.CameraShake setting the camera shake regardless if the character the effect is being applied on is the controlled one or not. +- Location names can now be translated. In the vanilla game the names are only translated if you're playing in Chinese. +- Fixed some ConversationAction options going outside the bounds of the dialog if there's a very large number of them. +- Fixed inability to use %ModDir% in LevelGenerationParams.ForceBeaconStation. +- Fixed status effects configured to spawn an item in ContainedInventory not spawning the item if it can't go inside the first item in the contained inventory. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.1.19.3 ------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs b/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs new file mode 100644 index 000000000..128ead59e --- /dev/null +++ b/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs @@ -0,0 +1,66 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma; +using Barotrauma.Items.Components; +using FluentAssertions; +using FsCheck; +using Microsoft.Xna.Framework; +using Xunit; + +namespace TestProject; + +public sealed class FabricatorQualityRollTests +{ + [Fact] + public void TestPercentageChance() + { + Prop.ForAll( + Arb.Generate().Where(static i => i is <= 3 and >= 0).ToArbitrary(), + Arb.Generate().Where(static i => i is <= 100 and >= 50).ToArbitrary(), + Arb.Generate().Where(static i => i is <= 100 and >= 0).ToArbitrary(), (startingQuality, skillLevel, targetLevel) => + { + float plusOneProbability = 0f, + plusTwoProbability = 0f; + + if (skillLevel >= Fabricator.PlusOneQualityBonusThreshold) + { + var bonusChance1 = MathHelper.Lerp(targetLevel, Fabricator.PlusOneTarget, Fabricator.PlusOneLerp); + plusOneProbability = Fabricator.CalculateBonusRollPercentage(skillLevel, bonusChance1); + + if (skillLevel >= Fabricator.PlusTwoQualityBonusThreshold) + { + var bonusChance2 = MathHelper.Lerp(targetLevel, Fabricator.PlusTwoTarget, Fabricator.PlusTwoLerp); + plusTwoProbability = Fabricator.CalculateBonusRollPercentage(skillLevel, bonusChance2); + } + } + + var result = new Fabricator.QualityResult(startingQuality, plusOneProbability, plusTwoProbability); + + // iterate to confirm that the percentage chance is correct + const int iterations = 100000; + var plusOneCount = 0; + var plusTwoCount = 0; + for (int i = 0; i < iterations; i++) + { + int quality = result.RollQuality(); + if (quality == startingQuality + 1) + { + plusOneCount++; + } + else if (quality == startingQuality + 2) + { + plusTwoCount++; + } + } + + var iteratedPlusOneChance = plusOneCount / (float)iterations * 100f; + var iteratedPlusTwoChance = plusTwoCount / (float)iterations * 100f; + + // check that the percentage chance is within 3% of the expected value + result.TotalPlusOnePercentage.Should().BeApproximately(iteratedPlusOneChance, 3f); + result.TotalPlusTwoPercentage.Should().BeApproximately(iteratedPlusTwoChance, 3f); + }).QuickCheckThrowOnFailure(); + } +} \ No newline at end of file