From 8a2e2ea0ae197ed759caf27e69aa3103cfb60701 Mon Sep 17 00:00:00 2001 From: Markus Isberg Date: Fri, 10 Nov 2023 17:45:19 +0200 Subject: [PATCH 1/3] Unstable 1.2.1.0 --- .../ClientSource/Characters/Character.cs | 5 +- .../CircuitBox/CircuitBoxConnection.cs | 13 +- .../ClientSource/DebugConsole.cs | 18 +- .../Events/EventActions/ConversationAction.cs | 3 +- .../ClientSource/Events/EventManager.cs | 2 +- .../Events/Missions/CargoMission.cs | 3 +- .../ClientSource/GUI/GUIMessageBox.cs | 12 + .../ClientSource/GUI/GUINumberInput.cs | 67 ++--- .../ClientSource/GUI/Store.cs | 76 +++-- .../ClientSource/GUI/UISprite.cs | 2 +- .../ClientSource/GUI/UpgradeStore.cs | 6 +- .../ClientSource/GameSession/CargoManager.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 10 + .../GameModes/MultiPlayerCampaign.cs | 2 +- .../ClientSource/GameSession/RoundSummary.cs | 2 +- .../Items/Components/GeneticMaterial.cs | 6 +- .../Items/Components/ItemComponent.cs | 9 +- .../Items/Components/ItemContainer.cs | 3 +- .../Items/Components/Machines/Fabricator.cs | 16 +- .../Items/Components/Machines/Sonar.cs | 61 ++-- .../Items/Components/Machines/Steering.cs | 9 +- .../Items/Components/RepairTool.cs | 2 +- .../Items/Components/Signal/CircuitBox.cs | 2 +- .../Components/Signal/CustomInterface.cs | 7 +- .../Items/Components/StatusHUD.cs | 6 +- .../ClientSource/Items/Inventory.cs | 33 ++- .../ClientSource/Items/Item.cs | 31 +- .../ClientSource/Items/ItemPrefab.cs | 20 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 43 ++- .../BackgroundCreatureManager.cs | 2 +- .../ClientSource/Map/Levels/Level.cs | 6 +- .../ClientSource/Map/Lights/LightSource.cs | 2 +- .../ClientSource/Map/Map/Radiation.cs | 2 +- .../ClientSource/Map/MapEntity.cs | 143 +++++++-- .../ClientSource/Map/MapEntityPrefab.cs | 2 +- .../ClientSource/Map/RoundSound.cs | 20 +- .../ClientSource/Map/Structure.cs | 38 ++- .../ClientSource/Map/StructurePrefab.cs | 13 +- .../ClientSource/Map/Submarine.cs | 14 +- .../ClientSource/Networking/Client.cs | 6 +- .../ClientSource/Networking/GameClient.cs | 25 +- .../Networking/ServerList/ServerInfo.cs | 28 +- .../ClientSource/Networking/ServerSettings.cs | 4 + .../ClientSource/Particles/ParticleEmitter.cs | 22 +- .../ClientSource/Particles/ParticlePrefab.cs | 3 +- .../BarotraumaClient/ClientSource/Program.cs | 3 + .../ClientSource/Screens/MainMenuScreen.cs | 115 ++++++-- .../ClientSource/Screens/NetLobbyScreen.cs | 47 ++- .../ServerListScreen/ServerListScreen.cs | 2 +- .../ClientSource/Screens/SubEditorScreen.cs | 271 ++++++++++-------- .../Serialization/SerializableEntityEditor.cs | 31 +- .../ClientSource/Sounds/OggSound.cs | 2 +- .../ClientSource/Sounds/Sound.cs | 6 +- .../ClientSource/Sprite/Sprite.cs | 43 ++- .../ClientSource/Steam/Lobby.cs | 4 + .../ClientSource/Utils/TextureLoader.cs | 9 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/DebugConsole.cs | 130 ++++++--- .../Events/EventActions/EventLogAction.cs | 3 +- .../BarotraumaServer/ServerSource/GameMain.cs | 25 +- .../ServerSource/GameSession/CargoManager.cs | 32 +-- .../GameSession/GameModes/CampaignMode.cs | 12 +- .../GameModes/CharacterCampaignData.cs | 16 +- .../GameModes/MultiPlayerCampaign.cs | 59 +++- .../Items/Components/Signal/CircuitBox.cs | 9 +- .../ServerSource/Items/Inventory.cs | 3 +- .../BarotraumaServer/ServerSource/Map/Hull.cs | 4 +- .../ServerSource/Networking/ChatMessage.cs | 115 ++++---- .../ServerSource/Networking/GameServer.cs | 90 +++--- .../ServerSource/Networking/ServerSettings.cs | 2 +- .../ServerSource/Networking/Voting.cs | 12 +- .../ServerSource/Steam/SteamManager.cs | 15 +- .../ServerSource/Traitors/TraitorManager.cs | 12 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Data/campaignsettings.xml | 12 +- .../Characters/AI/EnemyAIController.cs | 15 +- .../Characters/AI/HumanAIController.cs | 35 ++- .../Characters/AI/Objectives/AIObjective.cs | 11 - .../Objectives/AIObjectiveCheckStolenItems.cs | 160 +++++++++++ .../AI/Objectives/AIObjectiveFindThieves.cs | 152 ++++++++++ .../AI/Objectives/AIObjectiveLoop.cs | 2 +- .../AI/Objectives/AIObjectiveManager.cs | 4 + .../SharedSource/Characters/AI/Order.cs | 3 +- .../Characters/Animation/AnimController.cs | 3 +- .../Animation/HumanoidAnimController.cs | 205 +++++++------ .../Characters/Animation/Ragdoll.cs | 13 +- .../SharedSource/Characters/Attack.cs | 68 +++-- .../SharedSource/Characters/Character.cs | 110 ++++--- .../SharedSource/Characters/CharacterInfo.cs | 19 +- .../Characters/CharacterPrefab.cs | 3 +- .../Health/Afflictions/AfflictionHusk.cs | 27 +- .../Health/Afflictions/AfflictionPrefab.cs | 27 +- .../Characters/Health/CharacterHealth.cs | 25 +- .../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 | 9 +- .../Characters/Params/EditableParams.cs | 6 +- .../Params/Ragdoll/RagdollParams.cs | 50 ++-- .../AbilityConditionals/AbilityCondition.cs | 5 +- .../AbilityConditionAffliction.cs | 5 +- .../AbilityConditionAttackData.cs | 3 +- .../AbilityConditionCharacter.cs | 5 +- .../AbilityConditionData.cs | 6 +- .../AbilityConditionItem.cs | 3 +- .../AbilityConditionMission.cs | 3 +- .../AbilityConditionHasPermanentStat.cs | 3 +- .../AbilityConditionHasStatusTag.cs | 3 +- .../Talents/Abilities/CharacterAbility.cs | 26 +- .../Abilities/CharacterAbilityApplyForce.cs | 3 +- ...ilityApplyStatusEffectsToApprenticeship.cs | 3 +- .../CharacterAbilityGainSimultaneousSkill.cs | 3 +- .../CharacterAbilityGiveAffliction.cs | 6 +- .../CharacterAbilityGiveExperience.cs | 16 +- .../Abilities/CharacterAbilityGiveMoney.cs | 3 +- .../CharacterAbilityGivePermanentStat.cs | 3 +- .../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 | 44 ++- .../Characters/Talents/CharacterTalent.cs | 17 +- .../Characters/Talents/TalentPrefab.cs | 3 +- .../Characters/Talents/TalentTree.cs | 12 +- .../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 | 8 + .../ContentPackage/ContentPackage.cs | 5 +- .../ContentManagement/ContentXElement.cs | 3 +- .../SharedSource/DebugConsole.cs | 110 ++++--- .../SharedSource/Events/ArtifactEvent.cs | 10 +- .../SharedSource/Events/Event.cs | 2 +- .../EventActions/CheckConditionalAction.cs | 9 +- .../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 +- .../Events/EventActions/ConversationAction.cs | 3 +- .../Events/EventActions/CountTargetsAction.cs | 6 +- .../Events/EventActions/EventAction.cs | 11 +- .../Events/EventActions/EventLogAction.cs | 6 +- .../EventActions/EventObjectiveAction.cs | 6 +- .../Events/EventActions/GiveExpAction.cs | 3 +- .../Events/EventActions/GiveSkillExpAction.cs | 3 +- .../Events/EventActions/MissionAction.cs | 15 +- .../Events/EventActions/MissionStateAction.cs | 3 +- .../EventActions/ModifyLocationAction.cs | 9 +- .../EventActions/NPCChangeTeamAction.cs | 3 +- .../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 | 66 +++-- .../Events/EventActions/TriggerEventAction.cs | 3 +- .../EventActions/TutorialHighlightAction.cs | 3 +- .../WaitForItemFabricatedAction.cs | 3 +- .../SharedSource/Events/EventManager.cs | 3 +- .../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/EndMission.cs | 21 +- .../Events/Missions/EscortMission.cs | 11 +- .../Events/Missions/MineralMission.cs | 6 +- .../SharedSource/Events/Missions/Mission.cs | 26 +- .../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 | 95 ++++-- .../SharedSource/Events/ScriptedEvent.cs | 17 +- .../SharedSource/GameSession/CargoManager.cs | 174 +++++++---- .../SharedSource/GameSession/CrewManager.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 59 +++- .../GameSession/GameModes/CampaignSettings.cs | 6 + .../GameModes/MultiPlayerCampaign.cs | 7 +- .../GameSession/UpgradeManager.cs | 34 ++- .../SharedSource/Items/CharacterInventory.cs | 15 +- .../SharedSource/Items/Components/Door.cs | 27 ++ .../Items/Components/ElectricalDischarger.cs | 3 +- .../Items/Components/GeneticMaterial.cs | 6 +- .../SharedSource/Items/Components/Growable.cs | 3 +- .../Items/Components/Holdable/RangedWeapon.cs | 3 +- .../Items/Components/Holdable/RepairTool.cs | 8 +- .../Items/Components/Holdable/Throwable.cs | 2 +- .../Items/Components/ItemComponent.cs | 44 ++- .../Items/Components/ItemContainer.cs | 9 +- .../Items/Components/Machines/Controller.cs | 5 +- .../Items/Components/Machines/Fabricator.cs | 14 +- .../SharedSource/Items/Components/Planter.cs | 3 + .../Items/Components/Power/PowerTransfer.cs | 2 +- .../Items/Components/Projectile.cs | 3 +- .../SharedSource/Items/Components/Quality.cs | 3 +- .../SharedSource/Items/Components/Scanner.cs | 3 +- .../Items/Components/Signal/ButtonTerminal.cs | 3 +- .../Items/Components/Signal/CircuitBox.cs | 51 +++- .../Items/Components/TriggerComponent.cs | 3 +- .../SharedSource/Items/Components/Wearable.cs | 3 +- .../SharedSource/Items/Inventory.cs | 8 +- .../SharedSource/Items/Item.cs | 126 ++++++-- .../SharedSource/Items/ItemInventory.cs | 4 +- .../SharedSource/Items/ItemPrefab.cs | 130 ++++++--- .../SharedSource/Items/RelatedItem.cs | 4 +- .../SharedSource/Map/DummyFireSource.cs | 3 +- .../SharedSource/Map/Explosion.cs | 64 +++-- .../SharedSource/Map/FireSource.cs | 17 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 36 ++- .../BarotraumaShared/SharedSource/Map/Hull.cs | 6 +- .../SharedSource/Map/IDamageable.cs | 2 +- .../Map/Levels/DestructibleLevelWall.cs | 2 +- .../SharedSource/Map/Levels/Level.cs | 101 +++++-- .../Map/Levels/LevelObjects/LevelObject.cs | 2 +- .../Levels/LevelObjects/LevelObjectManager.cs | 3 +- .../SharedSource/Map/Map/Location.cs | 44 +-- .../Map/Map/LocationTypeChange.cs | 14 +- ...onStationInfo.cs => ExtraSubmarineInfo.cs} | 64 +++-- .../Map/Outposts/OutpostGenerationParams.cs | 2 +- .../SharedSource/Map/Structure.cs | 181 ++++++++---- .../SharedSource/Map/StructurePrefab.cs | 9 +- .../SharedSource/Map/Submarine.cs | 9 +- .../SharedSource/Map/SubmarineBody.cs | 5 +- .../SharedSource/Map/SubmarineInfo.cs | 18 +- .../SharedSource/Networking/BanList.cs | 10 + .../SharedSource/Networking/Client.cs | 6 +- .../SharedSource/Networking/ServerSettings.cs | 7 + .../Editable/ConditionallyEditable.cs | 66 +++++ .../Serialization/Editable/Editable.cs | 55 ++++ .../SerializableProperty.cs | 135 +-------- .../Serialization/StructSerialization.cs | 2 +- .../SharedSource/Settings/GameSettings.cs | 27 +- .../SharedSource/Sprite/Sprite.cs | 3 +- .../StatusEffects/PropertyConditional.cs | 4 +- .../StatusEffects/StatusEffect.cs | 101 ++++--- .../BarotraumaShared/SharedSource/Tags.cs | 4 +- .../Traitors/TraitorEventPrefab.cs | 5 +- .../SharedSource/Upgrades/Upgrade.cs | 3 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 26 +- .../SharedSource/Utils/MathUtils.cs | 48 ++-- Barotrauma/BarotraumaShared/changelog.txt | 80 ++++++ 268 files changed, 4076 insertions(+), 1843 deletions(-) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCheckStolenItems.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.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%) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 115a95e77..a223fe878 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -336,7 +336,10 @@ namespace Barotrauma float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; if (pressure > 0.0f) { - float zoomInEffectStrength = MathHelper.Clamp(pressure / 100.0f, 0.1f, 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); cam.Zoom = MathHelper.Lerp(cam.Zoom, cam.DefaultZoom + (Math.Max(pressure, 10) / 150.0f) * Rand.Range(0.9f, 1.1f), zoomInEffectStrength); diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs index 7fea1d1f1..ea89c9f57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxConnection.cs @@ -8,24 +8,25 @@ namespace Barotrauma { internal abstract partial class CircuitBoxConnection { - public string Name => Label.Value.Value; + public string Name => Connection.Name; + public CircuitBoxLabel Label { get; private set; } private Sprite? knobSprite, screwSprite, connectorSprite; - private static int padding => GUI.IntScale(8); + private static int Padding => GUI.IntScale(8); private Option tooltip = Option.None; private partial void InitProjSpecific(CircuitBox circuitBox) { - Label = new CircuitBoxLabel(Connection.Name, GUIStyle.SubHeadingFont); + Label = new CircuitBoxLabel(Connection.DisplayName, GUIStyle.SubHeadingFont); knobSprite = circuitBox.ConnectionSprite; screwSprite = circuitBox.ConnectionScrewSprite; connectorSprite = circuitBox.WireConnectorSprite; - Length = Rect.Width + padding + Label.Size.X; + Length = Rect.Width + Padding + Label.Size.X; } public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, Vector2 parentPos, Color color) @@ -41,11 +42,11 @@ namespace Barotrauma float xPos; if (IsOutput) { - xPos = drawRect.Left - padding - Label.Size.X; + xPos = drawRect.Left - Padding - Label.Size.X; } else { - xPos = drawRect.Right + padding; + xPos = drawRect.Right + Padding; } Vector2 stringPos = new Vector2(xPos, drawRect.Center.Y - Label.Size.Y / 2f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index d355c7cb9..de3f40011 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); @@ -706,6 +706,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); 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/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 27007040d..6b408b9a3 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; 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/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/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 34b935f84..270c2e6ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -341,9 +341,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 +583,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 +922,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 +1242,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 +1746,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 +1956,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,13 +2089,49 @@ 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()) + { + var 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?.Name)); + dialog.Buttons[0].OnClicked += dialog.Close; + } + } + return false; } 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/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..4c143b2ee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -121,6 +121,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)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index a3be9801c..8c408cf95 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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 96fb2522a..33f02d0fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -98,7 +98,7 @@ namespace Barotrauma if (gameSession.Missions.Any(m => m is CombatMission)) { crewHeader.Text = CombatMission.GetTeamName(CharacterTeamType.Team1); - GUIFrame crewFrame2 = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.45f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); + GUIFrame crewFrame2 = new GUIFrame(new RectTransform(crewFrame.RectTransform.RelativeSize, background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); rightPanels.Add(crewFrame2); GUIFrame crewFrameInner2 = new GUIFrame(new RectTransform(new Point(crewFrame2.Rect.Width - padding * 2, crewFrame2.Rect.Height - padding * 2), crewFrame2.RectTransform, Anchor.Center), style: "InnerFrame"); var crewContent2 = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), crewFrameInner2.RectTransform, Anchor.Center)) 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..b90e5bac4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -497,7 +497,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 +517,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 +530,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; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 830703d01..35799ed55 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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index c86d674e5..f300ae562 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -393,6 +393,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 +786,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 +795,11 @@ namespace Barotrauma.Items.Components { if (child.Enabled) { - child.Visible = recipeVisible; + if (child.Visible != recipeVisible) + { + child.Visible = recipeVisible; + visibleElementsChanged = true; + } } recipeVisible = false; } @@ -802,8 +809,11 @@ namespace Barotrauma.Items.Components } } - itemList.UpdateScrollBarSize(); - itemList.BarScroll = 0.0f; + if (visibleElementsChanged) + { + itemList.UpdateScrollBarSize(); + itemList.BarScroll = 0.0f; + } } public bool ClearFilter() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index e06de7a92..a5d5766f1 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); } } @@ -987,7 +995,7 @@ namespace Barotrauma.Items.Components (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 }) @@ -997,7 +1005,7 @@ namespace Barotrauma.Items.Components (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..c3827e5b5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -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/CircuitBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs index 7dafcf617..3f466be5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs @@ -269,7 +269,7 @@ namespace Barotrauma.Items.Components resource = ItemPrefab.Prefabs[Tags.FPGACircuit]; } - AddComponentInternal(ICircuitBoxIdentifiable.FindFreeID(Components), prefab, resource, pos, static delegate { }); + AddComponentInternal(ICircuitBoxIdentifiable.FindFreeID(Components), prefab, resource, pos, Character.Controlled, onItemSpawned: null); return; } 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/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index ac8f2e935..5163a7196 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; @@ -1505,14 +1505,28 @@ namespace Barotrauma int stackAmount = DraggingItems.Count; if (selectedSlot?.ParentInventory != null) { - stackAmount = Math.Min( - stackAmount, - selectedSlot.ParentInventory.HowManyCanBePut(draggedItem.Prefab, selectedSlot.SlotIndex, draggedItem.Condition)); + if (selectedSlot.Item?.OwnInventory != null) + { + int maxAmountPerSlot = 0; + for (int i = 0; i < SelectedSlot.Item.OwnInventory.Capacity; i++) + { + maxAmountPerSlot = Math.Max( + maxAmountPerSlot, + selectedSlot.Item.OwnInventory.HowManyCanBePut(draggedItem.Prefab, i, draggedItem.Condition, ignoreItemsInSlot: true)); + } + stackAmount = Math.Min(stackAmount, maxAmountPerSlot); + } + else + { + stackAmount = Math.Min( + stackAmount, + selectedSlot.ParentInventory.HowManyCanBePut(draggedItem.Prefab, selectedSlot.SlotIndex, draggedItem.Condition, ignoreItemsInSlot: true)); + } } Vector2 stackCountPos = itemPos + Vector2.One * iconSize * 0.25f; string stackCountText = "x" + stackAmount; GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); - GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, GUIStyle.TextColorBright); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, GUIStyle.TextColorBright); } } } @@ -1908,6 +1922,15 @@ 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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 0f42a313c..e2cb908f9 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) @@ -333,9 +333,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; @@ -406,12 +404,14 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + + Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color); 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, + size, color: decorativeSpriteColor, textureScale: Vector2.One * Scale, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } @@ -437,13 +437,15 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + + Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color); 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, + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), decorativeSpriteColor, RotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } @@ -618,6 +620,13 @@ namespace Barotrauma } return origin; } + + Color GetSpriteColor(Color defaultColor) + { + return + overrideColor ?? + (IsIncludedInSelection && editing ? GUIStyle.Blue : this.GetSpriteColor(defaultColor: defaultColor, withHighlight: true)); + } } partial void OnCollisionProjSpecific(float impact) @@ -852,7 +861,7 @@ namespace Barotrauma CanBeFocused = true }; - 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"), Enabled = Prefab.CanFlipX, @@ -863,10 +872,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"), Enabled = Prefab.CanFlipY, @@ -877,9 +888,11 @@ namespace Barotrauma me.FlipY(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } + ColorFlipButton(button, FlippedY); 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"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 49c3eb0e3..995e56880 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,23 @@ 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, new Vector2(placeRect.Center.X, -(placeRect.Y - placeRect.Height / 2)), SpriteColor * 0.8f, scale: scale, rotate: rotation); } 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, + new Vector2(position.X, -position.Y), + placeSize, + rotation: rotation, + textureScale: Vector2.One * scale, + color: SpriteColor * 0.8f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 5eb9bcd95..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, @@ -300,13 +309,27 @@ namespace Barotrauma " - Oxygen: " + ((int)OxygenPercentage), new Vector2(drawRect.X + 5, -drawRect.Y + 5), Color.White); GUIStyle.SmallFont.DrawString(spriteBatch, waterVolume + " / " + Volume, new Vector2(drawRect.X + 5, -drawRect.Y + 20), Color.White); - GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * Math.Min(waterVolume / Volume, 1.0f))), Color.Cyan, true); - if (WaterVolume > Volume) + if (WaterVolume > 0) { - float maxExcessWater = Volume * MaxCompress; - GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * (waterVolume - Volume) / maxExcessWater)), GUIStyle.Red, true); + drawProgressBar(50, new Point(0, 0), Math.Min(waterVolume / Volume, 1.0f), Color.Cyan); + if (WaterVolume > Volume) + { + float maxExcessWater = Volume * MaxCompress; + drawProgressBar(50, new Point(0, 0), (waterVolume - Volume) / maxExcessWater, GUIStyle.Red); + } + } + if (lethalPressure > 0) + { + drawProgressBar(50, new Point(20, 0), lethalPressure / 100.0f, Color.Red); + } + + void drawProgressBar(int height, Point offset, float fillAmount, Color color) + { + GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X - 2 + offset.X, -drawRect.Y - 2 + drawRect.Height / 2 + offset.Y, 14, height+4), Color.Black * 0.8f, depth: 0.01f, isFilled: true); + + int barHeight = (int)(fillAmount * height); + GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X + offset.X, -drawRect.Y + drawRect.Height / 2 + height - barHeight + offset.Y, 10, barHeight), color, isFilled: true); } - GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, 100), Color.Black); foreach (FireSource fs in FireSources) { @@ -732,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/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..5c54904ba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -53,7 +53,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 +61,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/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 7606b40c6..777b0ebd9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -1318,7 +1318,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 { 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..a9ab8cc23 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(); } } } @@ -494,6 +493,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 +798,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 +819,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 +844,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 +891,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 +1136,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 +1169,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 +1189,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 +1300,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 +1317,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)); } 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..d1453f665 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) => @@ -357,8 +362,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 +375,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 +394,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 +425,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 +440,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 +461,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..06e17348a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -94,16 +94,21 @@ 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, + position, + placeRect.Size.ToVector2(), + color: Color.White * 0.8f, + origin: placeRect.Size.ToVector2() * 0.5f, + rotation: rotation, textureScale: TextureScale * scale); Sprite.effects = oldEffects; 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/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..6fa7ec90f 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(); @@ -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..ab0cc11ea 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) @@ -309,6 +314,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 +398,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 +445,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 a3d44b5d6..4dd29adfa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -261,6 +261,9 @@ namespace Barotrauma { crashHeader += " " + exception.TargetSite.ToString(); } + //log the message separately, so the same error messages get grouped as the same error in GA + //(the full crash report tends to always have some differences between clients, so they get displayed separately) + GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Critical, crashHeader); GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Critical, crashHeader + "\n\n" + sb.ToString()); GameAnalyticsManager.ShutDown(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index bd977496b..143100d4d 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") @@ -587,7 +611,9 @@ namespace Barotrauma GameMain.SubEditorScreen?.ClearBackedUpSubInfo(); Submarine.Unload(); - + + versionMismatchWarning.Visible = GameMain.Version < ContentPackageManager.VanillaCorePackage.GameVersion; + ResetButtonStates(null); } @@ -663,7 +689,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 +1509,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..7e8b006c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -2389,10 +2389,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 +2601,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 +2640,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 +3170,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..f08ce1d14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -655,7 +655,7 @@ namespace Barotrauma ScrollBarVisible = true, OnSelected = (btn, obj) => { - if (!(obj is ServerInfo serverInfo)) { return false; } + if (obj is not ServerInfo serverInfo) { return false; } joinButton.Enabled = true; selectedServer = Option.Some(serverInfo); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 1fd3ca751..a48cb05d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1289,7 +1289,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; } @@ -2365,49 +2366,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 +2679,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; @@ -4439,6 +4454,7 @@ namespace Barotrauma MapEntity.SelectEntity(itemContainer); dummyCharacter.SelectedItem = itemContainer; FilterEntities(entityFilterBox.Text); + MapEntity.StopSelection(); } /// @@ -5556,11 +5572,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()) + { + if (linkedItem.OwnInventory?.visualSlots != null) + { + TryDragItemsToItem(linkedItem); + } + } + } + + void TryDragItemsToItem(Item item) + { + foreach (ItemContainer ic in item.GetComponents()) + { + 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 +5605,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/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index f816a71a8..25ea1f177 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Sounds 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); 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/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index e599e9c41..f76e97b23 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,35 @@ namespace Barotrauma } } - public void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, + 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) { if (Texture == null) { return; } + + bool flipHorizontal = (effects & SpriteEffects.FlipHorizontally) != 0; + bool flipVertical = (effects & 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, effects, depth ?? this.depth); + } //wrap the drawOffset inside the sourceRect drawOffset.X = (drawOffset.X / scale.X) % sourceRect.Width; @@ -368,8 +386,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 +434,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 +451,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/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/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/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 9a1fe727a..e31bc063d 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.18.1 + 1.2.1.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 7c9514990..42698bdfd 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.18.1 + 1.2.1.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index a388bc1f5..38e7e8161 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.1.18.1 + 1.2.1.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 7fd3c7774..093a855f3 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.18.1 + 1.2.1.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 1f5c8ac99..04527ab69 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.18.1 + 1.2.1.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index fd59111ec..532c057e7 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); } } } @@ -1156,6 +1158,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; } @@ -1432,7 +1451,6 @@ namespace Barotrauma GameMain.Server.PrintSenderTransters(); })); - commands.Add(new Command("forcelocationtypechange", "", (string[] args) => { if (GameMain.Server == null || GameMain.GameSession?.Campaign == null) { return; } @@ -1568,6 +1586,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 +1782,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 +2217,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 +2250,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 +2273,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 +2302,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 +2354,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); } } } @@ -2585,10 +2631,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 +2658,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..66e28c861 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/EventLogAction.cs @@ -27,7 +27,8 @@ 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) 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..69a72d1b7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -806,7 +806,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 +894,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 +905,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 +948,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 +979,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 +987,7 @@ namespace Barotrauma if (newItems.Any()) { - CargoManager.OnNewItemsPurchased(storeIdentifier, newItems, sender); + CargoManager.LogNewItemPurchases(storeIdentifier, newItems, sender); } } @@ -1015,7 +1052,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); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs index 010111c87..b0b24e75c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -1,11 +1,11 @@ #nullable enable +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Barotrauma.Networking; -using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components { @@ -138,7 +138,7 @@ namespace Barotrauma.Items.Components return; } - bool result = AddComponentInternal(id, prefab, resource.Prefab, data.Position, it => + bool result = AddComponentInternal(id, prefab, resource.Prefab, data.Position, c.Character, it => { CreateServerEvent(new CircuitBoxServerCreateComponentEvent(it.ID, resource.Prefab.UintIdentifier, id, data.Position)); }); @@ -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..d2afe45b1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -110,7 +110,8 @@ namespace Barotrauma (pickable.IsAttached && !pickable.PickingDone) || item.AllowedSlots.None()) { - 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/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/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index e9216ab7b..93151a357 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -80,58 +80,10 @@ namespace Barotrauma.Networking { c.LastSentChatMessages.RemoveRange(0, c.LastSentChatMessages.Count - 10); } - - float similarity = 0.0f; - for (int i = 0; i < c.LastSentChatMessages.Count; i++) - { - float closeFactor = 1.0f / (c.LastSentChatMessages.Count - i); - - if (string.IsNullOrEmpty(txt)) - { - similarity += closeFactor; - } - else - { - int levenshteinDist = ToolBox.LevenshteinDistance(txt, c.LastSentChatMessages[i]); - similarity += Math.Max((txt.Length - levenshteinDist) / (float)txt.Length * closeFactor, 0.0f); - } - } - //order/report messages can be sent a little faster than normal messages without triggering the spam filter - if (orderMsg != null) - { - similarity *= 0.25f; - } - - bool isSpamExempt = RateLimiter.IsExempt(c); - - if (similarity + c.ChatSpamSpeed > 5.0f && !isSpamExempt) - { - GameMain.Server.KarmaManager.OnSpamFilterTriggered(c); - - c.ChatSpamCount++; - if (c.ChatSpamCount > 3) - { - //kick for spamming too much - GameMain.Server.KickClient(c, TextManager.Get("SpamFilterKicked").Value); - } - else - { - ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); - c.ChatSpamTimer = 10.0f; - GameMain.Server.SendDirectChatMessage(denyMsg, c); - } - return; - } - - c.ChatSpamSpeed += similarity + 0.5f; - - if (c.ChatSpamTimer > 0.0f && !isSpamExempt) - { - ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); - c.ChatSpamTimer = 10.0f; - GameMain.Server.SendDirectChatMessage(denyMsg, c); - return; - } + //order/report messages can be sent a little faster than normal messages without triggering the spam filter; + float similarityMultiplier = orderMsg != null ? 0.25f : 1.0f; + HandleSpamFilter(c, txt, out bool flaggedAsSpam, similarityMultiplier); + if (flaggedAsSpam) { return; } if (type == ChatMessageType.Order) { @@ -177,6 +129,65 @@ namespace Barotrauma.Networking } } + /// + /// Increase the client's chat spam speed and check whether the spam filter should kick in + /// + public static void HandleSpamFilter(Client c, string messageText, out bool flaggedAsSpam, float similarityMultiplier = 1.0f) + { + float similarity = 0.0f; + for (int i = 0; i < c.LastSentChatMessages.Count; i++) + { + float closeFactor = 1.0f / (c.LastSentChatMessages.Count - i); + + if (string.IsNullOrEmpty(messageText)) + { + similarity += closeFactor; + } + else + { + int levenshteinDist = ToolBox.LevenshteinDistance(messageText, c.LastSentChatMessages[i]); + similarity += Math.Max((messageText.Length - levenshteinDist) / (float)messageText.Length * closeFactor, 0.0f); + } + } + + similarity *= similarityMultiplier; + + bool isSpamExempt = RateLimiter.IsExempt(c); + + if (similarity + c.ChatSpamSpeed > 5.0f && !isSpamExempt) + { + GameMain.Server.KarmaManager.OnSpamFilterTriggered(c); + + c.ChatSpamCount++; + if (c.ChatSpamCount > 3) + { + //kick for spamming too much + GameMain.Server.KickClient(c, TextManager.Get("SpamFilterKicked").Value); + } + else + { + ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); + c.ChatSpamTimer = 10.0f; + GameMain.Server.SendDirectChatMessage(denyMsg, c); + } + flaggedAsSpam = true; + return; + } + + c.ChatSpamSpeed += similarity + 0.5f; + + if (c.ChatSpamTimer > 0.0f && !isSpamExempt) + { + ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); + c.ChatSpamTimer = 10.0f; + GameMain.Server.SendDirectChatMessage(denyMsg, c); + flaggedAsSpam = true; + return; + } + + flaggedAsSpam = false; + } + public int EstimateLengthBytesServer(Client c) { int length = 1 + //(byte)ServerNetObject.CHAT_MESSAGE diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 34f6cd497..efd98d89e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -369,6 +369,9 @@ namespace Barotrauma.Networking if (!character.ClientDisconnected) { continue; } Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c)); + bool canOwnerTakeControl = + owner != null && owner.InGame && !owner.NeedsMidRoundSync && + (!ServerSettings.AllowSpectating || !owner.SpectateOnly); if (!character.IsDead) { character.KillDisconnectedTimer += deltaTime; @@ -379,18 +382,19 @@ namespace Barotrauma.Networking character.Kill(CauseOfDeathType.Disconnected, null); continue; } - if (owner != null && owner.InGame && !owner.NeedsMidRoundSync && - (!ServerSettings.AllowSpectating || !owner.SpectateOnly)) + if (canOwnerTakeControl) { SetClientCharacter(owner, character); } } - else if (owner != null && + else if (canOwnerTakeControl && character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected && character.CharacterHealth.VitalityDisregardingDeath > 0) { + //create network event immediately to ensure the character is revived client-side + //before the client gains control of it (normally status events are created periodically) + character.Revive(removeAfflictions: false, createNetworkEvent: true); SetClientCharacter(owner, character); - character.Revive(removeAfflictions: false); } } @@ -2534,7 +2538,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); } } @@ -2581,6 +2585,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); @@ -3706,8 +3711,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(); @@ -3729,10 +3738,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}."); } } @@ -3751,6 +3761,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); } @@ -3769,6 +3780,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); } } @@ -3780,7 +3792,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); } } @@ -3798,32 +3814,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++) @@ -3834,12 +3824,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); @@ -3855,7 +3850,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) @@ -3871,19 +3868,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}."); } } } 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/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 5afb8d321..81c472caf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -345,9 +345,15 @@ namespace Barotrauma sender.SetVote(voteType, client); if (client?.Character != null) { - GameMain.Server.SendChatMessage( - TextManager.GetWithVariable("traitor.blamebutton.dialog", "[name]", client.Character.DisplayName).Value, - ChatMessageType.Radio, senderClient: sender, senderCharacter: sender.Character); + string msg = TextManager.GetWithVariable("traitor.blamebutton.dialog", "[name]", client.Character.DisplayName).Value; + ChatMessage.HandleSpamFilter(sender, msg, out bool flaggedAsSpam); + if (!flaggedAsSpam) + { + GameMain.Server.SendChatMessage( + msg, + ChatMessageType.Radio, senderClient: sender, senderCharacter: sender.Character); + sender.LastSentChatMessages.Add(msg); + } } } break; 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..55eed416d 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); @@ -263,7 +264,8 @@ namespace Barotrauma { 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."); + $"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); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 72495dd7f..be16280af 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.1.18.1 + 1.2.1.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/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 9495c3e08..adbf7e933 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 + "\" - no AI element found.", + contentPackage: c.Prefab?.ContentPackage); outsideSteering = new SteeringManager(this); insideSteering = new IndoorsSteeringManager(this, false, false); return; @@ -330,7 +331,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); } @@ -2503,7 +2505,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 +2543,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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 43378e91a..95bae3353 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -167,10 +167,6 @@ namespace Barotrauma public HumanAIController(Character c) : base(c) { - if (!c.IsHuman) - { - throw new Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!"); - } insideSteering = new IndoorsSteeringManager(this, true, false); outsideSteering = new SteeringManager(this); objectiveManager = new AIObjectiveManager(c); @@ -1800,7 +1796,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)) { @@ -1861,16 +1857,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 @@ -1881,7 +1872,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)) { @@ -1902,6 +1893,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: () => @@ -1919,6 +1914,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/AIObjectiveFindThieves.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs new file mode 100644 index 000000000..bff65d6b5 --- /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)) + { + 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/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..4c862b4aa 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; } } 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..4004391cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -150,7 +150,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 +243,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 +279,7 @@ namespace Barotrauma if (!character.CanMove) { - if (fallingProneAnimTimer < FallingProneAnimDuration) + if (fallingProneAnimTimer < FallingProneAnimDuration && onGround) { fallingProneAnimTimer += deltaTime; UpdateFallingProne(1.0f); @@ -285,7 +288,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 +394,12 @@ namespace Barotrauma DragCharacter(character.SelectedCharacter, deltaTime); } + if (Anim != Animation.CPR) + { + cprAnimTimer = 0.0f; + cprPumpTimer = 0.0f; + } + switch (Anim) { case Animation.Climbing: @@ -648,14 +662,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; } @@ -1318,14 +1324,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 +1353,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 +1364,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 +1407,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 +1422,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 +1469,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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index c9ac1f88b..048d81e17 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); + } } } } 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 2aeee3c03..d2f146fa7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -1098,6 +1098,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; @@ -1223,19 +1232,15 @@ namespace Barotrauma public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool spawnInitialItems = true) { Character newCharacter = null; - if (prefab.Identifier != CharacterPrefab.HumanSpeciesName) + if (prefab.Identifier != CharacterPrefab.HumanSpeciesName || hasAi) { var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems); - var ai = new EnemyAIController(aiCharacter, seed); + + var ai = (prefab.Identifier == CharacterPrefab.HumanSpeciesName || aiCharacter.Params.UseHumanAI) ? + new HumanAIController(aiCharacter) as AIController : + new EnemyAIController(aiCharacter, seed); aiCharacter.SetAI(ai); - newCharacter = aiCharacter; - } - else if (hasAi) - { - var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems); - var ai = new HumanAIController(aiCharacter); - aiCharacter.SetAI(ai); - newCharacter = aiCharacter; + newCharacter = aiCharacter; } else { @@ -1282,7 +1287,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) { @@ -1406,7 +1412,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; @@ -2407,6 +2414,11 @@ namespace Barotrauma } else if (body.UserData is Item item) { + if (item.GetComponent() is { HasWindow: true } door) + { + if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; } + } + return item != target; } return true; @@ -2768,9 +2780,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; + } } } @@ -2797,7 +2817,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; } @@ -3301,10 +3326,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) { @@ -3980,15 +4002,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) { @@ -4000,7 +4022,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; @@ -4129,12 +4160,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; @@ -4167,7 +4198,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(); } @@ -4200,18 +4231,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; @@ -4656,7 +4686,7 @@ namespace Barotrauma } partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log); - public void Revive(bool removeAfflictions = true) + public void Revive(bool removeAfflictions = true, bool createNetworkEvent = false) { if (Removed) { @@ -4705,7 +4735,11 @@ namespace Barotrauma limb.IsSevered = false; } - GameMain.GameSession?.ReviveCharacter(this); + GameMain.GameSession?.ReviveCharacter(this); + if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true }) + { + GameMain.NetworkMember.CreateEntityEvent(this, new CharacterStatusEventData()); + } } public override void Remove() @@ -4986,7 +5020,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.ConnectedDoor is { HasWindow: true }) && + g.linkedTo.Contains(CurrentHull) && Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2))) { if (Vector2.DistanceSquared(hull.WorldPosition, WorldPosition) < Math.Pow(maxDistance, 2)) @@ -5005,7 +5041,7 @@ namespace Barotrauma else { if (h.ConnectedGaps.Any(g => - g.Open > 0.9f && + (g.Open > 0.9f || g.ConnectedDoor is { HasWindow: true }) && Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2) && CanSeeTarget(g))) { @@ -5027,7 +5063,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"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 61f4df9c7..0752b6b3f 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++; @@ -1311,12 +1311,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 +1342,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 +1364,7 @@ namespace Barotrauma private int GetCurrentLevel(out int experienceRequired) { - int level = 1; + int level = 0; experienceRequired = 0; while (experienceRequired + ExperienceRequiredPerLevel(level) <= ExperiencePoints) { 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..6f5ac8417 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; } } @@ -1046,7 +1054,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 d7567b91c..3df905b80 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; } @@ -148,11 +152,6 @@ namespace Barotrauma return minVitality; } return vitality; - - } - private set - { - vitality = value; } } @@ -254,7 +253,7 @@ namespace Barotrauma public CharacterHealth(Character character) { this.Character = character; - Vitality = 100.0f; + vitality = 100.0f; DoesBleed = true; UseHealthWindow = false; @@ -271,7 +270,7 @@ namespace Barotrauma this.Character = character; InitIrremovableAfflictions(); - Vitality = UnmodifiedMaxVitality; + vitality = UnmodifiedMaxVitality; minVitality = character.IsHuman ? -100.0f : 0.0f; @@ -971,7 +970,7 @@ namespace Barotrauma public void CalculateVitality() { - Vitality = MaxVitality; + vitality = MaxVitality; IsParalyzed = false; if (Unkillable || Character.GodMode) { return; } @@ -984,7 +983,7 @@ namespace Barotrauma { vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth); } - Vitality -= vitalityDecrease; + vitality -= vitalityDecrease; affliction.CalculateDamagePerSecond(vitalityDecrease); if (affliction.Strength >= affliction.Prefab.MaxStrength && 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 0fedaedf3..289940df0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -50,6 +50,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable] public bool NeedsWater { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Note: non-humans with a human AI aren't fully supported. Enabling this on a non-human character may lead to issues.")] + public bool UseHumanAI { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Is this creature an artificial creature, like robot or machine that shouldn't be affected by afflictions that affect only organic creatures? Overrides DoesBleed."), Editable] public bool IsMachine { get; set; } @@ -556,7 +559,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; } } @@ -708,7 +712,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 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/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..8b8fe05cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Abilities conditionElement.GetAttributeStringArray("targettypes", conditionElement.GetAttributeStringArray("targettype", Array.Empty()))); - foreach (XElement subElement in conditionElement.Elements()) + foreach (ContentXElement subElement in conditionElement.Elements()) { if (subElement.NameAsIdentifier() == "conditional") { @@ -27,7 +27,8 @@ namespace Barotrauma.Abilities if (!targetTypes.Any() && !conditionals.Any()) { - 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); } } 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/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..96ed33dab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -12,7 +12,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); 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/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..56807217a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs @@ -14,23 +14,25 @@ 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); } } 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..c046863a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -28,7 +28,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); 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..cb33943db 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()) @@ -55,7 +55,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 +91,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 +109,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 +121,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 +139,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 +166,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 +203,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 +215,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 +249,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 +265,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/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..68d3b4184 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) { } @@ -37,6 +38,13 @@ namespace Barotrauma if (newHashSet.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newHashSet); } } 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() 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/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..2599ebd65 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() }; })); @@ -1133,7 +1135,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 +1161,7 @@ namespace Barotrauma } Submarine.MainSub.SetPosition(pos); } - else + else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase)) { if (Level.Loaded == null) { @@ -1172,13 +1174,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)); @@ -1959,7 +1977,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 +1988,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 +2007,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 +2028,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 +2044,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 +2082,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 +2092,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 +2113,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 +2128,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 +2415,13 @@ namespace Barotrauma #endif } - public static void LogError(string msg, Color? color = null) + public static void LogError(string msg, Color? color = null, ContentPackage contentPackage = null) { + if (contentPackage != null) + { + string colorStr = XMLExtensions.ToStringHex(Color.MediumPurple); + msg = $"‖color:{colorStr}‖[{contentPackage.Name}]‖color:end‖ {msg}"; + } color ??= Color.Red; NewMessage(msg, color.Value, isCommand: false, isError: true); } @@ -2515,7 +2538,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 +2555,13 @@ 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) { + if (contentPackage != null) + { + string color = XMLExtensions.ToStringHex(Color.MediumPurple); + error = $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {error}"; + } if (e != null) { error += " {" + e.Message + "}\n"; @@ -2547,7 +2575,7 @@ namespace Barotrauma error += "\n\nInner exception: " + innermost.Message + "\n"; if (innermost.StackTrace != null) { - error += innermost.StackTrace.CleanupStackTrace(); ; + error += innermost.StackTrace.CleanupStackTrace(); } } } @@ -2580,10 +2608,16 @@ namespace Barotrauma errorMsg); } - public static void AddWarning(string warning) + public static void AddWarning(string warning, ContentPackage contentPackage = null) { + warning = $"WARNING: {warning}"; + if (contentPackage != null) + { + string color = XMLExtensions.ToStringHex(Color.MediumPurple); + warning = $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {warning}"; + } System.Diagnostics.Debug.WriteLine(warning); - NewMessage($"WARNING: {warning}", Color.Yellow); + NewMessage(warning, Color.Yellow); } #if CLIENT 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..d59add1f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -14,12 +14,14 @@ namespace Barotrauma { 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) { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed."); + 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 IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag"; @@ -46,7 +48,8 @@ namespace Barotrauma } 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."); + 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 (target == null || Conditional == null) { 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/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..4585edbc4 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); @@ -102,7 +103,7 @@ namespace Barotrauma public EventAction(ScriptedEvent parentEvent, ContentXElement element) { - ParentEvent = parentEvent; + ParentEvent = parentEvent ?? throw new ArgumentNullException(nameof(parentEvent)); SerializableProperty.DeserializeProperties(this, element); } @@ -147,7 +148,8 @@ namespace Barotrauma } 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,7 +164,8 @@ 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; } } 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..82cbb6ee8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs @@ -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/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 3973643a5..d87c58be1 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) { @@ -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..cd044361b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs @@ -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,7 +69,8 @@ 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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index 64861f6e4..239312aff 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); } } 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 e608b7cde..7374e3466 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -36,7 +36,19 @@ namespace Barotrauma private bool isFinished = false; - private bool targetNotFound = false; + /// + /// If the action tags some entities directly (not trying to find targets on the fly), + /// we may be able to determine that targets can not be found even if we'd recheck + /// + private bool cantFindTargets = false; + + /// + /// If the TagAction adds a target predicate (a criteria that keeps finding targets on the fly), + /// we must keep checking if targets have been found to determine if the action can continue or not + /// + private bool mustRecheckTargets = false; + + private bool taggingDone = false; public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { @@ -198,6 +210,7 @@ namespace Barotrauma else { ParentEvent.AddTargetPredicate(tag, predicate); + mustRecheckTargets = true; } } @@ -205,7 +218,7 @@ namespace Barotrauma { if (entities.None()) { - targetNotFound = true; + cantFindTargets = true; return; } if (ChoosePercentage > 0.0f) @@ -230,7 +243,7 @@ namespace Barotrauma { if (entities.None()) { - targetNotFound = true; + cantFindTargets = true; return; } @@ -250,7 +263,7 @@ namespace Barotrauma { if (entities.None()) { - targetNotFound = true; + cantFindTargets = true; return; } ParentEvent.AddTarget(tag, entities.GetRandomUnsynced()); @@ -260,26 +273,30 @@ namespace Barotrauma public override void Update(float deltaTime) { - if (isFinished || targetNotFound) { return; } + if (isFinished || cantFindTargets) { return; } - string[] criteriaSplit = Criteria.Split(';'); - - targetNotFound = false; - foreach (string entry in criteriaSplit) + if (!taggingDone) { - string[] kvp = entry.Split(':'); - Identifier key = kvp[0].Trim().ToIdentifier(); - Identifier value = kvp.Length > 1 ? kvp[1].Trim().ToIdentifier() : Identifier.Empty; - if (Taggers.TryGetValue(key, out Action tagger)) + cantFindTargets = false; + string[] criteriaSplit = Criteria.Split(';'); + foreach (string entry in criteriaSplit) { - tagger(value); - } - else - { - string errorMessage = $"Error in TagAction (event \"{ParentEvent.Prefab.Identifier}\") - unrecognized target criteria \"{key}\"."; - DebugConsole.ThrowError(errorMessage); - GameAnalyticsManager.AddErrorEventOnce($"TagAction.Update:InvalidCriteria_{ParentEvent.Prefab.Identifier}_{key}", GameAnalyticsManager.ErrorSeverity.Error, errorMessage); + string[] kvp = entry.Split(':'); + Identifier key = kvp[0].Trim().ToIdentifier(); + Identifier value = kvp.Length > 1 ? kvp[1].Trim().ToIdentifier() : Identifier.Empty; + if (Taggers.TryGetValue(key, out Action tagger)) + { + tagger(value); + } + else + { + string errorMessage = $"Error in TagAction (event \"{ParentEvent.Prefab.Identifier}\") - unrecognized target criteria \"{key}\"."; + DebugConsole.ThrowError(errorMessage, + contentPackage: ParentEvent.Prefab?.ContentPackage); + GameAnalyticsManager.AddErrorEventOnce($"TagAction.Update:InvalidCriteria_{ParentEvent.Prefab.Identifier}_{key}", GameAnalyticsManager.ErrorSeverity.Error, errorMessage); + } } + taggingDone = true; } if (ContinueIfNoTargetsFound) @@ -288,7 +305,14 @@ namespace Barotrauma } else { - isFinished = !targetNotFound; + if (mustRecheckTargets) + { + isFinished = ParentEvent.GetTargets(Tag).Any(); + } + else + { + isFinished = !cantFindTargets; + } } } 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 index 190f7fdc0..e227f401d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs @@ -14,7 +14,8 @@ partial class TutorialHighlightAction : EventAction { if (GameMain.NetworkMember != null) { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(TutorialHighlightAction)} is not supported in multiplayer."); + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(TutorialHighlightAction)} is not supported in multiplayer.", + contentPackage: element.ContentPackage); } } 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/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index c7c89bd26..183ba0a2b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -518,7 +518,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); } } 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/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..fa1cad95d 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; } @@ -180,7 +181,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 +222,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 +261,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..717befcb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -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); @@ -547,7 +548,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 +558,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 +590,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 +606,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 +620,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; } 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..03f144e2c 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). diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 85c735b0d..7503af5c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -50,7 +50,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 +60,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 +71,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 +111,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 @@ -349,7 +353,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); } } 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..4f6455dba 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); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index fcf412f27..0e5bfb3f0 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."); @@ -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) @@ -1353,6 +1369,7 @@ namespace Barotrauma if (item.HiddenInGame) { continue; } if (!connectedSubs.Contains(item.Submarine)) { continue; } if (item.Prefab.DontTransferBetweenSubs) { continue; } + if (AnyParentInventoryDisableTransfer(item)) { continue; } var rootOwner = item.GetRootInventoryOwner(); if (rootOwner is Character) { continue; } if (rootOwner is Item ownerItem && (ownerItem.NonInteractable || item.NonPlayerTeamInteractable || ownerItem.HiddenInGame)) { continue; } @@ -1362,6 +1379,15 @@ namespace Barotrauma if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; } itemsToTransfer.Add((item, item.Container)); item.Submarine = null; + + static bool AnyParentInventoryDisableTransfer(Item item) + { + if (item.ParentInventory?.Owner is not Item parentOwner) { return false; } + return HasProblematicComponent(parentOwner) || AnyParentInventoryDisableTransfer(parentOwner); + + static bool HasProblematicComponent(Item it) + => it.Components.Any(static c => c.DontTransferInventoryBetweenSubs); + } } foreach (var (item, container) in itemsToTransfer) { @@ -1369,8 +1395,15 @@ namespace Barotrauma { // Drop the item if it's not inside another item set to be transferred. item.Drop(null, createNetworkEvent: false, setTransform: false); + //dropping items sets the sub, set it back to null + item.Submarine = null; + foreach (var itemContainer in item.GetComponents()) + { + itemContainer.Inventory.FindAllItems((_) => true, recursive: true).ForEach(it => it.Submarine = null); + } } } + System.Diagnostics.Debug.Assert(itemsToTransfer.None(it => it.item.Submarine != null), "Item that was set to be transferred was not removed from the sub!"); currentSub.Info.NoItems = true; } // Serialize the current sub @@ -1408,6 +1441,7 @@ namespace Barotrauma { newContainer = newSub.FindContainerFor(item, onlyPrimary: true, checkTransferConditions: true, allowConnectedSubs: true); } + string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})"; if (item.Container == null && (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null, createNetworkEvent: false))) { var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); @@ -1416,13 +1450,16 @@ namespace Barotrauma Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); } - else if (cargoContainer.Item.Submarine is Submarine containerSub) + else { - // Use the item's sub in case the sub consists of multiple linked subs. - item.Submarine = containerSub; + if (cargoContainer.Item.Submarine is Submarine containerSub) + { + // Use the item's sub in case the sub consists of multiple linked subs. + item.Submarine = containerSub; + } + newContainerName = cargoContainer.Item.Prefab.Identifier.ToString(); } } - string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})"; string msg = "Item transfer log error."; if (oldContainer != null) { 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/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..1a3e665aa 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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 41ff58290..9f80f04ad 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; } @@ -381,6 +383,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.Y <= 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/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..a33359230 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); } } } 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 c73ea2f41..4e1862e59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -71,6 +71,18 @@ namespace Barotrauma.Items.Components protected const float CorrectionDelay = 1.0f; protected CoroutineHandle delayedCorrectionCoroutine; + /// + /// If enabled, the contents of the item are not transferred when the player transfers items between subs. + /// Use this if this component uses item containers in a way where removing the item from the container via external means would cause problems. + /// + 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 { @@ -285,7 +297,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; @@ -298,7 +311,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); @@ -310,7 +324,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 { @@ -325,7 +340,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) { @@ -360,7 +376,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; } @@ -426,7 +443,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); } } @@ -968,7 +986,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; } @@ -977,7 +996,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; } @@ -990,14 +1010,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; @@ -1010,7 +1032,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..015807276 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -284,7 +284,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(); @@ -321,7 +322,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 +351,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; 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/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index ff6719a73..0bc72b64c 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); @@ -416,7 +422,7 @@ namespace Barotrauma.Items.Components if (requiredItem.UseCondition && suitableIngredient.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) { suitableIngredient.Condition -= suitableIngredient.Prefab.Health * requiredItem.MinCondition; - continue; + break; } if (suitableIngredient.OwnInventory != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index 2e432496a..4c7cb459e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -82,6 +82,9 @@ namespace Barotrauma.Items.Components private List? lightComponents; + // We don't want the seeds to be transferred to a new submarine as seeds are not supposed to leave the container after they have been planted. + public override bool DontTransferInventoryBetweenSubs => true; + public Planter(Item item, ContentXElement element) : base(item, element) { canBePicked = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index f023ff870..c0d788133 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)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 2af67598e..6e5363695 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; } 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/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 f02fbebae..d2675e51b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -26,19 +26,40 @@ namespace Barotrauma.Items.Components public override bool IsActive => true; + // 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) { if (input.Name != connectionName) { continue; } - return Option.Some(input); } foreach (CircuitBoxOutputConnection output in Outputs) { if (output.Name != connectionName) { continue; } + return Option.Some(output); + } + return Option.None; + } + + public Option FindInputOutputConnection(Connection connection) + { + foreach (CircuitBoxInputConnection input in Inputs) + { + if (input.Connection != connection) { continue; } + return Option.Some(input); + } + + foreach (CircuitBoxOutputConnection output in Outputs) + { + if (output.Connection != connection) { continue; } return Option.Some(output); } @@ -338,7 +359,7 @@ namespace Barotrauma.Items.Components return; } - SpawnItem(this, prefab, WireContainer, wire => + SpawnItem(prefab, user: null, container: WireContainer, onSpawned: wire => { AddWireDirect(wireId, prefab, Option.Some(wire), one, two); onItemSpawned(wire); @@ -359,7 +380,7 @@ namespace Barotrauma.Items.Components private void AddWireDirect(ushort id, ItemPrefab prefab, Option backingItem, CircuitBoxConnection one, CircuitBoxConnection two) => Wires.Add(new CircuitBoxWire(this, id, backingItem, one, two, prefab)); - private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Action onItemSpawned) + private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Character? user, Action? onItemSpawned) { if (id is ICircuitBoxIdentifiable.NullComponentID) { @@ -373,12 +394,12 @@ namespace Barotrauma.Items.Components return false; } - SpawnItem(this, prefab, ComponentContainer, spawnedItem => + SpawnItem(prefab, user, ComponentContainer, spawnedItem => { Components.Add(new CircuitBoxComponent(id, spawnedItem, pos, this, usedResource)); - onItemSpawned(spawnedItem); + onItemSpawned?.Invoke(spawnedItem); + OnViewUpdateProjSpecific(); }); - OnViewUpdateProjSpecific(); return true; } @@ -646,7 +667,7 @@ namespace Barotrauma.Items.Components return Option.None; } - public static void SpawnItem(CircuitBox circuitBox, ItemPrefab prefab, ItemContainer? container, Action onSpawned) + public static void SpawnItem(ItemPrefab prefab, Character? user, ItemContainer? container, Action onSpawned) { if (container is null) { @@ -655,13 +676,27 @@ namespace Barotrauma.Items.Components if (IsInGame()) { - Entity.Spawner?.AddItemToSpawnQueue(prefab, container.Inventory, onSpawned: onSpawned); + Entity.Spawner?.AddItemToSpawnQueue(prefab, container.Inventory, onSpawned: it => + { + AssignWifiComponentTeam(it, user); + onSpawned(it); + }); return; } Item forceSpawnedItem = new Item(prefab, Vector2.Zero, null); container.Inventory.TryPutItem(forceSpawnedItem, null); onSpawned(forceSpawnedItem); + AssignWifiComponentTeam(forceSpawnedItem, user); + + static void AssignWifiComponentTeam(Item item, Character? user) + { + if (user == null) { return; } + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = user.TeamID; + } + } } public static void RemoveItem(Item item) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 85e995e5f..d6af54c78 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}"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index c94cc73e0..41b58165d 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index af251d6ce..4c4c9c0a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -98,11 +98,11 @@ namespace Barotrauma } /// Defaults to if null - public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null, float? condition = null) + public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null, float? condition = null, bool ignoreItemsInSlot = false) { if (itemPrefab == null) { return 0; } maxStackSize ??= itemPrefab.GetMaxStackSize(inventory); - if (items.Count > 0) + if (items.Count > 0 && !ignoreItemsInSlot) { if (condition.HasValue) { @@ -517,10 +517,10 @@ namespace Barotrauma return count; } - public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition) + public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition, bool ignoreItemsInSlot = false) { if (i < 0 || i >= slots.Length) { return 0; } - return slots[i].HowManyCanBePut(itemPrefab, condition: condition); + return slots[i].HowManyCanBePut(itemPrefab, condition: condition, ignoreItemsInSlot: ignoreItemsInSlot); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 81d0cf2f0..edb9917cc 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; @@ -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; @@ -862,7 +876,25 @@ namespace Barotrauma { get { - return ownInventory?.AllItems ?? Enumerable.Empty(); + if (OwnInventories.Length < 2) + { + if (OwnInventory == null) { yield break; } + + foreach (var item in OwnInventory.AllItems) + { + yield return item; + } + } + else + { + foreach (var inventory in OwnInventories) + { + foreach (var item in inventory.AllItems) + { + yield return item; + } + } + } } } @@ -871,6 +903,8 @@ namespace Barotrauma get { return ownInventory; } } + public readonly ImmutableArray OwnInventories = ImmutableArray.Empty; + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Enable if you want to display the item HUD side by side with another item's HUD, when linked together. " + "Disclaimer: It's possible or even likely that the views block each other, if they were not designed to be viewed together!")] @@ -1057,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 { @@ -1192,6 +1227,8 @@ namespace Barotrauma ownInventory = itemContainer.Inventory; } + OwnInventories = GetComponents().Select(ic => ic.Inventory).ToImmutableArray(); + qualityComponent = GetComponent(); IsLadder = GetComponent() != null; @@ -1210,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(); @@ -1654,6 +1692,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; @@ -1916,7 +1960,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(); } @@ -2533,8 +2577,7 @@ namespace Barotrauma foreach (Connection c in connectionPanel.Connections) { if (connectionFilter != null && !connectionFilter.Invoke(c)) { continue; } - var recipients = c.Recipients; - foreach (Connection recipient in recipients) + foreach (Connection recipient in c.Recipients) { var component = recipient.Item.GetComponent(); if (component != null && !connectedComponents.Contains(component)) @@ -2587,9 +2630,20 @@ namespace Barotrauma private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays, bool allowTraversingBackwards = true) where T : ItemComponent { alreadySearched.Add(c); - - var recipients = c.Recipients; - foreach (Connection recipient in recipients) + static IEnumerable GetRecipients(Connection c) + { + foreach (Connection recipient in c.Recipients) + { + yield return recipient; + } + //check circuit box inputs/outputs this connection is connected to + foreach (var circuitBoxConnection in c.CircuitBoxConnections) + { + yield return circuitBoxConnection.Connection; + } + } + + foreach (Connection recipient in GetRecipients(c)) { if (alreadySearched.Contains(recipient)) { continue; } var component = recipient.Item.GetComponent(); @@ -2598,23 +2652,53 @@ namespace Barotrauma connectedComponents.Add(component); } - //connected to a wifi component -> see which other wifi components it can communicate with - var wifiComponent = recipient.Item.GetComponent(); - if (wifiComponent != null && wifiComponent.CanTransmit()) + var circuitBox = recipient.Item.GetComponent(); + if (circuitBox != null) { - foreach (var wifiReceiver in wifiComponent.GetTransmittersInRange()) + //if this is a circuit box, check what the connection is connected to inside the box + var potentialCbConnection = circuitBox.FindInputOutputConnection(recipient); + if (potentialCbConnection.TryUnwrap(out var cbConnection)) { - var receiverConnections = wifiReceiver.Item.Connections; - if (receiverConnections == null) { continue; } - foreach (Connection wifiOutput in receiverConnections) + if (cbConnection is CircuitBoxInputConnection inputConnection) { - if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; } - GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); + foreach (var connectedTo in inputConnection.ExternallyConnectedTo) + { + if (alreadySearched.Contains(connectedTo.Connection)) { continue; } + CheckRecipient(connectedTo.Connection); + } + } + else + { + foreach (var connectedFrom in cbConnection.ExternallyConnectedFrom) + { + if (alreadySearched.Contains(connectedFrom.Connection) || !allowTraversingBackwards) { continue; } + CheckRecipient(connectedFrom.Connection); + } } } } + CheckRecipient(recipient); - recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); + void CheckRecipient(Connection recipient) + { + //connected to a wifi component -> see which other wifi components it can communicate with + var wifiComponent = recipient.Item.GetComponent(); + if (wifiComponent != null && wifiComponent.CanTransmit()) + { + foreach (var wifiReceiver in wifiComponent.GetTransmittersInRange()) + { + var receiverConnections = wifiReceiver.Item.Connections; + if (receiverConnections == null) { continue; } + foreach (Connection wifiOutput in receiverConnections) + { + if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; } + GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); + } + } + } + + recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); + } } if (ignoreInactiveRelays) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index 0b8aea197..bc8f2f41f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -58,12 +58,12 @@ namespace Barotrauma return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition, quality) && slots[i].Items.Count < container.GetMaxStackSize(i); } - public override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition) + public override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition, bool ignoreItemsInSlot = false) { if (itemPrefab == null) { return 0; } if (i < 0 || i >= slots.Length) { return 0; } if (!container.CanBeContained(itemPrefab, i)) { return 0; } - return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.GetMaxStackSize(this), container.GetMaxStackSize(i)), condition); + return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.GetMaxStackSize(this), container.GetMaxStackSize(i)), condition, ignoreItemsInSlot); } public override bool IsFull(bool takeStacksIntoAccount = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index dd3abc0ad..ddedb9091 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; } @@ -843,7 +852,7 @@ namespace Barotrauma } private int maxStackSizeCharacterInventory; - [Serialize(-1, IsPropertySaveable.No)] + [Serialize(-1, IsPropertySaveable.No, description: "Maximum stack size when the item is in a character inventory.")] public int MaxStackSizeCharacterInventory { get { return maxStackSizeCharacterInventory; } @@ -851,7 +860,9 @@ namespace Barotrauma } private int maxStackSizeHoldableOrWearableInventory; - [Serialize(-1, IsPropertySaveable.No)] + [Serialize(-1, IsPropertySaveable.No, description: + "Maximum stack size when the item is inside a holdable or wearable item. "+ + "If not set, defaults to MaxStackSizeCharacterInventory.")] public int MaxStackSizeHoldableOrWearableInventory { get { return maxStackSizeHoldableOrWearableInventory; } @@ -864,15 +875,20 @@ namespace Barotrauma { return maxStackSizeCharacterInventory; } - else if (maxStackSizeHoldableOrWearableInventory > 0 && - inventory?.Owner is Item item && (item.GetComponent() != null || item.GetComponent() != null)) + else if (inventory?.Owner is Item item && + (item.GetComponent() is { Attachable: false } || item.GetComponent() != null)) { - return maxStackSizeHoldableOrWearableInventory; - } - else - { - return maxStackSize; + if (maxStackSizeHoldableOrWearableInventory > 0) + { + return maxStackSizeHoldableOrWearableInventory; + } + else if (maxStackSizeCharacterInventory > 0) + { + //if maxStackSizeHoldableOrWearableInventory is not set, it defaults to maxStackSizeCharacterInventory + return maxStackSizeCharacterInventory; + } } + return maxStackSize; } [Serialize(false, IsPropertySaveable.No)] @@ -880,7 +896,7 @@ namespace Barotrauma public ImmutableHashSet AllowDroppingOnSwapWith { get; private set; } - [Serialize(false, IsPropertySaveable.No)] + [Serialize(false, IsPropertySaveable.No, "If enabled, the item is not transferred when the player transfers items between subs.")] public bool DontTransferBetweenSubs { get; private set; } [Serialize(true, IsPropertySaveable.No)] @@ -1009,7 +1025,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); @@ -1032,6 +1049,7 @@ namespace Barotrauma var levelCommonness = new Dictionary(); var levelQuantity = new Dictionary(); + List loadedRecipes = new List(); foreach (ContentXElement subElement in ConfigElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -1046,7 +1064,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; @@ -1064,7 +1083,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 @@ -1077,7 +1097,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 @@ -1095,13 +1116,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); @@ -1114,16 +1137,21 @@ 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 = GetParentModPackageOrThisPackage(); + + int prevRecipeIndex = loadedRecipes.IndexOf(prevRecipe); DebugConsole.ThrowError( $"Error in item prefab \"{ToString()}\": " + - $"{prevRecipe.TargetItemPrefabIdentifier} has the same hash as {newRecipe.TargetItemPrefabIdentifier}. " + - $"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 { fabricationRecipes.Add(newRecipe.RecipeHash, newRecipe); } + loadedRecipes.Add(newRecipe); break; case "preferredcontainer": var preferredContainer = new PreferredContainer(subElement); @@ -1132,7 +1160,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 @@ -1182,7 +1211,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); @@ -1191,6 +1221,8 @@ namespace Barotrauma } } + Size = ConfigElement.GetAttributeVector2(nameof(Size), Size); + #if CLIENT ParseSubElementsClient(ConfigElement, variantOf); #endif @@ -1221,7 +1253,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); @@ -1238,7 +1270,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 @@ -1246,7 +1279,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 @@ -1306,9 +1340,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; @@ -1515,6 +1549,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") @@ -1528,7 +1565,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; } @@ -1539,7 +1577,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" && @@ -1549,18 +1588,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/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/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index db32a7ec5..3fab10249 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -130,10 +130,17 @@ namespace Barotrauma /// /// When set to true, the explosion don't deal less damage when the target is behind a solid object. /// - public bool IgnoreCover - { - get; set; - } + public bool IgnoreCover { get; set; } + + /// + /// Does the damage from the explosion decrease with distance from the origin of the explosion? + /// + public bool DistanceFalloff { get; set; } = true; + + /// + /// Structures that don't count as "cover" that reduces damage from the explosion. Only relevant if IgnoreCover is set to false. + /// + public IEnumerable IgnoredCover; /// /// How long the light source created by the explosion lasts. @@ -311,12 +318,15 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(Attack.GetStructureDamage(1.0f), 0.0f) || !MathUtils.NearlyEqual(Attack.GetLevelWallDamage(1.0f), 0.0f)) { - RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, IgnoredSubmarines, Attack.EmitStructureDamageParticles); + RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, + IgnoredSubmarines, + Attack.EmitStructureDamageParticles, + DistanceFalloff); } if (BallastFloraDamage > 0.0f) { - RangedBallastFloraDamage(worldPosition, displayRange, BallastFloraDamage, attacker); + RangedBallastFloraDamage(worldPosition, displayRange, BallastFloraDamage, attacker, DistanceFalloff); } if (EmpStrength > 0.0f) @@ -326,7 +336,7 @@ namespace Barotrauma { float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); if (distSqr > displayRangeSqr) { continue; } - float distFactor = CalculateDistanceFactor(distSqr, displayRange); + float distFactor = DistanceFalloff ? CalculateDistanceFactor(distSqr, displayRange) : 1.0f; //damage repairable power-consuming items var powered = item.GetComponent(); @@ -362,7 +372,10 @@ namespace Barotrauma float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); if (distSqr > displayRangeSqr) { continue; } - float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange; + float distFactor = + DistanceFalloff ? + 1.0f - (float)Math.Sqrt(distSqr) / displayRange : + 1.0f; //repair repairable items if (item.Repairables.Any()) { @@ -415,13 +428,16 @@ namespace Barotrauma if (item.Prefab.DamagedByExplosions && !item.Indestructible) { - float distFactor = 1.0f - dist / displayRange; + float distFactor = + DistanceFalloff ? + 1.0f - dist / displayRange : + 1.0f; float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier); Vector2 explosionPos = worldPosition; if (item.Submarine != null) { explosionPos -= item.Submarine.Position; } - damageAmount *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, item.SimPosition); + damageAmount *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, item.SimPosition, IgnoredCover); item.Condition -= damageAmount * distFactor; } } @@ -482,12 +498,15 @@ namespace Barotrauma if (dist > attack.Range) { continue; } - float distFactor = 1.0f - dist / attack.Range; + float distFactor = + DistanceFalloff ? + 1.0f - dist / attack.Range : + 1.0f; //solid obstacles between the explosion and the limb reduce the effect of the explosion if (!IgnoreCover) { - distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition); + distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition, IgnoredCover); } if (distFactor > 0) { @@ -602,7 +621,8 @@ namespace Barotrauma /// /// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken /// - public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null, bool emitWallDamageParticles = true) + public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null, + bool emitWallDamageParticles = true, bool distanceFalloff = true) { float dist = 600.0f; damagedStructures.Clear(); @@ -616,7 +636,10 @@ namespace Barotrauma { for (int i = 0; i < structure.SectionCount; i++) { - float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange); + float distFactor = + distanceFalloff ? + 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange) : + 1.0f; if (distFactor <= 0.0f) { continue; } structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles); @@ -680,7 +703,7 @@ namespace Barotrauma return damagedStructures; } - public static void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null) + public static void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null, bool distanceFalloff = true) { List ballastFlorae = new List(); @@ -698,7 +721,10 @@ namespace Barotrauma float branchDist = Vector2.Distance(branchWorldPos, worldPosition); if (branchDist < worldRange) { - float distFactor = 1.0f - (branchDist / worldRange); + float distFactor = + distanceFalloff ? + 1.0f - (branchDist / worldRange) : + 1.0f; if (distFactor <= 0.0f) { return; } Vector2 explosionPos = worldPosition; @@ -715,7 +741,7 @@ namespace Barotrauma } } - private static float GetObstacleDamageMultiplier(Vector2 explosionSimPos, Vector2 explosionWorldPos, Vector2 targetSimPos) + private static float GetObstacleDamageMultiplier(Vector2 explosionSimPos, Vector2 explosionWorldPos, Vector2 targetSimPos, IEnumerable ignoredCover = null) { float damageMultiplier = 1.0f; var obstacles = Submarine.PickBodies(targetSimPos, explosionSimPos, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall); @@ -728,6 +754,10 @@ namespace Barotrauma } else if (body.UserData is Structure structure) { + if (ignoredCover != null) + { + if (ignoredCover.Contains(structure)) { continue; } + } int sectionIndex = structure.FindSectionIndex(explosionWorldPos, world: true, clamp: true); if (structure.SectionBodyDisabled(sectionIndex)) { 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/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 4c93da0cc..40e897dea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -109,6 +109,8 @@ namespace Barotrauma public float Size => IsHorizontal ? Rect.Height : Rect.Width; + public float PressureDistributionSpeed => Size / 100.0f * open; + private Door connectedDoor; public Door ConnectedDoor { @@ -427,11 +429,9 @@ namespace Barotrauma if (hull1.WaterVolume <= 0.0 && hull2.WaterVolume <= 0.0) { return; } - float size = IsHorizontal ? rect.Height : rect.Width; - //a variable affecting the water flow through the gap //the larger the gap is, the faster the water flows - float sizeModifier = size / 100.0f * open; + float sizeModifier = Size / 100.0f * open; //horizontal gap (such as a regular door) if (IsHorizontal) @@ -440,7 +440,7 @@ namespace Barotrauma float delta = 0.0f; //water level is above the lower boundary of the gap - if (Math.Max(hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.Surface + subOffset.Y + hull2.WaveY[0]) > rect.Y - size) + if (Math.Max(hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.Surface + subOffset.Y + hull2.WaveY[0]) > rect.Y - Size) { int dir = (hull1.Pressure > hull2.Pressure + subOffset.Y) ? 1 : -1; @@ -569,27 +569,35 @@ namespace Barotrauma if (open > 0.0f) { - if (hull1.WaterVolume > hull1.Volume / Hull.MaxCompress && hull2.WaterVolume > hull2.Volume / Hull.MaxCompress) + if (hull1.WaterVolume > hull1.Volume / Hull.MaxCompress && + hull2.WaterVolume > hull2.Volume / Hull.MaxCompress) { + //both hulls full -> distribute pressure float avgLethality = (hull1.LethalPressure + hull2.LethalPressure) / 2.0f; - hull1.LethalPressure = avgLethality; - hull2.LethalPressure = avgLethality; + changePressure(hull1, avgLethality, PressureDistributionSpeed, deltaTime); + changePressure(hull2, avgLethality, PressureDistributionSpeed, deltaTime); + + static void changePressure(Hull hull, float target, float speed, float deltaTime) + { + float diff = target - hull.LethalPressure; + float maxChange = Hull.PressureBuildUpSpeed * speed * deltaTime; + hull.LethalPressure += MathHelper.Clamp(diff, -maxChange, maxChange); + } } else { - hull1.LethalPressure -= Hull.PressureDropSpeed * deltaTime; - hull2.LethalPressure -= Hull.PressureDropSpeed * deltaTime; + //either hull not full -> pressure drops + hull1.LethalPressure -= Hull.PressureDropSpeed * PressureDistributionSpeed * deltaTime; + hull2.LethalPressure -= Hull.PressureDropSpeed * PressureDistributionSpeed * deltaTime; } } } void UpdateRoomToOut(float deltaTime, Hull hull1) { - float size = IsHorizontal ? rect.Height : rect.Width; - //a variable affecting the water flow through the gap //the larger the gap is, the faster the water flows - float sizeModifier = size * open * open; + float sizeModifier = Size * open * open; float delta = 500.0f * sizeModifier * deltaTime; @@ -642,7 +650,7 @@ namespace Barotrauma } else { - hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * PressureDistributionSpeed * deltaTime; } } else @@ -657,7 +665,7 @@ namespace Barotrauma } if (hull1.WaterVolume >= hull1.Volume / Hull.MaxCompress) { - hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * deltaTime; + hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * PressureDistributionSpeed * deltaTime; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 7de5bf629..7f899899a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1016,7 +1016,11 @@ namespace Barotrauma if (waterVolume < Volume) { - LethalPressure -= PressureDropSpeed * deltaTime; + //pressure drop speed is inversely proportionate to water percentage + //= pressure drops very fast if the hull is nowhere near full + float waterVolumeFactor = Math.Max((100.0f - WaterPercentage) / 10.0f, 1.0f); + LethalPressure -= + PressureDropSpeed * waterVolumeFactor * deltaTime; if (WaterVolume <= 0.0f) { #if CLIENT 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..106c1354b 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 @@ -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; } @@ -4235,7 +4285,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/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..29ca85cf5 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 03ebcb980..cb8c2faf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -639,7 +639,7 @@ 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); @@ -659,15 +659,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; } @@ -776,11 +773,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 +795,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 +803,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 +821,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 +839,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 +861,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 +996,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; } 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/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 a2abd562e..10e72f4ca 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,17 +733,6 @@ 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; - } - /// /// Checks if there's a structure items can be attached to at the given position and returns it. /// @@ -727,8 +753,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 +769,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; } } } @@ -934,11 +963,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 +997,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 +1033,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 +1051,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 +1074,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 +1087,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 +1204,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 +1215,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 +1232,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) { @@ -1245,7 +1319,7 @@ namespace Barotrauma private static void CreateWallDamageExplosion(Gap gap, Character attacker) { - const float explosionRange = 750.0f; + const float explosionRange = 500.0f; float explosionStrength = gap.Open; var linkedHull = gap.linkedTo.FirstOrDefault() as Hull; @@ -1264,20 +1338,22 @@ namespace Barotrauma if (explosionOnBroken == null) { - explosionOnBroken = new Explosion(explosionRange, force: 10.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f); + explosionOnBroken = new Explosion(explosionRange, force: 5.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f); if (AfflictionPrefab.Prefabs.TryGet("lacerations".ToIdentifier(), out AfflictionPrefab lacerations)) { - explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(3.0f), null); + explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(5.0f), null); } else { - explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(3.0f), null); + explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null); } - explosionOnBroken.IgnoreCover = true; + explosionOnBroken.IgnoreCover = false; explosionOnBroken.OnlyInside = true; + explosionOnBroken.DistanceFalloff = false; explosionOnBroken.DisableParticles(); } + explosionOnBroken.IgnoredCover = gap.ConnectedWall?.ToEnumerable(); explosionOnBroken.Attack.Range = explosionRange * gap.Open; explosionOnBroken.Attack.DamageMultiplier = explosionStrength; explosionOnBroken.Attack.Stun = MathHelper.Clamp(explosionStrength, 0.5f, 1.0f); @@ -1335,7 +1411,7 @@ namespace Barotrauma { hasHoles = true; - if (!mergedSections.Any()) continue; + if (!mergedSections.Any()) { continue; } var mergedRect = GenerateMergedRect(mergedSections); mergedSections.Clear(); CreateRectBody(mergedRect, createConvexHull: true); @@ -1371,18 +1447,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), @@ -1396,7 +1471,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..459307b73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -624,7 +624,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 +636,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; } @@ -1840,6 +1846,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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index e0d060c04..9976f1f51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -800,7 +800,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/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/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index b6dfb0840..2a782728c 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 { 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..7d7012dcd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -548,6 +548,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 +588,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 0f9370041..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); } @@ -1606,28 +1610,7 @@ namespace Barotrauma } } } - if (removeItem) - { - for (int i = 0; i < targets.Count; i++) - { - if (targets[i] is Item item) { Entity.Spawner?.AddItemToRemoveQueue(item); } - } - } - if (removeCharacter) - { - for (int i = 0; i < targets.Count; i++) - { - var target = targets[i]; - if (target is Character character) - { - Entity.Spawner?.AddEntityToRemoveQueue(character); - } - else if (target is Limb limb) - { - Entity.Spawner?.AddEntityToRemoveQueue(limb.character); - } - } - } + if (breakLimb || hideLimb) { for (int i = 0; i < targets.Count; i++) @@ -1718,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 @@ -1731,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); } @@ -1899,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); } @@ -2266,8 +2249,8 @@ namespace Barotrauma { OnItemSpawned(newItem, chosenItemSpawnInfo); }); + break; } - break; } } } @@ -2292,6 +2275,30 @@ namespace Barotrauma ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); + //do this last - the entities spawned by the effect might need the entity for something, so better to remove it last + if (removeItem) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item item) { Entity.Spawner?.AddItemToRemoveQueue(item); } + } + } + if (removeCharacter) + { + for (int i = 0; i < targets.Count; i++) + { + var target = targets[i]; + if (target is Character character) + { + Entity.Spawner?.AddEntityToRemoveQueue(character); + } + else if (target is Limb limb) + { + Entity.Spawner?.AddEntityToRemoveQueue(limb.character); + } + } + } + if (oneShot) { Disabled = true; @@ -2406,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/Tags.cs b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs index 282c7d503..53282a021 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(); 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..8deb9ec70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -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 diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index ab028f246..2adfd257b 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,83 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.1.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Changes: +- Structures can now be rotated in the sub editor! +- 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. +- 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. + +Fixes: +- Fixed dedicated servers with lots of mods enabled not showing up in the server browser. +- Fixed ability to sell components from inside circuit boxes in outposts. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.0.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Changes: +- 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. +- 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. +- 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. +- 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. +- Optimized labels (large labels with a scaled down texture in particular were unnecessarily heavy). +- Optimized Watcher's status effects. +- 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. + +Multiplayer: +- 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. + +Fixes: +- 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 a couple of inaccurate talent and mission descriptions in Russian. +- 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: +- 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 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed shrapnel from flak cannon ammo always launching upwards. +- Fixed wall damage shrapnel going through walls and being a little excessive overall. +- Fixes to pressure distribution logic: fixes pressure sometimes being lethal in breached rooms that weren't full of water. +- Fixed clients not regaining control of their braindead character after rejoining if the character's taken any amount of damage while braindead. +- Fixes to inconsistent stack sizes (e.g. certain ammo types stacking up to 12 in character inventories, but only 8 holdable/wearable items like backpacks and toolbelts. +- Fixed console errors when switching subs with circuit boxes on board. +- Fixed items that are inside a container that doesn't get transferred getting duplicated during item transfer when switching subs. +- Fixed bots not cleaning up circuit boxes from the floor. +- Fixed stack size being displayed incorrectly in the tooltip when dragging a stack of items to an already-occupied slot. +- Fixed nav terminal's docking button not working if the signal is routed from the terminal to the docking port through a circuit box. +- Fixed double-clicking a component while a circuit box is equipped making the component vanish inside the circuit box. +- Fixed chat messages sent when accusing someone as a traitor not triggering the spam filter. +- Fixed connection names not being translated in circuit boxes (always showed up in English). +- Fixed right-side crew panel overlapping with the mission panel in the PvP mode round summary. +- Fixed depleted and fulgurium fuel rods stacking up to 32 (should be 8 like all other fuel rods). + +Modding: +- Added "UseHumanAI" property to characters (can be used to enable the human AI on characters other than humans). While using the human AI on non-humans isn't a fully supported or tested feature, it was previously possible to do that by creating a human prefab using a different species than human, but that no longer worked as of the Treacherous Tides update. +- Attachable holdable items don't count as "HoldableOrWearableInventories". Fixes e.g. tanks only stacking up to 1 in things like movable cabinets and shelves. Does not affect any vanilla content. +- Fixed fabricator reducing the condition of all the available ingredients if the crafting recipe reduces condition instead of consuming the whole item. +- Fixed TagAction continuing in some situations when it can't find targets, even if it's been configured not to with the "ContinueIfNoTargetsFound" attribute. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.1.18.1 ------------------------------------------------------------------------------------------------------------------------------------------------- From fb5ea537bf906adf956ea96b76d01d6b8bbedd75 Mon Sep 17 00:00:00 2001 From: Markus Isberg Date: Thu, 30 Nov 2023 13:53:00 +0200 Subject: [PATCH 2/3] Unstable 1.2.4.0 --- .../BarotraumaClient/ClientSource/Camera.cs | 19 +- .../ClientSource/Characters/Character.cs | 12 +- .../ClientSource/Characters/CharacterHUD.cs | 2 +- .../ClientSource/Characters/CharacterInfo.cs | 14 +- .../Characters/Health/CharacterHealth.cs | 2 + .../ClientSource/Characters/Limb.cs | 2 +- .../ClientSource/DebugConsole.cs | 27 +- .../EventActions/EventObjectiveAction.cs | 10 +- ...lHighlightAction.cs => HighlightAction.cs} | 17 +- .../ClientSource/Events/EventManager.cs | 9 +- .../ClientSource/Events/Missions/Mission.cs | 8 +- .../ClientSource/GUI/CrewManagement.cs | 56 +-- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 18 +- .../ClientSource/GUI/GUIComponent.cs | 21 +- .../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 | 3 +- .../ClientSource/GUI/SubmarineSelection.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 9 +- .../BarotraumaClient/ClientSource/GameMain.cs | 6 +- .../GameSession/GameModes/CampaignMode.cs | 8 +- .../GameModes/MultiPlayerCampaign.cs | 17 +- .../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/ItemComponent.cs | 19 +- .../Items/Components/ItemContainer.cs | 13 +- .../Items/Components/ItemLabel.cs | 2 +- .../Items/Components/LightComponent.cs | 2 +- .../Items/Components/Machines/Engine.cs | 6 +- .../Items/Components/Machines/Fabricator.cs | 88 ++++- .../Items/Components/Machines/Sonar.cs | 4 +- .../Items/Components/Machines/Steering.cs | 10 +- .../ClientSource/Items/Components/Turret.cs | 16 + .../ClientSource/Items/Inventory.cs | 65 +++- .../ClientSource/Items/Item.cs | 156 ++++++--- .../ClientSource/Items/ItemPrefab.cs | 18 +- .../ClientSource/Map/ItemAssemblyPrefab.cs | 2 +- .../ClientSource/Map/Levels/Level.cs | 3 +- .../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 | 128 +++++-- .../ClientSource/Map/Map/Map.cs | 10 +- .../ClientSource/Map/MapEntity.cs | 23 +- .../ClientSource/Map/Structure.cs | 16 +- .../ClientSource/Map/StructurePrefab.cs | 8 +- .../ClientSource/Map/SubmarinePreview.cs | 28 +- .../ClientSource/Networking/GameClient.cs | 2 +- .../Networking/ServerList/ServerInfo.cs | 80 +++-- .../ClientSource/Screens/CampaignUI.cs | 5 +- .../ClientSource/Screens/GameScreen.cs | 20 +- .../ClientSource/Screens/LevelEditorScreen.cs | 12 +- .../ClientSource/Screens/MainMenuScreen.cs | 1 + .../ClientSource/Screens/NetLobbyScreen.cs | 1 + .../ServerListScreen/ServerListScreen.cs | 186 +++++++++- .../ClientSource/Screens/SubEditorScreen.cs | 55 ++- .../ClientSource/Settings/SettingsMenu.cs | 16 +- .../ClientSource/Sounds/OggSound.cs | 11 +- .../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 | 19 +- .../StatusEffects/StatusEffect.cs | 2 +- .../ClientSource/Steam/BulkDownloader.cs | 36 +- .../ClientSource/Steam/Workshop.cs | 23 +- .../ClientSource/SubEditorCommands.cs | 83 +++-- .../Utils/{Quad.cs => GraphicsQuad.cs} | 2 +- .../ClientSource/Utils/SpriteRecorder.cs | 11 +- .../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 | 15 +- .../Events/EventActions/HighlightAction.cs | 24 ++ .../ServerSource/Events/Missions/Mission.cs | 8 +- .../GameModes/MultiPlayerCampaign.cs | 48 ++- .../ServerSource/Items/Inventory.cs | 5 +- .../ServerSource/Items/Item.cs | 14 + .../ServerSource/Items/ItemEventData.cs | 21 +- .../ServerSource/Networking/GameServer.cs | 10 +- .../ServerEntityEventManager.cs | 5 +- .../ServerSource/Networking/RespawnManager.cs | 2 +- .../ServerSource/Traitors/TraitorManager.cs | 11 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/AIController.cs | 7 +- .../Characters/AI/EnemyAIController.cs | 15 +- .../Characters/AI/HumanAIController.cs | 98 +++--- .../AI/Objectives/AIObjectiveCombat.cs | 6 +- .../Objectives/AIObjectiveEscapeHandcuffs.cs | 10 +- .../AI/Objectives/AIObjectiveFindThieves.cs | 2 +- .../AI/Objectives/AIObjectiveIdle.cs | 2 +- .../SharedSource/Characters/AI/Order.cs | 14 +- .../Animation/HumanoidAnimController.cs | 50 ++- .../Characters/Animation/Ragdoll.cs | 131 ++++--- .../SharedSource/Characters/Character.cs | 150 ++++---- .../SharedSource/Characters/CharacterInfo.cs | 36 +- .../Health/Afflictions/AfflictionPrefab.cs | 4 + .../Characters/Health/CharacterHealth.cs | 2 + .../Characters/Params/CharacterParams.cs | 14 +- .../SharedSource/Characters/SkillSettings.cs | 7 + .../AbilityConditionCharacter.cs | 54 ++- .../AbilityConditionCharacterNotLooted.cs | 8 +- .../AbilityConditionCharacterUnconcious.cs | 8 +- .../AbilityConditionItemIsStatic.cs | 19 + .../AbilityConditionHasPermanentStat.cs | 13 +- .../AbilityConditionLowestLevel.cs | 13 +- .../CharacterAbilityGiveExperience.cs | 6 +- .../Abilities/CharacterAbilityGiveItemStat.cs | 4 +- .../CharacterAbilityGiveItemStatToTags.cs | 4 +- .../CharacterAbilityGivePermanentStat.cs | 38 +- .../AbilityGroups/CharacterAbilityGroup.cs | 4 + .../CircuitBox/ItemSlotIndexPair.cs | 19 +- .../ContentManagement/ContentFile/TextFile.cs | 17 +- .../ContentPackageManager.cs | 1 + .../SharedSource/DebugConsole.cs | 57 +-- .../BarotraumaShared/SharedSource/Enums.cs | 27 +- .../EventActions/CheckConditionalAction.cs | 103 +++++- .../EventActions/CheckVisibilityAction.cs | 19 +- .../Events/EventActions/EventAction.cs | 32 +- .../EventActions/EventObjectiveAction.cs | 2 +- .../SharedSource/Events/EventActions/GoTo.cs | 18 +- .../Events/EventActions/HighlightAction.cs | 43 +++ .../Events/EventActions/MissionAction.cs | 4 +- .../EventActions/ModifyLocationAction.cs | 6 +- .../EventActions/NPCChangeTeamAction.cs | 2 +- .../Events/EventActions/TagAction.cs | 78 ++++- .../EventActions/TutorialHighlightAction.cs | 34 -- .../EventActions/WaitForItemUsedAction.cs | 31 +- .../SharedSource/Events/EventManager.cs | 14 +- .../Events/Missions/CombatMission.cs | 2 +- .../SharedSource/Events/Missions/Mission.cs | 11 +- .../SharedSource/Events/MonsterEvent.cs | 4 +- .../SharedSource/Events/ScriptedEvent.cs | 96 ++++- .../Extensions/IEnumerableExtensions.cs | 5 + .../SharedSource/GameSession/CrewManager.cs | 9 +- .../GameSession/Data/CampaignMetadata.cs | 9 +- .../GameSession/Data/Reputation.cs | 2 +- .../GameSession/GameModes/CampaignMode.cs | 25 +- .../SharedSource/GameSession/GameSession.cs | 11 +- .../SharedSource/GameSession/HireManager.cs | 20 +- .../SharedSource/Items/CharacterInventory.cs | 13 + .../SharedSource/Items/Components/Door.cs | 9 +- .../Items/Components/Holdable/IdCard.cs | 3 - .../Items/Components/Holdable/MeleeWeapon.cs | 5 +- .../Items/Components/Holdable/Pickable.cs | 2 +- .../Items/Components/Holdable/RepairTool.cs | 12 +- .../Items/Components/ItemComponent.cs | 18 +- .../Items/Components/ItemContainer.cs | 26 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Engine.cs | 4 +- .../Items/Components/Machines/Fabricator.cs | 113 ++++-- .../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 | 1 + .../Items/Components/Projectile.cs | 5 +- .../Items/Components/Repairable.cs | 6 +- .../Items/Components/Signal/CircuitBox.cs | 2 +- .../Components/Signal/ConnectionPanel.cs | 6 + .../Items/Components/Signal/LightComponent.cs | 36 +- .../Items/Components/TriggerComponent.cs | 9 + .../SharedSource/Items/Components/Turret.cs | 136 +++++++- .../SharedSource/Items/Components/Wearable.cs | 1 + .../SharedSource/Items/Inventory.cs | 10 +- .../SharedSource/Items/Item.cs | 57 ++- .../SharedSource/Items/ItemEventData.cs | 3 +- .../SharedSource/Items/ItemPrefab.cs | 34 +- .../SharedSource/Items/ItemStatManager.cs | 87 ++++- .../BarotraumaShared/SharedSource/Map/Hull.cs | 5 + .../SharedSource/Map/Levels/Level.cs | 25 +- .../SharedSource/Map/Levels/LevelData.cs | 2 +- .../Levels/LevelObjects/LevelObjectManager.cs | 1 + .../SharedSource/Map/Map/Location.cs | 145 ++++++-- .../SharedSource/Map/Map/LocationType.cs | 100 +++++- .../SharedSource/Map/Map/Map.cs | 22 +- .../SharedSource/Map/MapEntity.cs | 10 +- .../SharedSource/Map/Structure.cs | 16 +- .../SharedSource/Map/Submarine.cs | 156 +++++---- .../SharedSource/Map/SubmarineBody.cs | 53 ++- .../SharedSource/Networking/EntitySpawner.cs | 6 +- .../Networking/OrderChatMessage.cs | 2 +- .../SharedSource/Networking/RespawnManager.cs | 7 + .../SharedSource/Networking/ServerSettings.cs | 7 + .../SharedSource/Settings/GameSettings.cs | 2 + .../SharedSource/SteamAchievementManager.cs | 1 + .../BarotraumaShared/SharedSource/Tags.cs | 10 + .../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 +- .../SharedSource/Utils/MathUtils.cs | 54 ++- .../SharedSource/Utils/SaveUtil.cs | 9 +- .../SharedSource/Utils/Shapes/Quad2D.cs | 113 ++++++ .../SharedSource/Utils/Shapes/Triangle2D.cs | 21 ++ Barotrauma/BarotraumaShared/changelog.txt | 117 +++++++ .../FabricatorQualityRollTests.cs | 66 ++++ 210 files changed, 4201 insertions(+), 1283 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/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 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..f7af8779d 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; @@ -521,22 +522,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 de3f40011..d08948b86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -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); @@ -2234,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; } @@ -3094,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/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 6b408b9a3..d5687b38a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -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/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/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 270c2e6ec..4ebc75a56 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -2127,11 +2127,10 @@ namespace Barotrauma { var dialog = new GUIMessageBox( TextManager.Get("newsupplies"), - TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name)); + TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName)); dialog.Buttons[0].OnClicked += dialog.Close; } } - return false; } 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/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/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 4c143b2ee..80723ea67 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -192,12 +192,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: @@ -205,7 +205,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; @@ -221,7 +221,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 8c408cf95..ce6972067 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -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/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index b90e5bac4..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; } @@ -761,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 35799ed55..21b3c9c76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -346,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; } @@ -571,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; } @@ -589,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 f300ae562..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 @@ -825,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) @@ -853,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')); @@ -865,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) { @@ -875,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'); @@ -894,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 = ""; @@ -921,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), @@ -946,7 +1004,6 @@ namespace Barotrauma.Items.Components font: GUIStyle.SmallFont); } - return true; } public void HighlightRecipe(string identifier, Color color) @@ -1056,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 a5d5766f1..1e636ec15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -991,7 +991,7 @@ 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, @@ -1001,7 +1001,7 @@ namespace Barotrauma.Items.Components 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, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index c3827e5b5..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() 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 5163a7196..054df6051 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -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,19 @@ 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) + { + if (!container.AllowDragAndDrop || !container.DrawInventory) + { + allowCombine = false; + } + } + bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, allowCombine, Character.Controlled); if (success) { anySuccess = true; @@ -1380,18 +1396,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 +1415,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 +1606,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; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index e2cb908f9..1633cf7bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -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) @@ -386,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, @@ -401,20 +402,7 @@ namespace Barotrauma textureScale: Vector2.One * Scale, depth: d); } - foreach (var decorativeSprite in Prefab.DecorativeSprites) - { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - - Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color); - 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: decorativeSpriteColor, - 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,21 +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; } - Color decorativeSpriteColor = GetSpriteColor(decorativeSprite.Color); - 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)), decorativeSpriteColor, - 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) @@ -492,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) @@ -524,7 +485,6 @@ namespace Barotrauma rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } - } activeSprite.effects = oldEffects; @@ -569,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) { @@ -629,6 +595,55 @@ namespace Barotrauma } } + 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) { if (impact > 1.0f && @@ -804,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(); @@ -861,6 +889,11 @@ namespace Barotrauma CanBeFocused = true }; + 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"), @@ -873,6 +906,7 @@ namespace Barotrauma } if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); } ColorFlipButton(button, FlippedX); + if (rotationField != null) { rotationField.FloatValue = Rotation; } return true; } }; @@ -889,6 +923,7 @@ namespace Barotrauma } if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } ColorFlipButton(button, FlippedY); + if (rotationField != null) { rotationField.FloatValue = Rotation; } return true; } }; @@ -1553,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}"); } @@ -1953,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 995e56880..7a7701b9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -371,19 +371,27 @@ namespace Barotrauma { if (!ResizeHorizontal && !ResizeVertical) { - sprite.Draw(spriteBatch, new Vector2(placeRect.Center.X, -(placeRect.Y - placeRect.Height / 2)), SpriteColor * 0.8f, scale: scale, rotate: rotation); + 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 { Vector2 position = placeRect.Location.ToVector2(); Vector2 placeSize = placeRect.Size.ToVector2(); sprite?.DrawTiled( - spriteBatch, - new Vector2(position.X, -position.Y), - placeSize, + spriteBatch: spriteBatch, + position: new Vector2(position.X, -position.Y), + targetSize: placeSize, rotation: rotation, textureScale: Vector2.One * scale, - color: SpriteColor * 0.8f); + color: SpriteColor * 0.8f, + spriteEffects: spriteEffects ^ sprite.effects); } } 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/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 5c54904ba..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 { 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..90dcac14c 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; } @@ -259,7 +268,8 @@ namespace Barotrauma.Lights { if (!light.Enabled) { continue; } if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } - + //above the top boundary of the level (in an inactive respawn shuttle?) + if (Level.Loaded != null && light.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } if (light.ParentBody != null) { light.ParentBody.UpdateDrawPosition(); @@ -801,6 +811,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 777b0ebd9..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) { @@ -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/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index a9ab8cc23..4e97d4929 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -162,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(); } } @@ -1332,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/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index d1453f665..4fe5ce8e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -236,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; @@ -312,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))); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index 06e17348a..587e21dab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -96,9 +96,6 @@ namespace Barotrauma 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; @@ -109,9 +106,8 @@ namespace Barotrauma color: Color.White * 0.8f, origin: placeRect.Size.ToVector2() * 0.5f, rotation: rotation, - textureScale: TextureScale * scale); - - Sprite.effects = oldEffects; + textureScale: TextureScale * scale, + spriteEffects: spriteEffects ^ Sprite.effects); } } } 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/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 6fa7ec90f..c6d338147 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -2222,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); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index ab0cc11ea..4ce4dd2e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -181,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())) @@ -263,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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 1cefb2a67..a78816c2a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -254,7 +254,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 +598,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 143100d4d..0982130cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -165,6 +165,7 @@ namespace Barotrauma } } #else + SpamServerFilters.RequestGlobalSpamFilter(); FetchRemoteContent(); #endif diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 7e8b006c9..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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index f08ce1d14..d48c4bc39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -655,6 +655,7 @@ namespace Barotrauma ScrollBarVisible = true, OnSelected = (btn, obj) => { + if (GUI.MouseOn is GUIButton) { return false; } if (obj is not ServerInfo serverInfo) { return false; } joinButton.Enabled = true; @@ -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 a48cb05d6..63e8af01b 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; @@ -1560,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 @@ -3933,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)); } } @@ -5485,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; } } 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 25ea1f177..d050e09c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -1,15 +1,16 @@ -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 @@ -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/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 f76e97b23..5314247ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -284,13 +284,22 @@ namespace Barotrauma } } - 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) + 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; } - bool flipHorizontal = (effects & SpriteEffects.FlipHorizontally) != 0; - bool flipVertical = (effects & SpriteEffects.FlipVertically) != 0; + 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; } @@ -311,7 +320,7 @@ namespace Barotrauma Vector2 transformedPos = slicePos - position; transformedPos = advanceX * transformedPos.X + advanceY * transformedPos.Y; transformedPos += position - transformedOrigin; - spriteBatch.Draw(texture, transformedPos, sliceRect, drawColor, addedRotation, Vector2.Zero, scale, effects, depth ?? this.depth); + spriteBatch.Draw(texture, transformedPos, sliceRect, drawColor, addedRotation, Vector2.Zero, scale, spriteEffects.Value, depth ?? this.depth); } //wrap the drawOffset inside the sourceRect 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/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/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 e31bc063d..6f4b4da41 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.1.0 + 1.2.4.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 42698bdfd..38f410733 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.1.0 + 1.2.4.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 38e7e8161..2b8cb45e3 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.2.1.0 + 1.2.4.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 093a855f3..d921409b5 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.1.0 + 1.2.4.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 04527ab69..9c8837281 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.1.0 + 1.2.4.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 532c057e7..aebcc26a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1134,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); } } @@ -1416,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 { @@ -1461,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]}."); @@ -1484,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() }; })); @@ -2457,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); } ); 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/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 69a72d1b7..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; @@ -1231,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/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index d2afe45b1..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,8 +107,7 @@ 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"})", item.Prefab.ContentPackage); 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/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index efd98d89e..a790ba686 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -804,7 +804,7 @@ namespace Barotrauma.Networking { using (dosProtection.Pause(connectedClient)) { - MultiPlayerCampaign.LoadCampaign(saveName); + MultiPlayerCampaign.LoadCampaign(saveName, connectedClient); } } } @@ -1230,11 +1230,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 +1479,7 @@ namespace Barotrauma.Networking { using (dosProtection.Pause(sender)) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath, sender); } } } @@ -3945,7 +3940,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/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index 55eed416d..498108b29 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -263,7 +263,7 @@ namespace Barotrauma if (amountToChoose > viableTraitors.Count) { DebugConsole.ThrowError( - $"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} 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; @@ -455,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 be16280af..06bd6d52a 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.2.1.0 + 1.2.4.0 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer 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 adbf7e933..d4188f275 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -262,7 +262,7 @@ 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); @@ -312,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); @@ -322,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 @@ -565,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); @@ -3097,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 95bae3353..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]; @@ -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) 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 index bff65d6b5..f6ac0bec8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs @@ -114,7 +114,7 @@ namespace Barotrauma 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)) + character.CanSeeTarget(target, seeThroughWindows: true)) { AIObjectiveCheckStolenItems? existingObjective = objectiveManager.GetActiveObjectives().FirstOrDefault(o => o.TargetCharacter == target); 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/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 4c862b4aa..04ee9ccad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -664,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) @@ -713,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/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 4004391cc..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; } @@ -501,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; } @@ -586,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( @@ -764,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); } @@ -1536,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 @@ -1741,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 048d81e17..ffebedae2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -572,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++) @@ -582,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) @@ -668,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; } @@ -710,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) @@ -1335,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 @@ -1593,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 || @@ -1621,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; @@ -1700,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/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index d2f146fa7..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(); @@ -1694,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)); @@ -2303,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) @@ -2365,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; } @@ -2382,39 +2386,37 @@ 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) + if (item.GetComponent() is { HasWindow: true } door && seeThroughWindows) { if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; } } @@ -2513,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; } @@ -3584,6 +3598,8 @@ namespace Barotrauma private void Despawn(bool createNetworkEvents = true) { + if (!EnableDespawn) { return; } + Identifier despawnContainerId = IsHuman ? "despawncontainer".ToIdentifier() : @@ -3665,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) @@ -3683,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; @@ -4340,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); } } @@ -4366,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); + } } /// @@ -5005,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() @@ -5021,7 +5037,7 @@ namespace Barotrauma foreach (var hull in adjacentHulls) { if (hull.ConnectedGaps.Any(g => - (g.Open > 0.9f || g.ConnectedDoor is { HasWindow: true }) && + g.Open > 0.9f && g.linkedTo.Contains(CurrentHull) && Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2))) { @@ -5041,7 +5057,7 @@ namespace Barotrauma else { if (h.ConnectedGaps.Any(g => - (g.Open > 0.9f || g.ConnectedDoor is { HasWindow: true }) && + g.Open > 0.9f && Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2) && CanSeeTarget(g))) { @@ -5582,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 0752b6b3f..2b8466406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -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); @@ -1902,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/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 6f5ac8417..035a8c9ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -1026,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()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 3df905b80..7e813b74a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -1313,6 +1313,8 @@ namespace Barotrauma public void Remove() { RemoveProjSpecific(); + afflictionsToRemove.Clear(); + afflictionsToUpdate.Clear(); } partial void RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 289940df0..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) @@ -603,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); @@ -732,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/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/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index 8b8fe05cb..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,6 +10,12 @@ 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( @@ -25,30 +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.", 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/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/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs index 96ed33dab..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; @@ -21,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/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/CharacterAbilityGiveExperience.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs index 56807217a..3aa2719ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs @@ -38,7 +38,11 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility 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/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index c046863a6..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; @@ -40,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) @@ -72,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); } } @@ -83,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/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index cb33943db..4d177dd69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -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; } } 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/TextFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs index 68d3b4184..ff4493c9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs @@ -19,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(); } @@ -33,9 +32,9 @@ 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)) @@ -49,7 +48,11 @@ namespace Barotrauma 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/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/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 2599ebd65..ae30a5412 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -858,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) @@ -877,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() }; })); @@ -1613,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) => @@ -1627,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 @@ -1641,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); } })); @@ -1913,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}"; @@ -2417,11 +2432,7 @@ namespace Barotrauma public static void LogError(string msg, Color? color = null, ContentPackage contentPackage = null) { - if (contentPackage != null) - { - string colorStr = XMLExtensions.ToStringHex(Color.MediumPurple); - msg = $"‖color:{colorStr}‖[{contentPackage.Name}]‖color:end‖ {msg}"; - } + msg = AddContentPackageInfoToMessage(msg, contentPackage); color ??= Color.Red; NewMessage(msg, color.Value, isCommand: false, isError: true); } @@ -2557,11 +2568,7 @@ namespace Barotrauma public static void ThrowError(string error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false) { - if (contentPackage != null) - { - string color = XMLExtensions.ToStringHex(Color.MediumPurple); - error = $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {error}"; - } + error = AddContentPackageInfoToMessage(error, contentPackage); if (e != null) { error += " {" + e.Message + "}\n"; @@ -2610,16 +2617,22 @@ namespace Barotrauma public static void AddWarning(string warning, ContentPackage contentPackage = null) { - warning = $"WARNING: {warning}"; - if (contentPackage != null) - { - string color = XMLExtensions.ToStringHex(Color.MediumPurple); - warning = $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {warning}"; - } + warning = AddContentPackageInfoToMessage($"WARNING: {warning}", contentPackage); System.Diagnostics.Debug.WriteLine(warning); 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 private static IEnumerable CreateMessageBox(string errorMsg) { 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/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index d59add1f5..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,7 +11,16 @@ 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) { @@ -17,14 +29,38 @@ namespace Barotrauma 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()) + { + //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(); + } + + 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 IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag"; + 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() @@ -34,32 +70,65 @@ namespace Barotrauma protected override bool? DetermineSuccess() { - ISerializableEntity target = null; + IEnumerable targets = null; if (!TargetTag.IsEmpty) { - foreach (var t in ParentEvent.GetTargets(TargetTag)) - { - if (t is ISerializableEntity e) - { - target = e; - break; - } - } + targets = ParentEvent.GetTargets(TargetTag).OfType(); } - if (target == null) + + 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 (target == null || Conditional == null) + + if (targets.None() || Conditionals.None()) { + foreach (var target in targets) + { + ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls); + } 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/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/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index 4585edbc4..f9be2652e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -103,7 +103,7 @@ namespace Barotrauma public EventAction(ScriptedEvent parentEvent, ContentXElement element) { - ParentEvent = parentEvent ?? throw new ArgumentNullException(nameof(parentEvent)); + ParentEvent = parentEvent; SerializableProperty.DeserializeProperties(this, element); } @@ -141,7 +141,11 @@ 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(); } @@ -170,6 +174,30 @@ namespace Barotrauma } } + 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/EventObjectiveAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs index 82cbb6ee8..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; } 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 d87c58be1..4592e6751 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -123,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), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs index cd044361b..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; @@ -77,9 +77,9 @@ namespace Barotrauma 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 239312aff..239b95f60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -101,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/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 7374e3466..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; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs deleted file mode 100644 index e227f401d..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialHighlightAction.cs +++ /dev/null @@ -1,34 +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.", - contentPackage: element.ContentPackage); - } - } - - 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/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 183ba0a2b..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; } @@ -905,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/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/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 717befcb9..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); } @@ -431,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)); @@ -652,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/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 03f144e2c..187c8f919 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -673,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)) { @@ -700,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 7503af5c5..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; @@ -191,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)) { @@ -229,7 +243,6 @@ namespace Barotrauma } List targetsToReturn = new List(); - if (Targets.ContainsKey(tag)) { foreach (Entity e in Targets[tag]) @@ -240,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); + } } } } @@ -293,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++; @@ -310,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; @@ -369,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/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 4f6455dba..762807d0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -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 0e5bfb3f0..3b10f6269 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -558,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" + @@ -570,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" + @@ -582,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") + ", " + @@ -1024,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) @@ -1034,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); @@ -1263,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++) @@ -1271,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); } } @@ -1307,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); 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/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 1a3e665aa..a8d47043e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -151,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 9f80f04ad..041465193 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -343,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 @@ -404,7 +409,7 @@ namespace Barotrauma.Items.Components position.Y >= item.Rect.Y + Window.Y && position.Y <= item.Rect.Y + Window.Y + Window.Height && position.X >= item.Rect.X - maxPerpendicularDistance && - position.Y <= item.Rect.Right + maxPerpendicularDistance; + position.X <= item.Rect.Right + maxPerpendicularDistance; } } 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/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index a33359230..4f0686cef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -572,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) @@ -660,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/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 4e1862e59..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; @@ -394,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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 015807276..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(); @@ -298,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++) { @@ -389,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) @@ -397,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)); @@ -405,7 +417,6 @@ namespace Barotrauma.Items.Components } } - var relatedItem = FindContainableItem(containedItem); var containedItemInfo = new ContainedItem(containedItem, Hide: relatedItem?.Hide ?? false, ItemPos: relatedItem?.ItemPos, @@ -786,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)); } /// @@ -1095,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) @@ -1107,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/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 0bc72b64c..63de9cce6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -469,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; @@ -534,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); } @@ -576,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); @@ -593,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; } @@ -698,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 c0d788133..93d47b5c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -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 6e5363695..9b3444649 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -1017,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/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/Signal/CircuitBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs index d2675e51b..69c395e3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -157,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()) { 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 d6af54c78..8f1228208 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -275,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 41b58165d..180487e83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -559,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..cebc3baad 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. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index edb9917cc..c44369a96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -383,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 @@ -393,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) { @@ -1327,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) { @@ -1640,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 /// @@ -2516,7 +2522,7 @@ namespace Barotrauma if (Prefab.AllowRotatingInEditor) { - RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad); + RotationRad = MathUtils.WrapAnglePi(-RotationRad); } #if CLIENT if (Prefab.CanSpriteFlipX) @@ -2543,6 +2549,10 @@ namespace Barotrauma return; } + if (Prefab.AllowRotatingInEditor) + { + RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad); + } #if CLIENT if (Prefab.CanSpriteFlipY) { @@ -3043,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))) { @@ -3068,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; } } } @@ -3526,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; } @@ -3799,6 +3831,11 @@ namespace Barotrauma } break; } + case "itemstats": + { + item.StatManager.Load(subElement); + break; + } default: { ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString()); @@ -3924,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(); } @@ -3994,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 ddedb9091..1b6d9aff5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -871,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)] @@ -1138,10 +1157,13 @@ namespace Barotrauma 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 = GetParentModPackageOrThisPackage(); + 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.", 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/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/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 106c1354b..5db866ad8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -633,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); @@ -4122,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 @@ -4230,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); @@ -4254,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 @@ -4263,7 +4276,7 @@ namespace Barotrauma if (EndLocation != null) { outpost.TeamID = EndLocation.Type.OutpostTeam; - outpost.Info.Name = EndLocation.Name; + outpost.Info.Name = EndLocation.DisplayName.Value; } } } 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/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 29ca85cf5..98b36532c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -639,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 cb8c2faf5..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); @@ -641,7 +658,7 @@ namespace Barotrauma LevelData = new LevelData(element.GetChildElement("Level"), clampDifficultyToBiome: true); - PortraitId = ToolBox.StringToInt(Name); + PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value); LoadStores(element); LoadMissions(element); @@ -687,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); @@ -698,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); @@ -735,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) { @@ -1058,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; } @@ -1086,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) @@ -1125,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; @@ -1133,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; @@ -1444,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), @@ -1455,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)); @@ -1491,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/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/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 10e72f4ca..afbdb5271 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -733,6 +733,12 @@ namespace Barotrauma } } + 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. /// @@ -912,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; @@ -1304,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); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 459307b73..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); @@ -929,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; @@ -968,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; @@ -1885,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) { @@ -1925,9 +1950,11 @@ namespace Barotrauma Ragdoll.RemoveAll(); PhysicsBody.RemoveAll(); + StatusEffect.StopAll(); GameMain.World = null; Powered.Grids.Clear(); + Powered.ChangedConnections.Clear(); GC.Collect(); @@ -1947,6 +1974,7 @@ namespace Barotrauma outdoorNodes?.Clear(); outdoorNodes = null; + obstructedNodes.Clear(); GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged?.TryDeregister(upgradeEventIdentifier); @@ -1958,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 9976f1f51..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); } } } 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 2a782728c..c105f27de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -848,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/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 7d7012dcd..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; 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 53282a021..830c43168 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs @@ -100,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/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 8deb9ec70..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); } @@ -1091,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; } @@ -1106,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 2adfd257b..4eec61623 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,120 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.4.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed captain tutorial's drone steering objective completing automatically. +- Fixed items inside held items (like tanks in welding tools) being mirrored vertically when you're facing left. +- Fixed lights sometimes being visible even if the item emitting the light is hidden inside an inventory. +- Fixed items sometimes not emitting light when brought outside. +- Fixed looping sounds occasionally not stopping when they should. Happened in situations where there was no streaming audio active (i.e. no music or background ambience). +- Fixed items with an inaccessible/hidden inventory (like magazines) no longer stacking. +- Fixed inability to edit components' values in circuit boxes in multiplayer. + +Fixes: +- Fixed turret lights always being forced on at the start of the round. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.3.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed crashing when a bot tries to fire a turret at an ice spire. +- Fixed bots trying to shoot through windowed doors and glass walls. +- Adjusted indentured servitude talent: you only get a discount of 20% from assistants (the effects do still stack though, so you if you have lots of assistants with the talent in the crew, you can get a 100% discount). We're still not completely sure about this: is it too powerful/balance-breaking to get free crewmembers? +- Fixed "starter quest" talent crashing the game when you kill a crawler. + +Changes and additions: +- 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". + +Optimization: +- 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. + +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. + +Talents: +- Fixed "junction junkie" not working on sonar monitors. + +Fixes: +- 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. + +Modding: +- 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. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.2.2.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed rotated structures sometimes getting culled even though they're still visible on the screen. +- Fixed area select not working correctly on rotated structures in the sub editor. +- Fixed rotated structures not appearing rotated on the submarine preview. + +Changes and additions: +- 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. +- 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. +- Added new variants of the Monsters Nearby and Submarine Flooded tracks. +- Location names can now be translated. In the vanilla game the names are only translated if you're playing in Chinese. +- Minor adjustments to level layouts: all the levels now slope down a bit to give the impression you're actually heading deeper. + +Talents: +- Five new assistant talents: Starter Quest, Mule, Jenga Master, Indentured Servitude, Tasty Target. + +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. +- Miscellaneous small optimizations. + +Traitors: +- 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). + +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 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. + +Modding: +- 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. + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.2.1.0 ------------------------------------------------------------------------------------------------------------------------------------------------- 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 From ff1b8951a756f7689f3d9a602ee4e53df9995448 Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Wed, 24 Apr 2024 18:09:05 +0300 Subject: [PATCH 3/3] v1.4.4.1 (Blood in the Water Update) --- .../BarotraumaClient/ClientSource/Camera.cs | 2 +- .../Characters/Animation/Ragdoll.cs | 2 +- .../ClientSource/Characters/Character.cs | 415 ++- .../ClientSource/Characters/CharacterHUD.cs | 163 +- .../ClientSource/Characters/CharacterInfo.cs | 19 +- .../Characters/CharacterNetworking.cs | 42 + .../Characters/Health/CharacterHealth.cs | 12 +- .../Characters/InteractionLabelManager.cs | 328 +++ .../ClientSource/Characters/Limb.cs | 16 +- .../CircuitBox/CircuitBoxComponent.cs | 7 +- .../CircuitBox/CircuitBoxLabelNode.cs | 184 ++ .../CircuitBoxMouseDragSnapshotHandler.cs | 75 +- .../ClientSource/CircuitBox/CircuitBoxNode.cs | 10 +- .../ClientSource/CircuitBox/CircuitBoxUI.cs | 229 +- .../ClientSource/DebugConsole.cs | 134 +- .../EventActions/CheckObjectiveAction.cs | 8 +- .../Events/EventActions/MessageBoxAction.cs | 1 - .../Events/Missions/CargoMission.cs | 4 +- .../ClientSource/Events/Missions/Mission.cs | 4 +- .../Events/Missions/SalvageMission.cs | 37 +- .../ClientSource/GUI/ComponentStyle.cs | 5 + .../ClientSource/GUI/CrewManagement.cs | 29 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 88 +- .../ClientSource/GUI/GUIComponent.cs | 33 +- .../ClientSource/GUI/GUINumberInput.cs | 156 +- .../ClientSource/GUI/GUIPrefab.cs | 8 +- .../ClientSource/GUI/GUIScrollBar.cs | 2 +- .../ClientSource/GUI/GUISelectionCarousel.cs | 191 ++ .../ClientSource/GUI/GUIStyle.cs | 16 +- .../ClientSource/GUI/GUITextBox.cs | 7 +- .../ClientSource/GUI/GUITickBox.cs | 6 +- .../ClientSource/GUI/RectTransform.cs | 18 + .../ClientSource/GUI/Store.cs | 3 + .../ClientSource/GUI/TabMenu.cs | 2 +- .../ClientSource/GUI/TalentMenu.cs | 22 +- .../ClientSource/GUI/UpgradeStore.cs | 2 +- .../ClientSource/GUI/Widget.cs | 122 +- .../BarotraumaClient/ClientSource/GameMain.cs | 22 +- .../ClientSource/GameSession/CrewManager.cs | 104 +- .../GameSession/GameModes/CampaignMode.cs | 9 +- .../GameModes/MultiPlayerCampaign.cs | 14 +- .../GameModes/SinglePlayerCampaign.cs | 14 +- .../GameSession/GameModes/TestGameMode.cs | 13 +- .../GameModes/Tutorials/Tutorial.cs | 6 +- .../ClientSource/GameSession/GameSession.cs | 7 +- .../ClientSource/GameSession/HintManager.cs | 33 +- .../ClientSource/GameSession/RoundSummary.cs | 11 +- .../GameSession/UpgradeManager.cs | 20 +- .../ClientSource/Items/CharacterInventory.cs | 2 +- .../ClientSource/Items/Components/Door.cs | 15 +- .../Items/Components/Holdable/Holdable.cs | 3 +- .../Items/Components/Holdable/RangedWeapon.cs | 46 +- .../Items/Components/ItemContainer.cs | 4 +- .../Items/Components/LightComponent.cs | 1 + .../Components/Machines/Deconstructor.cs | 19 +- .../Items/Components/Machines/Engine.cs | 6 +- .../Items/Components/Machines/Fabricator.cs | 12 +- .../Items/Components/Machines/MiniMap.cs | 7 +- .../Components/Machines/OutpostTerminal.cs | 2 +- .../Items/Components/Machines/Sonar.cs | 7 +- .../Items/Components/Machines/Steering.cs | 14 +- .../Items/Components/Projectile.cs | 18 + .../Items/Components/Repairable.cs | 13 +- .../ClientSource/Items/Components/Rope.cs | 99 +- .../Items/Components/Signal/CircuitBox.cs | 127 +- .../Items/Components/Signal/MotionSensor.cs | 12 +- .../Items/Components/StatusHUD.cs | 1 + .../ClientSource/Items/Components/Turret.cs | 45 +- .../ClientSource/Items/Inventory.cs | 132 +- .../ClientSource/Items/Item.cs | 579 ++++- .../BarotraumaClient/ClientSource/Map/Gap.cs | 1 - .../BarotraumaClient/ClientSource/Map/Hull.cs | 8 +- .../ClientSource/Map/ItemAssemblyPrefab.cs | 34 +- .../Map/Levels/DestructibleLevelWall.cs | 12 +- .../ClientSource/Map/Levels/Level.cs | 1 + .../Levels/LevelObjects/LevelObjectManager.cs | 2 +- .../ClientSource/Map/Levels/LevelRenderer.cs | 7 + .../ClientSource/Map/Lights/ConvexHull.cs | 12 +- .../ClientSource/Map/Lights/LightManager.cs | 19 +- .../ClientSource/Map/Map/Map.cs | 11 +- .../ClientSource/Map/MapEntity.cs | 17 +- .../ClientSource/Map/MapEntityPrefab.cs | 22 +- .../ClientSource/Map/RoundSound.cs | 5 +- .../ClientSource/Map/Structure.cs | 67 +- .../ClientSource/Map/Submarine.cs | 88 +- .../ClientSource/Map/SubmarineBody.cs | 40 + .../ClientSource/Map/WayPoint.cs | 7 +- .../ClientSource/Networking/BanList.cs | 20 +- .../ClientSource/Networking/Client.cs | 23 +- .../ClientSource/Networking/EntitySpawner.cs | 14 +- .../ClientSource/Networking/GameClient.cs | 85 +- .../ClientEntityEventManager.cs | 7 +- .../Networking/Primitives/Peers/ClientPeer.cs | 3 + .../ClientSource/Networking/RespawnManager.cs | 32 +- .../ClientSource/Networking/ServerSettings.cs | 1039 +------- .../Networking/ServerSettingsUI.cs | 924 +++++++ .../Networking/Voip/VoipCapture.cs | 2 +- .../Networking/Voip/VoipClient.cs | 10 +- .../ClientSource/Particles/Particle.cs | 16 +- .../ClientSource/Particles/ParticlePrefab.cs | 7 +- .../CampaignSetupUI/CampaignSetupUI.cs | 567 ++++- .../MultiPlayerCampaignSetupUI.cs | 210 +- .../CharacterEditor/CharacterEditorScreen.cs | 489 ++-- .../Screens/CharacterEditor/Wizard.cs | 22 +- .../ClientSource/Screens/EditorImage.cs | 14 +- .../Screens/EventEditor/EventEditorScreen.cs | 35 +- .../ClientSource/Screens/GameScreen.cs | 2 + .../ClientSource/Screens/LevelEditorScreen.cs | 33 +- .../Screens/MainMenuScreen/MainMenuScreen.cs | 6 +- .../ClientSource/Screens/ModDownloadScreen.cs | 39 +- .../ClientSource/Screens/NetLobbyScreen.cs | 2238 +++++++++-------- .../ServerListScreen/ServerListScreen.cs | 51 +- .../Screens/SpriteEditorScreen.cs | 62 +- .../ClientSource/Screens/SubEditorScreen.cs | 303 ++- .../Serialization/SerializableEntityEditor.cs | 123 +- .../ClientSource/Settings/SettingsMenu.cs | 83 +- .../ClientSource/Sounds/OggSound.cs | 2 + .../ClientSource/Sounds/Sound.cs | 18 + .../ClientSource/Sounds/SoundChannel.cs | 7 +- .../ClientSource/Sounds/SoundManager.cs | 12 +- .../ClientSource/Sounds/SoundPlayer.cs | 59 +- .../ClientSource/Sounds/VoipSound.cs | 11 +- .../ClientSource/Sprite/DecorativeSprite.cs | 16 +- .../ClientSource/Sprite/Sprite.cs | 2 +- .../StatusEffects/StatusEffect.cs | 50 +- .../Mutable/MutableWorkshopMenu.cs | 5 +- .../Steam/WorkshopMenu/Mutable/PublishTab.cs | 17 +- .../ClientSource/SubEditorCommands.cs | 2 +- .../ClientSource/Utils/ToolBox.cs | 33 + .../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 | 13 +- .../Characters/CharacterNetworking.cs | 27 +- .../ServerSource/DebugConsole.cs | 7 +- .../Events/Missions/SalvageMission.cs | 1 + .../GameModes/MultiPlayerCampaign.cs | 41 +- .../ServerSource/Items/Components/Door.cs | 3 + .../ServerSource/Items/Components/Rope.cs | 26 +- .../Items/Components/Signal/CircuitBox.cs | 65 +- .../ServerSource/Items/Inventory.cs | 19 +- .../ServerSource/Map/Submarine.cs | 26 +- .../ServerSource/Networking/BanList.cs | 2 +- .../ServerSource/Networking/GameServer.cs | 20 +- .../ServerEntityEventManager.cs | 22 +- .../Primitives/Peers/Server/ServerPeer.cs | 3 +- .../ServerSource/Networking/RespawnManager.cs | 51 +- .../ServerSource/Networking/ServerSettings.cs | 74 +- .../Networking/Voip/VoipServer.cs | 4 +- .../ServerSource/Screens/NetLobbyScreen.cs | 26 +- .../ServerSource/Traitors/TraitorManager.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Data/campaignsettings.xml | 76 +- .../Characters/AI/AIController.cs | 17 +- .../Characters/AI/EnemyAIController.cs | 232 +- .../Characters/AI/HumanAIController.cs | 65 +- .../SharedSource/Characters/AI/LatchOntoAI.cs | 108 +- .../Characters/AI/Objectives/AIObjective.cs | 41 +- .../AI/Objectives/AIObjectiveCleanupItem.cs | 13 +- .../AI/Objectives/AIObjectiveCleanupItems.cs | 24 +- .../AI/Objectives/AIObjectiveCombat.cs | 365 ++- .../Objectives/AIObjectiveDeconstructItem.cs | 116 + .../Objectives/AIObjectiveDeconstructItems.cs | 122 + .../AI/Objectives/AIObjectiveDecontainItem.cs | 3 + .../Objectives/AIObjectiveExtinguishFire.cs | 17 +- .../Objectives/AIObjectiveFightIntruders.cs | 33 +- .../Objectives/AIObjectiveFindDivingGear.cs | 61 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 34 +- .../AI/Objectives/AIObjectiveFindThieves.cs | 4 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 2 +- .../AI/Objectives/AIObjectiveGetItem.cs | 178 +- .../AI/Objectives/AIObjectiveGetItems.cs | 59 +- .../AI/Objectives/AIObjectiveGoTo.cs | 67 +- .../AI/Objectives/AIObjectiveIdle.cs | 14 +- .../AI/Objectives/AIObjectiveLoadItem.cs | 5 +- .../AI/Objectives/AIObjectiveManager.cs | 53 +- .../AI/Objectives/AIObjectiveOperateItem.cs | 4 + .../AI/Objectives/AIObjectivePrepare.cs | 28 +- .../AI/Objectives/AIObjectiveRepairItem.cs | 11 +- .../AI/Objectives/AIObjectiveRepairItems.cs | 2 +- .../AI/Objectives/AIObjectiveRescue.cs | 98 +- .../SharedSource/Characters/AI/Order.cs | 36 +- .../AI/ShipCommand/ShipIssueWorker.cs | 2 +- .../Characters/AI/SteeringManager.cs | 4 +- .../Characters/Animation/AnimController.cs | 358 ++- .../Animation/FishAnimController.cs | 20 +- .../Animation/HumanoidAnimController.cs | 51 +- .../Characters/Animation/Ragdoll.cs | 135 +- .../SharedSource/Characters/Attack.cs | 31 +- .../SharedSource/Characters/Character.cs | 225 +- .../Characters/CharacterEventData.cs | 55 +- .../SharedSource/Characters/CharacterInfo.cs | 80 +- .../Characters/CharacterPrefab.cs | 13 + .../Health/Afflictions/Affliction.cs | 11 +- .../Health/Afflictions/AfflictionHusk.cs | 18 +- .../Characters/Health/CharacterHealth.cs | 86 +- .../SharedSource/Characters/HumanPrefab.cs | 5 + .../SharedSource/Characters/Limb.cs | 90 +- .../Params/Animation/AnimationParams.cs | 332 +-- .../Params/Animation/FishAnimations.cs | 20 +- .../Params/Animation/HumanoidAnimations.cs | 27 +- .../Characters/Params/CharacterParams.cs | 114 +- .../Characters/Params/EditableParams.cs | 6 +- .../Params/Ragdoll/RagdollParams.cs | 250 +- .../AbilityConditionAffliction.cs | 4 +- ...rine.cs => AbilityConditionInSubmarine.cs} | 21 +- .../AbilityConditionHasLevel.cs | 19 +- .../AbilityConditionRagdolled.cs | 4 +- .../Talents/Abilities/CharacterAbility.cs | 2 +- .../CharacterAbilityGiveAffliction.cs | 44 +- .../CharacterAbilityGiveResistance.cs | 3 +- .../CharacterAbilityReduceAffliction.cs | 2 +- .../CharacterAbilityUpgradeSubmarine.cs | 57 + .../CharacterAbilityWarStories.cs | 63 + .../AbilityGroups/CharacterAbilityGroup.cs | 2 +- .../Characters/Talents/TalentPrefab.cs | 12 + .../Characters/Talents/TalentTree.cs | 11 +- .../CircuitBox/CircuitBoxLabelNode.cs | 83 + .../CircuitBox/CircuitBoxNetStructs.cs | 47 +- .../SharedSource/CircuitBox/CircuitBoxNode.cs | 47 +- .../CircuitBox/CircuitBoxSizes.cs | 1 + .../SharedSource/CircuitBox/CircuitBoxWire.cs | 10 +- .../ContentFile/CharacterFile.cs | 14 +- .../ContentFile/ContainerTagFile.cs | 16 + .../ContentManagement/ContentFile/TextFile.cs | 5 +- .../ContentPackageManager.cs | 9 +- .../ContentManagement/ContentPath.cs | 17 +- .../SharedSource/DebugConsole.cs | 32 +- .../BarotraumaShared/SharedSource/Enums.cs | 29 +- .../SharedSource/Events/ArtifactEvent.cs | 7 +- .../SharedSource/Events/Event.cs | 15 +- .../Events/EventActions/AfflictionAction.cs | 46 +- .../EventActions/CheckAfflictionAction.cs | 15 +- .../EventActions/CheckConditionalAction.cs | 12 +- .../EventActions/CheckConnectionAction.cs | 15 +- .../Events/EventActions/CheckDataAction.cs | 13 +- .../Events/EventActions/CheckItemAction.cs | 34 +- .../Events/EventActions/CheckMissionAction.cs | 11 +- .../Events/EventActions/CheckMoneyAction.cs | 10 +- .../EventActions/CheckObjectiveAction.cs | 3 + .../Events/EventActions/CheckOrderAction.cs | 13 +- .../EventActions/CheckPurchasedItemsAction.cs | 11 +- .../EventActions/CheckReputationAction.cs | 5 +- .../EventActions/CheckSelectedAction.cs | 9 +- .../Events/EventActions/CheckTalentAction.cs | 7 +- .../CheckTraitorEventStateAction.cs | 5 +- .../EventActions/CheckTraitorVoteAction.cs | 2 +- .../EventActions/CheckVisibilityAction.cs | 3 + .../Events/EventActions/ClearTagAction.cs | 8 +- .../Events/EventActions/CombatAction.cs | 18 +- .../Events/EventActions/ConversationAction.cs | 38 +- .../Events/EventActions/CountTargetsAction.cs | 25 +- .../Events/EventActions/EventLogAction.cs | 10 +- .../EventActions/EventObjectiveAction.cs | 65 +- .../Events/EventActions/FireAction.cs | 11 +- .../Events/EventActions/GiveExpAction.cs | 7 +- .../Events/EventActions/GiveSkillExpAction.cs | 9 +- .../SharedSource/Events/EventActions/GoTo.cs | 7 +- .../Events/EventActions/GodModeAction.cs | 7 +- .../Events/EventActions/HighlightAction.cs | 7 +- .../EventActions/InventoryHighlightAction.cs | 11 +- .../SharedSource/Events/EventActions/Label.cs | 5 +- .../Events/EventActions/LayerAction.cs | 61 + .../Events/EventActions/MessageBoxAction.cs | 33 +- .../Events/EventActions/MissionAction.cs | 18 +- .../Events/EventActions/MissionStateAction.cs | 10 +- .../EventActions/ModifyLocationAction.cs | 11 +- .../Events/EventActions/MoneyAction.cs | 11 +- .../EventActions/NPCChangeTeamAction.cs | 13 +- .../Events/EventActions/NPCFollowAction.cs | 13 +- .../EventActions/NPCOperateItemAction.cs | 33 +- .../Events/EventActions/NPCWaitAction.cs | 9 +- .../Events/EventActions/OnRoundEndAction.cs | 3 + .../Events/EventActions/RNGAction.cs | 9 +- .../Events/EventActions/RemoveItemAction.cs | 9 +- .../Events/EventActions/ReputationAction.cs | 17 +- .../Events/EventActions/SetDataAction.cs | 10 +- .../EventActions/SetPriceMultiplierAction.cs | 10 +- .../SetTraitorEventStateAction.cs | 5 +- .../Events/EventActions/SkillCheckAction.cs | 14 +- .../Events/EventActions/SpawnAction.cs | 15 +- .../Events/EventActions/StatusEffectAction.cs | 5 +- .../Events/EventActions/TagAction.cs | 89 +- .../Events/EventActions/TeleportAction.cs | 11 +- .../Events/EventActions/TriggerAction.cs | 4 +- .../Events/EventActions/TriggerEventAction.cs | 9 +- .../EventActions/TutorialCompleteAction.cs | 3 + .../Events/EventActions/TutorialIconAction.cs | 9 +- .../Events/EventActions/UIHighlightAction.cs | 19 +- .../Events/EventActions/UnlockPathAction.cs | 6 +- .../Events/EventActions/WaitAction.cs | 5 +- .../WaitForItemFabricatedAction.cs | 16 +- .../EventActions/WaitForItemUsedAction.cs | 19 +- .../SharedSource/Events/EventManager.cs | 28 +- .../SharedSource/Events/EventPrefab.cs | 10 +- .../SharedSource/Events/EventSet.cs | 20 +- .../SharedSource/Events/MalfunctionEvent.cs | 7 +- .../Events/Missions/BeaconMission.cs | 23 +- .../Events/Missions/CargoMission.cs | 58 +- .../Events/Missions/EscortMission.cs | 8 +- .../Events/Missions/MineralMission.cs | 2 +- .../SharedSource/Events/Missions/Mission.cs | 86 +- .../Events/Missions/MissionPrefab.cs | 39 +- .../Events/Missions/PirateMission.cs | 8 +- .../Events/Missions/SalvageMission.cs | 273 +- .../SharedSource/Events/MonsterEvent.cs | 97 +- .../SharedSource/Events/ScriptedEvent.cs | 2 +- .../GameSession/AutoItemPlacer.cs | 7 +- .../SharedSource/GameSession/CargoManager.cs | 4 + .../SharedSource/GameSession/CrewManager.cs | 33 +- .../GameSession/GameModes/CampaignMode.cs | 11 +- .../GameModes/CampaignModePresets.cs | 73 +- .../GameSession/GameModes/CampaignSettings.cs | 205 +- .../SharedSource/GameSession/GameSession.cs | 138 +- .../GameSession/UpgradeManager.cs | 14 + .../SharedSource/InputType.cs | 5 +- .../Items/Components/DockingPort.cs | 6 +- .../SharedSource/Items/Components/Door.cs | 9 +- .../SharedSource/Items/Components/Growable.cs | 3 + .../Items/Components/Holdable/Holdable.cs | 43 +- .../Components/Holdable/LevelResource.cs | 2 +- .../Items/Components/Holdable/MeleeWeapon.cs | 15 +- .../Items/Components/Holdable/Pickable.cs | 4 +- .../Items/Components/Holdable/RangedWeapon.cs | 59 +- .../Items/Components/Holdable/RepairTool.cs | 28 +- .../Items/Components/Holdable/Throwable.cs | 6 +- .../Items/Components/ItemComponent.cs | 60 +- .../Items/Components/ItemContainer.cs | 4 +- .../Components/Machines/Deconstructor.cs | 36 +- .../Items/Components/Machines/Fabricator.cs | 58 +- .../Components/Machines/OxygenGenerator.cs | 16 +- .../Items/Components/Machines/Sonar.cs | 31 +- .../Items/Components/Machines/Steering.cs | 5 + .../Items/Components/Projectile.cs | 12 +- .../Items/Components/Repairable.cs | 79 +- .../SharedSource/Items/Components/Rope.cs | 240 +- .../Items/Components/Signal/CircuitBox.cs | 113 +- .../Items/Components/Signal/Wire.cs | 20 +- .../Items/Components/TriggerComponent.cs | 20 +- .../SharedSource/Items/Components/Wearable.cs | 11 + .../SharedSource/Items/ContainerTagPrefab.cs | 149 ++ .../SharedSource/Items/Item.cs | 157 +- .../Map/Creatures/BallastFloraBehavior.cs | 4 +- .../SharedSource/Map/Entity.cs | 3 + .../SharedSource/Map/Explosion.cs | 44 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 7 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 6 +- .../SharedSource/Map/HullEventData.cs | 8 +- .../SharedSource/Map/ItemAssemblyPrefab.cs | 33 +- .../Map/Levels/DestructibleLevelWall.cs | 17 +- .../SharedSource/Map/Levels/Level.cs | 441 ++-- .../SharedSource/Map/Levels/LevelData.cs | 4 + .../Map/Levels/LevelObjects/LevelTrigger.cs | 24 +- .../SharedSource/Map/Map/Location.cs | 62 +- .../SharedSource/Map/Map/LocationType.cs | 13 + .../SharedSource/Map/Map/Map.cs | 47 +- .../SharedSource/Map/MapEntity.cs | 17 +- .../Map/Outposts/OutpostGenerationParams.cs | 7 + .../Map/Outposts/OutpostGenerator.cs | 12 +- .../Map/Outposts/OutpostModuleInfo.cs | 13 +- .../SharedSource/Map/Structure.cs | 118 +- .../SharedSource/Map/Submarine.cs | 129 +- .../SharedSource/Map/SubmarineBody.cs | 67 +- .../SharedSource/Map/SubmarineInfo.cs | 21 +- .../SharedSource/Map/WayPoint.cs | 6 +- .../SharedSource/Map/WreckConverter.cs | 193 ++ .../SharedSource/Networking/ChatMessage.cs | 6 + .../Networking/INetSerializableStruct.cs | 10 +- .../SharedSource/Networking/NetworkMember.cs | 10 +- .../Networking/OrderChatMessage.cs | 4 +- .../Primitives/NetworkPeerStructs.cs | 1 + .../SharedSource/Networking/RespawnManager.cs | 34 +- .../SharedSource/Networking/ServerSettings.cs | 63 +- .../SharedSource/Physics/Physics.cs | 5 +- .../SharedSource/Physics/PhysicsBody.cs | 23 + .../SharedSource/ProcGen/VoronoiElements.cs | 2 +- .../SharedSource/Screens/NetLobbyScreen.cs | 20 +- .../SerializableProperty.cs | 8 +- .../Serialization/XMLExtensions.cs | 6 + .../SharedSource/Settings/GameSettings.cs | 23 +- .../StatusEffects/PropertyConditional.cs | 102 +- .../StatusEffects/StatusEffect.cs | 710 +++--- .../BarotraumaShared/SharedSource/Tags.cs | 14 + .../SharedSource/Text/TextManager.cs | 30 +- .../SharedSource/Traitors/TraitorEvent.cs | 5 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 6 + .../SharedSource/Utils/NetLimitedString.cs | 22 + .../SharedSource/Utils/ToolBox.cs | 3 + Barotrauma/BarotraumaShared/changelog.txt | 244 ++ Barotrauma/BarotraumaShared/hintmanager.xml | 4 + .../FabricatorQualityRollTests.cs | 2 +- .../BarotraumaTest/GenericToolBoxTests.cs | 9 +- .../BarotraumaCore/Utils/ReflectionUtils.cs | 51 +- .../Utils/TypePreviouslyKnownAs.cs | 24 + .../Input/KeyboardUtil.SDL.cs | 8 + 397 files changed, 15250 insertions(+), 6479 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabelNode.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GUI/GUISelectionCarousel.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs rename Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/{AbilityConditionItemInSubmarine.cs => AbilityConditionInSubmarine.cs} (57%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUpgradeSubmarine.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityWarStories.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxLabelNode.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContainerTagFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/LayerAction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/ContainerTagPrefab.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/WreckConverter.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/NetLimitedString.cs create mode 100644 Libraries/BarotraumaLibs/BarotraumaCore/Utils/TypePreviouslyKnownAs.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 9ec4022c3..1858f969c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -277,7 +277,7 @@ namespace Barotrauma velocity = Vector2.Lerp(velocity, moveInput, deltaTime * 10.0f); moveCam = velocity * moveSpeed * deltaTime * FreeCamMoveSpeed * 60.0f; - if (Screen.Selected == GameMain.GameScreen && (followSub ?? FollowSub)) + if (Screen.Selected == GameMain.GameScreen && (followSub ?? FollowSub) && GameMain.Instance is not { Paused: true }) { var closestSub = Submarine.FindClosest(WorldViewCenter); if (closestSub != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index c09d92817..e6b91667c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -109,7 +109,7 @@ namespace Barotrauma } float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); - float errorTolerance = character.CanMove && !character.IsRagdolled ? 0.01f : 0.2f; + float errorTolerance = character.CanMove && (!character.IsRagdolled || character.AnimController.IsHangingWithRope) ? 0.01f : 0.2f; if (distSqrd > errorTolerance) { if (distSqrd > 10.0f || !character.CanMove) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 8df8deea0..3b5ff2c41 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -1,3 +1,4 @@ +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using Barotrauma.Particles; @@ -9,7 +10,6 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -42,6 +42,8 @@ namespace Barotrauma private set; } = true; + public bool ShowInteractionLabels { get; private set; } + //the Character that the player is currently controlling private static Character controlled; @@ -230,14 +232,51 @@ namespace Barotrauma } } - private float pressureEffectTimer; - private readonly List activeObjectiveEntities = new List(); public IEnumerable ActiveObjectiveEntities { get { return activeObjectiveEntities; } } + private static readonly List speechBubbles = new List(); + + private SpeechBubble textlessSpeechBubble; + + sealed class SpeechBubble + { + public float LifeTime; + public Vector2 PrevPosition; + public Vector2 Position; + public Vector2 DrawPosition; + public float MoveUpAmount; + public readonly string Text; + public readonly Character Character; + public readonly Submarine Submarine; + public readonly Vector2 TextSize; + + public Color Color; + public bool Moving; + + public SpeechBubble(Character character, float lifeTime, Color color, string text = "") + { + Text = ToolBox.WrapText(text, GUI.IntScale(300), GUIStyle.SmallFont.GetFontForStr(text)); + TextSize = GUIStyle.SmallFont.MeasureString(Text); + + Character = character; + Position = GetDesiredPosition(); + Submarine = character.Submarine; + LifeTime = lifeTime; + Color = color; + } + + public Vector2 GetDesiredPosition() + { + return Character.Position + Vector2.UnitY * 100; + } + } + + private float pressureEffectTimer; + partial void InitProjSpecific(ContentXElement mainElement) { soundTimer = Rand.Range(0.0f, Params.SoundInterval); @@ -272,6 +311,9 @@ namespace Barotrauma } } + private readonly List previousInteractablesInRange = new(); + private readonly List interactablesInRange = new(); + private bool wasFiring; /// @@ -279,6 +321,7 @@ namespace Barotrauma /// public void ControlLocalPlayer(float deltaTime, Camera cam, bool moveCam = true) { + if (DisableControls || GUI.InputBlockingMenuOpen) { foreach (Key key in keys) @@ -316,6 +359,13 @@ namespace Barotrauma } } + ShowInteractionLabels = keys[(int)InputType.ShowInteractionLabels].Held; + + if (ShowInteractionLabels) + { + focusedItem = InteractionLabelManager.HoveredItem; + } + //if we were firing (= pressing the aim and shoot keys at the same time) //and the fire key is the same as Select or Use, reset the key to prevent accidentally selecting/using items if (wasFiring && !keys[(int)InputType.Shoot].Held) @@ -549,19 +599,61 @@ namespace Barotrauma if (Lights.LightManager.ViewTarget == this) { Lights.LightManager.ViewTarget = null; } } + private void UpdateInteractablesInRange() + { + // keep two lists to detect changes to the current state of interactables in range + previousInteractablesInRange.Clear(); + previousInteractablesInRange.AddRange(interactablesInRange); + + interactablesInRange.Clear(); + + //use the list of visible entities if it exists + var entityList = Submarine.VisibleEntities ?? Item.ItemList; + + foreach (MapEntity entity in entityList) + { + if (entity is not Item item) { continue; } + + if (item.body != null && !item.body.Enabled) { continue; } + + if (item.ParentInventory != null) { continue; } + + if (item.Prefab.RequireCampaignInteract && + item.CampaignInteractionType == CampaignMode.InteractionType.None) + { + continue; + } + + if (Screen.Selected is SubEditorScreen { WiringMode: true } && + item.GetComponent() == null) + { + continue; + } + + if (CanInteractWith(item)) + { + interactablesInRange.Add(item); + } + } + + if (!interactablesInRange.SequenceEqual(previousInteractablesInRange)) + { + InteractionLabelManager.RefreshInteractablesInRange(interactablesInRange); + } + } private readonly List debugInteractablesInRange = new List(); private readonly List debugInteractablesAtCursor = new List(); private readonly List<(Item item, float dist)> debugInteractablesNearCursor = new List<(Item item, float dist)>(); /// - /// Finds the front (lowest depth) interactable item at a position. "Interactable" in this case means that the character can "reach" the item. + /// Finds the front (lowest depth) interactable item at a position. "Interactable" in this case means that the character can "reach" the item. /// - /// The Character who is looking for the interactable item, only items that are close enough to this character are returned - /// The item at the simPosition, with the lowest depth, is returned - /// If this is true and an item cannot be found at simPosition then a nearest item will be returned if possible - /// If a hull is specified, only items within that hull are returned - public Item FindItemAtPosition(Vector2 simPosition, float aimAssistModifier = 0.0f, Item[] ignoredItems = null) + /// Item collection to look in + /// sim position for distance comparison (such as mouse position) + /// aim assist modifier + /// + public Item FindClosestItem(List itemCollection, Vector2 simPosition, float aimAssistModifier = 0.0f) { if (Submarine != null) { @@ -580,24 +672,11 @@ namespace Barotrauma float aimAssistAmount = SelectedItem == null ? 100.0f * aimAssistModifier : 1.0f; Vector2 displayPosition = ConvertUnits.ToDisplayUnits(simPosition); - - //use the list of visible entities if it exists - var entityList = Submarine.VisibleEntities ?? Item.ItemList; - + Item closestItem = null; float closestItemDistance = Math.Max(aimAssistAmount, 2.0f); - foreach (MapEntity entity in entityList) + foreach (var item in itemCollection) { - if (entity is not Item item) - { - continue; - } - if (item.body != null && !item.body.Enabled) { continue; } - if (item.ParentInventory != null) { continue; } - if (ignoredItems != null && ignoredItems.Contains(item)) { continue; } - if (item.Prefab.RequireCampaignInteract && item.CampaignInteractionType == CampaignMode.InteractionType.None) { continue; } - if (Screen.Selected is SubEditorScreen editor && editor.WiringMode && item.GetComponent() == null) { continue; } - if (draggingItemToWorld) { if (item.OwnInventory == null || @@ -644,7 +723,7 @@ namespace Barotrauma distanceToItem = 2.0f + Vector2.Distance(rectIntersectionPoint, displayPosition); } } - + if (distanceToItem > closestItemDistance) { continue; } if (!CanInteractWith(item)) { continue; } @@ -661,7 +740,7 @@ namespace Barotrauma Character closestCharacter = null; maxDist = ConvertUnits.ToSimUnits(maxDist); - float closestDist = maxDist * maxDist; + float closestDist = maxDist; foreach (Character c in CharacterList) { if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; } @@ -704,6 +783,12 @@ namespace Barotrauma } guiMessages.RemoveAll(m => m.Timer >= m.Lifetime); + if (textlessSpeechBubble != null) + { + textlessSpeechBubble.LifeTime -= deltaTime; + if (textlessSpeechBubble.LifeTime <= 0) { textlessSpeechBubble = null; } + } + if (!enabled) { return; } if (!IsIncapacitated) @@ -890,13 +975,6 @@ namespace Barotrauma pos.Y = -pos.Y; - if (speechBubbleTimer > 0.0f) - { - GUIStyle.SpeechBubbleIcon.Value.Sprite.Draw(spriteBatch, pos - Vector2.UnitY * 5, - speechBubbleColor * Math.Min(speechBubbleTimer, 1.0f), 0.0f, - Math.Min(speechBubbleTimer, 1.0f)); - } - if (this == controlled) { if (DebugDrawInteract) @@ -921,113 +999,232 @@ namespace Barotrauma ToolBox.GradientLerp(dist, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green), width: 2); } } - return; } - - float hoverRange = 300.0f; - float fadeOutRange = 200.0f; - float cursorDist = Vector2.Distance(WorldPosition, cam.ScreenToWorld(PlayerInput.MousePosition)); - float hudInfoAlpha = - CampaignInteractionType == CampaignMode.InteractionType.None ? - MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) : - 1.0f; - - if (!GUI.DisableCharacterNames && hudInfoVisible && - (controlled == null || this != controlled.FocusedCharacter || IsPet) && cam.Zoom > 0.4f) + else { - if (info != null) + + float hoverRange = 300.0f; + float fadeOutRange = 200.0f; + float cursorDist = Vector2.Distance(WorldPosition, cam.ScreenToWorld(PlayerInput.MousePosition)); + float hudInfoAlpha = + CampaignInteractionType == CampaignMode.InteractionType.None ? + MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) : + 1.0f; + + if (!GUI.DisableCharacterNames && hudInfoVisible && + (controlled == null || this != controlled.FocusedCharacter || IsPet) && cam.Zoom > 0.4f) { - LocalizedString name = Info.DisplayName; - if (controlled == null && name != Info.Name) - { - name += " " + TextManager.Get("Disguised"); - } - else if (Info.Title != null && TeamID != CharacterTeamType.Team1) + if (info != null) { - name += '\n' + Info.Title; + LocalizedString name = Info.DisplayName; + if (controlled == null && name != Info.Name) + { + name += " " + TextManager.Get("Disguised"); + } + else if (Info.Title != null && TeamID != CharacterTeamType.Team1) + { + name += '\n' + Info.Title; + } + + Vector2 nameSize = GUIStyle.Font.MeasureString(name); + Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; + Color nameColor = GetNameColor(); + + Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height); + namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y; + namePos *= screenSize / viewportSize; + namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y); + namePos *= viewportSize / screenSize; + namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y; + + if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) + { + var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); + if (iconStyle != null) + { + Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; + Vector2 iconPos = headPos; + iconPos.Y = -iconPos.Y; + nameColor = iconStyle.Color; + var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); + float iconScale = (30.0f / icon.Sprite.size.X / cam.Zoom) * GUI.Scale; + icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); + } + } + + GUIStyle.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f); + GUIStyle.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); + if (GameMain.DebugDraw) + { + GUIStyle.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); + } } - Vector2 nameSize = GUIStyle.Font.MeasureString(name); - Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; - Color nameColor = GetNameColor(); - - Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height); - namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y; - namePos *= screenSize / viewportSize; - namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y); - namePos *= viewportSize / screenSize; - namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y; - - if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) + var petBehavior = (AIController as EnemyAIController)?.PetBehavior; + if (petBehavior != null && !IsDead && !IsUnconscious) { - var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); + var petStatus = petBehavior.GetCurrentStatusIndicatorType(); + var iconStyle = GUIStyle.GetComponentStyle("PetIcon." + petStatus); if (iconStyle != null) { Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; Vector2 iconPos = headPos; iconPos.Y = -iconPos.Y; - nameColor = iconStyle.Color; var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); - float iconScale = (30.0f / icon.Sprite.size.X / cam.Zoom) * GUI.Scale; + float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); } } + } - GUIStyle.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f); - GUIStyle.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); - if (GameMain.DebugDraw) + if (IsDead) { return; } + + var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; + if (healthBarMode != EnemyHealthBarMode.ShowAll) + { + if (Controlled == null) { - GUIStyle.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); + if (!IsOnPlayerTeam) { return; } + } + else + { + if (!HumanAIController.IsFriendly(Controlled, this) || + (AIController is HumanAIController humanAi && humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective && HumanAIController.IsFriendly(Controlled, combatObjective.Enemy))) + { + return; + } } } - - var petBehavior = (AIController as EnemyAIController)?.PetBehavior; - if (petBehavior != null && !IsDead && !IsUnconscious) + + if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) { - var petStatus = petBehavior.GetCurrentStatusIndicatorType(); - var iconStyle = GUIStyle.GetComponentStyle("PetIcon." + petStatus); - if (iconStyle != null) - { - Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; - Vector2 iconPos = headPos; - iconPos.Y = -iconPos.Y; - var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); - float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; - icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); - } + hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f)); + + Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y); + GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f), + CharacterHealth.DisplayedVitality / MaxVitality, + Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, + new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha); } } - if (IsDead) { return; } - - var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; - if (healthBarMode != EnemyHealthBarMode.ShowAll) + if (textlessSpeechBubble != null) { - if (Controlled == null) + Vector2 iconPos = pos - Vector2.UnitY * 5; + + GUIStyle.SpeechBubbleIcon.Value.Sprite.Draw(spriteBatch, iconPos, + textlessSpeechBubble.Color * Math.Min(textlessSpeechBubble.LifeTime, 1.0f), 0.0f, + Math.Min(textlessSpeechBubble.LifeTime, 1.0f)); + } + } + + public void ShowSpeechBubble(Color color, string text) + { + if (!GameSettings.CurrentConfig.ChatSpeechBubbles) + { + ShowTextlessSpeechBubble(1.0f, color); + return; + } + float duration = MathHelper.Lerp(1.0f, 8.0f, Math.Min(text.Length / 100.0f, 1.0f)); + speechBubbles.Add(new SpeechBubble(this, duration, color, text)); + textlessSpeechBubble = null; + } + + public void ShowTextlessSpeechBubble(float duration, Color color) + { + if (speechBubbles.Any(sb => sb.Character == this)) { return; } + if (textlessSpeechBubble == null) + { + textlessSpeechBubble = new SpeechBubble(this, duration, color); + } + else + { + textlessSpeechBubble.Color = color; + textlessSpeechBubble.LifeTime = Math.Max(textlessSpeechBubble.LifeTime, duration); + } + } + + public static void DrawSpeechBubbles(SpriteBatch spriteBatch, Camera cam) + { + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform); + foreach (var bubble in speechBubbles) + { + Vector2 iconPos = Timing.Interpolate(bubble.PrevPosition, bubble.Position); + iconPos += Vector2.UnitY * bubble.MoveUpAmount; + if (bubble.Submarine != null) { - if (!IsOnPlayerTeam) { return; } + iconPos += bubble.Submarine.DrawPosition; } - else + + float alpha = 1.0f; + float mouseDist = Vector2.Distance(cam.WorldToScreen(iconPos), PlayerInput.MousePosition); + //treat the size of the bubble from corner to corner as the + float textSize = bubble.TextSize.Length(); + if (mouseDist < textSize) { - if (!HumanAIController.IsFriendly(Controlled, this) || - (AIController is HumanAIController humanAi && humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective && HumanAIController.IsFriendly(Controlled, combatObjective.Enemy))) - { - return; + alpha *= Math.Max(mouseDist / textSize, 0.5f); + } + + iconPos.Y = -iconPos.Y; + if (GUIStyle.SpeechBubbleIconSliced.Value is { } speechBubbleIconSliced) + { + Vector2 bubbleSize = bubble.TextSize + Vector2.One * GUI.IntScale(15); + speechBubbleIconSliced.Draw(spriteBatch, new RectangleF(iconPos - bubbleSize / 2, bubbleSize), bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha); + } + GUI.DrawString(spriteBatch, iconPos - bubble.TextSize / 2, bubble.Text, bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha, font: GUIStyle.SmallFont); + } + spriteBatch.End(); + } + + static partial void UpdateSpeechBubbles(float deltaTime) + { + for (int i = speechBubbles.Count - 1; i >= 0; i--) + { + var bubble = speechBubbles[i]; + bubble.LifeTime -= deltaTime; + if (bubble.LifeTime <= 0 || bubble.Character is { Removed: true }) + { + speechBubbles.RemoveAt(i); + continue; + } + + bubble.PrevPosition = bubble.Position; + + Vector2 desiredPos = bubble.GetDesiredPosition(); + Vector2 diff = desiredPos - bubble.Position; + float dist = diff.Length(); + //how far the bubble needs to be from the desired position to start moving + const float MoveThreshold = 100.0f; + const float MaxSpeed = 1000.0f; + if (dist < 1) + { + bubble.Moving = false; + } + else if (dist > MoveThreshold || bubble.Moving) + { + Vector2 moveAmount = diff / dist * MathHelper.Clamp(dist * 5, 0, MaxSpeed) * deltaTime; + //slower vertical movement (don't want to interfere too much with the bubbles floating up + //and the overlap prevention which works vertically) + moveAmount.Y *= 0.1f; + bubble.Position += moveAmount; + bubble.Moving = true; + } + + bubble.MoveUpAmount += deltaTime * 5.0f; + //go through the newer bubbles, move this one out of the way if one is overlapping + for (int j = i + 1; j < speechBubbles.Count; j++) + { + var otherBubble = speechBubbles[j]; + { + if (Math.Abs(bubble.Position.X - otherBubble.Position.X) < (bubble.TextSize.X + otherBubble.TextSize.X) / 2 && + Math.Abs(bubble.Position.Y - otherBubble.Position.Y) < (bubble.TextSize.Y + otherBubble.TextSize.Y) / 2 + 10) + { + bubble.Position += Vector2.UnitY * deltaTime * 50.0f; + } } } } - - if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) - { - hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f)); - - Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y); - GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f), - CharacterHealth.DisplayedVitality / MaxVitality, - Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, - new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha); - } } public Color GetNameColor() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 348cdd874..1bee8cd84 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -207,7 +207,7 @@ namespace Barotrauma { cachedHudTexts.Clear(); } - Identifier key = (textTag + keyBind).ToIdentifier(); + Identifier key = (textTag + keyBind + GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).ToIdentifier(); if (cachedHudTexts.TryGetValue(key, out LocalizedString text)) { return text; } text = TextManager.GetWithVariable(textTag, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).Value; cachedHudTexts.Add(key, text); @@ -246,6 +246,7 @@ namespace Barotrauma public static void Update(float deltaTime, Character character, Camera cam) { + UpdateBossProgressBars(deltaTime); if (GUI.DisableHUD) @@ -256,6 +257,11 @@ namespace Barotrauma } return; } + + if (character.ShowInteractionLabels && character.ViewTarget == null) + { + InteractionLabelManager.Update(character, cam); + } if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen) { @@ -375,7 +381,7 @@ namespace Barotrauma static bool DrawIcon(Order o) => o != null && - (!(o.TargetEntity is Item i) || + (o.TargetEntity is not Item i || o.DrawIconWhenContained || i.GetRootInventoryOwner() == i); } @@ -405,90 +411,115 @@ namespace Barotrauma if (!brokenItem.IsInteractable(character)) { continue; } float alpha = GetDistanceBasedIconAlpha(brokenItem); if (alpha <= 0.0f) { continue; } - GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite, + GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite, Color.Lerp(GUIStyle.Red, GUIStyle.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); } + if (OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder)) + { + foreach (Item deconstructItem in Item.DeconstructItems) + { + if (deconstructItem.ParentInventory != null) { continue; } + if (deconstructItem.OrderedToBeIgnored) { continue; } + if (deconstructItem.Submarine != character.Submarine || !deconstructItem.IsInteractable(character)) { continue; } + float alpha = GetDistanceBasedIconAlpha(deconstructItem, maxDistance: 450) * 0.7f; + if (alpha <= 0.0f) { continue; } + GUI.DrawIndicator(spriteBatch, deconstructItem.DrawPosition, cam, 100.0f, deconstructOrder.SymbolSprite, + GUIStyle.Red, scaleMultiplier: 0.5f, overrideAlpha: alpha); + } + } + float GetDistanceBasedIconAlpha(ISpatialEntity target, float maxDistance = 1000.0f) { float dist = Vector2.Distance(character.WorldPosition, target.WorldPosition); return Math.Min((maxDistance - dist) / maxDistance * 2.0f, 1.0f); } - + if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen && (!character.IsKeyDown(InputType.Aim) || character.HeldItems.None(it => it?.GetComponent() != null))) { - if (character.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected) + if (!character.ShowInteractionLabels) { - DrawCharacterHoverTexts(spriteBatch, cam, character); - } - - if (character.FocusedItem != null) - { - if (focusedItem != character.FocusedItem) + if (character.FocusedCharacter is { CanBeSelected: true }) { - focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer); - RecreateHudTexts = true; + DrawCharacterHoverTexts(spriteBatch, cam, character); } - focusedItem = character.FocusedItem; - } - - if (focusedItem != null && focusedItemOverlayTimer > ItemOverlayDelay) - { - Vector2 circlePos = cam.WorldToScreen(focusedItem.DrawPosition); - float circleSize = Math.Max(focusedItem.Rect.Width, focusedItem.Rect.Height) * 1.5f; - circleSize = MathHelper.Clamp(circleSize, 45.0f, 100.0f) * Math.Min((focusedItemOverlayTimer - 1.0f) * 5.0f, 1.0f); - if (circleSize > 0.0f) + + if (character.FocusedItem != null) { - Vector2 scale = new Vector2(circleSize / GUIStyle.FocusIndicator.FrameSize.X); - GUIStyle.FocusIndicator.Draw(spriteBatch, - (int)((focusedItemOverlayTimer - 1.0f) * GUIStyle.FocusIndicator.FrameCount * 3.0f), - circlePos, - Color.LightBlue * 0.3f, - origin: GUIStyle.FocusIndicator.FrameSize.ToVector2() / 2, - rotate: (float)Timing.TotalTime, - scale: scale); - } - - if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) - { - bool hudTextsContextual = PlayerInput.IsShiftDown(); - if (RecreateHudTexts || lastHudTextsContextual != hudTextsContextual) + if (focusedItem != character.FocusedItem) { + focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer); RecreateHudTexts = true; - lastHudTextsContextual = hudTextsContextual; } - var hudTexts = focusedItem.GetHUDTexts(character, RecreateHudTexts); - RecreateHudTexts = false; - - int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); - - Vector2 textSize = GUIStyle.Font.MeasureString(hudTexts.First().Text); - Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString(hudTexts.First().Text); - - Vector2 startPos = cam.WorldToScreen(focusedItem.DrawPosition); - startPos.Y -= (hudTexts.Count + 1) * textSize.Y; - if (focusedItem.Sprite != null) + focusedItem = character.FocusedItem; + } + + if (focusedItem != null && focusedItemOverlayTimer > ItemOverlayDelay) + { + Vector2 circlePos = cam.WorldToScreen(focusedItem.DrawPosition); + float circleSize = Math.Max(focusedItem.Rect.Width, focusedItem.Rect.Height) * 1.5f; + circleSize = MathHelper.Clamp(circleSize, 45.0f, 100.0f) * Math.Min((focusedItemOverlayTimer - 1.0f) * 5.0f, 1.0f); + if (circleSize > 0.0f) { - startPos.X += (int)(circleSize * 0.4f * dir); - startPos.Y -= (int)(circleSize * 0.4f); + Vector2 scale = new Vector2(circleSize / GUIStyle.FocusIndicator.FrameSize.X); + GUIStyle.FocusIndicator.Draw(spriteBatch, + (int)((focusedItemOverlayTimer - 1.0f) * GUIStyle.FocusIndicator.FrameCount * 3.0f), + circlePos, + Color.LightBlue * 0.3f, + origin: GUIStyle.FocusIndicator.FrameSize.ToVector2() / 2, + rotate: (float)Timing.TotalTime, + scale: scale); } - Vector2 textPos = startPos; - if (dir == -1) { textPos.X -= largeTextSize.X; } - - float alpha = MathHelper.Clamp((focusedItemOverlayTimer - ItemOverlayDelay) * 2.0f, 0.0f, 1.0f); - - GUI.DrawString(spriteBatch, textPos, hudTexts.First().Text, hudTexts.First().Color * alpha, Color.Black * alpha * 0.7f, 2, font: GUIStyle.SubHeadingFont, ForceUpperCase.No); - startPos.X += dir * 10.0f * GUI.Scale; - textPos.X += dir * 10.0f * GUI.Scale; - textPos.Y += largeTextSize.Y; - foreach (ColoredText coloredText in hudTexts.Skip(1)) + if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) { - if (dir == -1) textPos.X = (int)(startPos.X - GUIStyle.SmallFont.MeasureString(coloredText.Text).X); - GUI.DrawString(spriteBatch, textPos, coloredText.Text, coloredText.Color * alpha, Color.Black * alpha * 0.7f, 2, GUIStyle.SmallFont); - textPos.Y += textSize.Y; - } - } + bool hudTextsContextual = PlayerInput.KeyDown(InputType.ContextualCommand); + if (RecreateHudTexts || lastHudTextsContextual != hudTextsContextual) + { + RecreateHudTexts = true; + lastHudTextsContextual = hudTextsContextual; + } + var hudTexts = focusedItem.GetHUDTexts(character, RecreateHudTexts); + RecreateHudTexts = false; + + int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); + + Vector2 textSize = GUIStyle.Font.MeasureString(hudTexts.First().Text); + Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString(hudTexts.First().Text); + + Vector2 startPos = cam.WorldToScreen(focusedItem.DrawPosition); + startPos.Y -= (hudTexts.Count + 1) * textSize.Y; + if (focusedItem.Sprite != null) + { + startPos.X += (int)(circleSize * 0.4f * dir); + startPos.Y -= (int)(circleSize * 0.4f); + } + + Vector2 textPos = startPos; + if (dir == -1) { textPos.X -= largeTextSize.X; } + + float alpha = MathHelper.Clamp((focusedItemOverlayTimer - ItemOverlayDelay) * 2.0f, 0.0f, 1.0f); + + GUI.DrawString(spriteBatch, textPos, hudTexts.First().Text, hudTexts.First().Color * alpha, Color.Black * alpha * 0.7f, 2, font: GUIStyle.SubHeadingFont, ForceUpperCase.No); + startPos.X += dir * 10.0f * GUI.Scale; + textPos.X += dir * 10.0f * GUI.Scale; + textPos.Y += largeTextSize.Y; + foreach (ColoredText coloredText in hudTexts.Skip(1)) + { + if (dir == -1) + { + textPos.X = (int)(startPos.X - GUIStyle.SmallFont.MeasureString(coloredText.Text).X); + } + GUI.DrawString(spriteBatch, textPos, coloredText.Text, coloredText.Color * alpha, Color.Black * alpha * 0.7f, 2, GUIStyle.SmallFont); + textPos.Y += textSize.Y; + } + } + } + } + + if (character.ShowInteractionLabels && character.ViewTarget == null) + { + InteractionLabelManager.DrawLabels(spriteBatch, cam, character); } foreach (HUDProgressBar progressBar in character.HUDProgressBars.Values) @@ -673,6 +704,8 @@ namespace Barotrauma } } + + public static bool MouseOnCharacterPortrait() { if (Character.Controlled == null) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 1ce98e9b5..0b41bcf3d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Networking; using System; using System.Linq; @@ -46,6 +46,7 @@ namespace Barotrauma ContentPath tintMaskPath = maskElement.GetAttributeContentPath("texture"); if (!tintMaskPath.IsNullOrEmpty()) { + VerifySpriteTagsLoaded(); tintMask = new Sprite(maskElement, file: Limb.GetSpritePath(tintMaskPath, this)); tintHighlightThreshold = maskElement.GetAttributeFloat("highlightthreshold", 0.6f); tintHighlightMultiplier = maskElement.GetAttributeFloat("highlightmultiplier", 0.8f); @@ -61,7 +62,7 @@ namespace Barotrauma //Stretch = true }; - var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.322f), paddedFrame.RectTransform), isHorizontal: true); + var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), paddedFrame.RectTransform), isHorizontal: true); new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform), onDraw: (sb, component) => DrawInfoFrameCharacterIcon(sb, component.Rect)); @@ -185,7 +186,7 @@ namespace Barotrauma private void DrawInfoFrameCharacterIcon(SpriteBatch sb, Rectangle componentRect) { - if (_headSprite == null) { return; } + if (HeadSprite == null) { return; } Vector2 targetAreaSize = componentRect.Size.ToVector2(); float scale = Math.Min(targetAreaSize.X / _headSprite.size.X, targetAreaSize.Y / _headSprite.size.Y); DrawIcon(sb, componentRect.Location.ToVector2() + _headSprite.size / 2 * scale, targetAreaSize); @@ -537,8 +538,7 @@ namespace Barotrauma Color skinColor = inc.ReadColorR8G8B8(); Color hairColor = inc.ReadColorR8G8B8(); Color facialHairColor = inc.ReadColorR8G8B8(); - - string ragdollFile = inc.ReadString(); + Identifier npcId = inc.ReadIdentifier(); Identifier factionId = inc.ReadIdentifier(); @@ -560,18 +560,15 @@ namespace Barotrauma throw new Exception($"Error while reading {nameof(CharacterInfo)} received from the server: could not find a job prefab with the identifier \"{jobIdentifier}\"."); } byte skillCount = inc.ReadByte(); - List jobSkills = jobPrefab?.Skills.OrderBy(s => s.Identifier).ToList(); for (int i = 0; i < skillCount; i++) { + Identifier skillIdentifier = inc.ReadIdentifier(); float skillLevel = inc.ReadSingle(); - if (jobSkills != null && i < jobSkills.Count) - { - skillLevels.Add(jobSkills[i].Identifier, skillLevel); - } + skillLevels.Add(skillIdentifier, skillLevel); } } - CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant, npcIdentifier: npcId) + CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, variant, npcIdentifier: npcId) { ID = infoID, MinReputationToHire = (factionId, minReputationToHire) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index de19d3561..08f944f2c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -1,5 +1,6 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Immutable; @@ -532,6 +533,47 @@ namespace Barotrauma bool removeOnDeath = msg.ReadBoolean(); info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true); } + break; + case EventType.LatchOntoTarget: + bool attached = msg.ReadBoolean(); + if (attached) + { + Vector2 characterSimPos = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + Vector2 attachSurfaceNormal = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + Vector2 attachPos = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + int attachWallIndex = msg.ReadInt32(); + UInt16 attachTargetId = msg.ReadUInt16(); + + if (AIController is EnemyAIController { LatchOntoAI: { } latchOntoAi }) + { + var attachTargetEntity = FindEntityByID(attachTargetId); + switch (attachTargetEntity) + { + case Character attachTargetCharacter: + latchOntoAi.SetAttachTarget(attachTargetCharacter); + break; + case Structure attachTargetStructure: + latchOntoAi.SetAttachTarget(attachTargetStructure, attachPos, attachSurfaceNormal); + break; + default: + var allLevelWalls = Level.Loaded.GetAllCells(); + if (attachWallIndex >= 0 && attachWallIndex <= allLevelWalls.Count) + { + latchOntoAi.SetAttachTarget(allLevelWalls[attachWallIndex]); + } + break; + } + latchOntoAi.AttachToBody(attachPos, attachSurfaceNormal, characterSimPos); + } + } + else + { + if (AIController is EnemyAIController { LatchOntoAI: { } latchOntoAi }) + { + latchOntoAi.DeattachFromBody(reset: false); + } + } + break; } msg.ReadPadBits(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index b0a721fc3..e9798be70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -858,8 +858,8 @@ namespace Barotrauma foreach (GUIComponent component in recommendedTreatmentContainer.Content.Children) { var treatmentButton = component.GetChild(); - if (!(treatmentButton?.UserData is ItemPrefab itemPrefab)) { continue; } - var matchingItem = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); + if (treatmentButton?.UserData is not ItemPrefab itemPrefab) { continue; } + var matchingItem = AIObjectiveRescue.FindMedicalItem(Character.Controlled.Inventory, itemPrefab.Identifier); treatmentButton.Enabled = matchingItem != null; if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover) { @@ -1387,7 +1387,6 @@ namespace Barotrauma //float = suitability Dictionary treatmentSuitability = new Dictionary(); GetSuitableTreatments(treatmentSuitability, - normalize: true, user: Character.Controlled, ignoreHiddenAfflictions: true, limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex)); @@ -1421,9 +1420,12 @@ namespace Barotrauma int count = 0; foreach (KeyValuePair treatment in treatmentSuitabilities) { + //don't list negative treatments + if (treatment.Value < 0) { continue; } + count++; if (count > 5) { break; } - if (!(MapEntityPrefab.Find(name: null, identifier: treatment.Key, showErrorMessages: false) is ItemPrefab item)) { continue; } + if (MapEntityPrefab.FindByIdentifier(treatment.Key) is not ItemPrefab item) { continue; } var itemSlot = new GUIFrame(new RectTransform(new Vector2(1.0f / 6.0f, 1.0f), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopLeft), style: null) @@ -1439,7 +1441,7 @@ namespace Barotrauma OnClicked = (btn, userdata) => { if (userdata is not ItemPrefab itemPrefab) { return false; } - var item = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); + var item = AIObjectiveRescue.FindMedicalItem(Character.Controlled.Inventory, it => it.Prefab == itemPrefab); if (item == null) { return false; } Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); item.ApplyTreatment(Character.Controlled, Character, targetLimb); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs new file mode 100644 index 000000000..73e733677 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs @@ -0,0 +1,328 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System.Collections.Generic; +using Barotrauma.Items.Components; + +namespace Barotrauma; + +public static class InteractionLabelManager +{ + + private class LabelData + { + private readonly Camera drawCamera; + public readonly Item Item; + + public RectangleF TextRect { get; set; } + + public readonly Vector2 OriginalItemPosition; + + public bool OverlapPreventionDone; + + public LabelData(Item item, RectangleF textRect, Camera drawCamera) + { + Item = item; + TextRect = textRect; + OriginalItemPosition = item.Position; + this.drawCamera = drawCamera; + } + + public RectangleF GetScreenDrawRect(Camera cam) + { + float scale = cam.Zoom; + RectangleF screenDrawRect = TextRect; + screenDrawRect.Location = drawCamera + .WorldToScreen(screenDrawRect.Location + (Item.Submarine?.DrawPosition ?? Vector2.Zero)); + + return new RectangleF( + screenDrawRect.X, + screenDrawRect.Y, + screenDrawRect.Width * scale, + screenDrawRect.Height * scale); + } + + public Vector2 GetInteractableDrawPositionScreen() + { + return drawCamera.WorldToScreen(Item.DrawPosition); + } + } + + private static readonly List labels = new(); + + private const int TextBoxMarginPx = 4; + + /// + /// Multiplier on the scale of the labels. Ad-hoc formula: since the zoom affects the size of the labels, + /// and high resolutions are more zoomed in to keep the view range the same, let's scale down the labels on large resolutions to compensate. + /// + private static float LabelScale => 1.0f / GUI.Scale; + + private static InteractionLabelDisplayMode displayMode; + + private static int graphicsWidth, graphicsHeight; + + private static bool shouldRecalculate; + private static bool recalculateEverything; + + private static readonly List interactablesInRange = new(); + + internal static Item? HoveredItem { get; private set; } + + internal static void RefreshInteractablesInRange(List interactables) + { + interactablesInRange.Clear(); + interactablesInRange.AddRange(interactables); + + shouldRecalculate = true; + } + + private static void RecalculateLabelPositions(Camera cam, Character character) + { + if (recalculateEverything) + { + labels.Clear(); + recalculateEverything = false; + } + + labels.RemoveAll(l => !interactablesInRange.Contains(l.Item)); + + // for every interactable, create a label data object with relevant info for real-time drawing + foreach (var interactableInRange in interactablesInRange) + { + // this removes the hidden vents from the list + if (interactableInRange.HasTag(Tags.HiddenItemContainer)) { continue; } + + // filter out items depending on visibility filter setting + switch (displayMode) + { + case InteractionLabelDisplayMode.InteractionAvailable when !interactableInRange.HasVisibleInteraction(character): + case InteractionLabelDisplayMode.LooseItems when !IsLooseItem(interactableInRange): + continue; + } + + RectangleF textRect = GetLabelRect(interactableInRange, cam); + + if (labels.None(l => l.Item == interactableInRange)) + { + var labelData = new LabelData(interactableInRange, textRect, cam); + labels.Add(labelData); + } + } + + PreventInteractionLabelOverlap(centerPos: character.Position); + } + + private static bool IsLooseItem(Item item) + { + bool hasActivePhysics = item.body is { Enabled: true }; + bool hasPickableComponent = item.GetComponent() != null; + return hasActivePhysics && hasPickableComponent; + } + + private static RectangleF GetLabelRect(Item item, Camera cam) + { + // create rectangle for overlap prevention + Vector2 itemTextSizeScreen = GUIStyle.SubHeadingFont.MeasureString(item.Name) * LabelScale; + Vector2 interactablePosScreen = cam.WorldToScreen(item.Position); + RectangleF textRect = new RectangleF(interactablePosScreen.X, interactablePosScreen.Y, itemTextSizeScreen.X, itemTextSizeScreen.Y); + // center the rectangle on the item + textRect.X -= textRect.Width / 2; + textRect.Y += textRect.Height / 2; + + // inflate by a bit, because the text is drawn with padding + textRect.Inflate(TextBoxMarginPx * LabelScale, TextBoxMarginPx * LabelScale); + + // the rect has screen space size, and sub-relative position + textRect.Location = cam.ScreenToWorld(textRect.Location); + return textRect; + } + + private static void PreventInteractionLabelOverlap(Vector2 centerPos) + { + //sort by distance from "centerPos": moving labels further away from the character (or whatever the center is) is preferred + labels.Sort((l1, l2) => + Vector2.DistanceSquared(l1.TextRect.Center, centerPos).CompareTo( + Vector2.DistanceSquared(l2.TextRect.Center, centerPos))); + + const float MoveStep = 10.0f; + bool intersections = true; + int iterations = 0; + int maxIterations = System.Math.Max(labels.Count * labels.Count, 100); + + while (intersections && iterations < maxIterations) + { + intersections = false; + foreach (var label in labels) + { + if (label.OverlapPreventionDone) { continue; } + foreach (var otherLabel in labels) + { + if (label == otherLabel) { continue; } + + //allow labels to overlap if there's multiple instances of the same item at (roughly) the same position + if (label.Item.Prefab == otherLabel.Item.Prefab && + Vector2.DistanceSquared(label.Item.WorldPosition, otherLabel.Item.WorldPosition) < 1.0f) + { + continue; + } + + if (!label.TextRect.Intersects(otherLabel.TextRect)) + { + continue; + } + intersections = true; + Vector2 moveAmount = Vector2.Normalize(label.TextRect.Center - centerPos) * MoveStep; + label.TextRect = new RectangleF(label.TextRect.Location + moveAmount, label.TextRect.Size); + } + if (intersections) { break; } + } + iterations++; + } + + foreach (var labelData in labels) + { + labelData.OverlapPreventionDone = true; + } + } + + private static int GetMouseHoveredLabelIndex(Camera cam) + { + for (int i = 0; i < labels.Count; i++) + { + var labelData = labels[i]; + var drawRect = labelData.GetScreenDrawRect(cam); + if (drawRect.Contains(PlayerInput.MousePosition)) + { + return i; + } + } + return -1; + } + + private static bool RefreshSettings() + { + bool settingsChanged = false; + + if (GameSettings.CurrentConfig.InteractionLabelDisplayMode != displayMode) + { + displayMode = GameSettings.CurrentConfig.InteractionLabelDisplayMode; + settingsChanged = true; + } + + if (GameMain.GraphicsWidth != graphicsWidth || GameMain.GraphicsHeight != graphicsHeight) + { + graphicsWidth = GameMain.GraphicsWidth; + graphicsHeight = GameMain.GraphicsHeight; + settingsChanged = true; + } + + return settingsChanged; + } + + internal static void Update(Character character, Camera cam) + { + if (RefreshSettings()) { shouldRecalculate = true; recalculateEverything = true; } + + if (shouldRecalculate) + { + RecalculateLabelPositions(cam, character); + } + } + + internal static void DrawLabels(SpriteBatch spriteBatch, Camera cam, Character character) + { + //if any item changes subs or moves significantly, we need to recalculate the label position + foreach (var label in labels) + { + const float MoveThreshold = 150.0f; + if (Vector2.DistanceSquared(label.OriginalItemPosition, label.Item.Position) > MoveThreshold * MoveThreshold) + { + label.TextRect = GetLabelRect(label.Item, cam); + } + } + + // find out if mouse is on top of any of the labels + int mouseOnLabelIndex = GetMouseHoveredLabelIndex(cam); + bool isMouseOnLabel = mouseOnLabelIndex >= 0; + + const float LineAlpha = 0.5f; + + if (!isMouseOnLabel) + { + HoveredItem = null; + } + + // draw order: draw lines for labels first + for (int i = 0; i < labels.Count; i++) + { + // Skip the box that the mouse is on, it will be drawn last + if (i == mouseOnLabelIndex) { continue; } + + DrawLineForLabel(spriteBatch, cam, labels[i], GUIStyle.InteractionLabelColor * LineAlpha); + } + + // Then draw labels + for (int i = 0; i < labels.Count; i++) + { + // Skip the box that the mouse is on, it will be drawn last + if (i == mouseOnLabelIndex) { continue; } + + DrawLabelForItem(spriteBatch, cam, labels[i], GUIStyle.InteractionLabelColor); + } + + // Draw the label and line that the mouse is on last (for draw order) + if (isMouseOnLabel) + { + var labelData = labels[mouseOnLabelIndex]; + + HoveredItem = labelData.Item; + + DrawLineForLabel(spriteBatch, cam, labelData, GUIStyle.InteractionLabelHoverColor * LineAlpha); + DrawLabelForItem(spriteBatch, cam,labelData, GUIStyle.InteractionLabelHoverColor); + } + } + + private static void DrawLineForLabel(SpriteBatch spriteBatch, Camera cam, LabelData labelData, Color color) + { + var drawRect = labelData.GetScreenDrawRect(cam); + // deflate by one pixel to avoid gap between line and box graphic edge + const int lineAnchorInsetPx = 1; + var deflateAmount = lineAnchorInsetPx * GUI.Scale; + deflateAmount = MathHelper.Max(deflateAmount * Screen.Selected.Cam.Zoom, lineAnchorInsetPx); + drawRect.Inflate(-deflateAmount, -deflateAmount); + + var itemDrawPosScreen = labelData.GetInteractableDrawPositionScreen(); + + // if item position is inside the box, don't draw a line + if (drawRect.Contains(itemDrawPosScreen)) { return; } + + // find the point on the box edge that is closest to the item + Vector2 textLineAnchorScreenPos = new Vector2( + MathHelper.Clamp(itemDrawPosScreen.X, drawRect.Left, drawRect.Right), + MathHelper.Clamp(itemDrawPosScreen.Y, drawRect.Top, drawRect.Bottom)); + + // draw line from label to item in the world + GUI.DrawLine(spriteBatch, textLineAnchorScreenPos, itemDrawPosScreen, color, depth: 0f, width: 2f); + } + + private static void DrawLabelForItem(SpriteBatch spriteBatch, Camera cam, LabelData labelData, Color color) + { + float scale = Screen.Selected.Cam.Zoom * LabelScale; + + var textDrawRect = labelData.GetScreenDrawRect(cam); + RectangleF backgroundRect = textDrawRect; + + // remove margin from the box the text is drawn in + textDrawRect.Inflate(-TextBoxMarginPx * scale, -TextBoxMarginPx * scale); + Vector2 textDrawPosScreen = new Vector2(textDrawRect.X, textDrawRect.Y); + + GUIStyle.InteractionLabelBackground.Draw(spriteBatch, backgroundRect, color * 0.7f); + + GUIStyle.SubHeadingFont.DrawString(spriteBatch, + labelData.Item.Name, + textDrawPosScreen, color, rotation: 0, origin: Vector2.Zero, scale, spriteEffects: SpriteEffects.None, layerDepth: 0.0f, + forceUpperCase: ForceUpperCase.No); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 32ce3df20..519b12eed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -483,9 +483,11 @@ namespace Barotrauma //2. check if the base prefab defines the texture if (texturePath.IsNullOrEmpty() && !character.Prefab.VariantOf.IsEmpty) { + Identifier speciesName = character.GetBaseCharacterSpeciesName(); RagdollParams parentRagdollParams = character.IsHumanoid ? - RagdollParams.GetRagdollParams(character.Prefab.VariantOf) : - RagdollParams.GetRagdollParams(character.Prefab.VariantOf); + RagdollParams.GetDefaultRagdollParams(speciesName, character.Params, character.Prefab.ContentPackage) : + RagdollParams.GetDefaultRagdollParams(speciesName, character.Params, character.Prefab.ContentPackage); + texturePath = parentRagdollParams.OriginalElement?.GetAttributeContentPath("texture"); } //3. "default case", get the texture from this character's XML @@ -514,8 +516,8 @@ namespace Barotrauma if (characterInfo != null) { spritePath = characterInfo.ReplaceVars(spritePath); - - if (characterInfo.HeadSprite != null && characterInfo.SpriteTags.Any()) + characterInfo.VerifySpriteTagsLoaded(); + if (characterInfo.SpriteTags.Any()) { string tags = ""; characterInfo.SpriteTags.ForEach(tag => tags += $"[{tag}]"); @@ -695,7 +697,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null, bool disableDeformations = false) { var spriteParams = Params.GetSprite(); - if (spriteParams == null) { return; } + if (spriteParams == null || Alpha <= 0) { return; } float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength; float brightness = Math.Max(1.0f - burn, 0.2f); Color clr = spriteParams.Color; @@ -724,6 +726,8 @@ namespace Barotrauma color = overrideColor ?? color; blankColor = overrideColor ?? blankColor; + color *= Alpha; + blankColor *= Alpha; if (isSevered) { @@ -779,7 +783,7 @@ namespace Barotrauma else { bool useTintMask = TintMask != null && spriteBatch.GetCurrentEffect() is null; - if (useTintMask) + if (useTintMask && Sprite?.Texture != null && TintMask?.Texture != null) { tintEffectParams.Effect ??= GameMain.GameScreen.ThresholdTintEffect; tintEffectParams.Params ??= new Dictionary(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs index 014ccad7b..34d62728d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs @@ -65,7 +65,7 @@ namespace Barotrauma bool isEditor = Screen.Selected is { IsEditor: true }; GUILayoutGroup titleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), listBox.Content.RectTransform)); - new GUITextBlock(new RectTransform(Vector2.One, titleHolder.RectTransform), Item.Name, font: GUIStyle.LargeFont) + new GUITextBlock(new RectTransform(Vector2.One, titleHolder.RectTransform), Item.Prefab.Name, font: GUIStyle.LargeFont) { TextColor = Color.White, Color = Color.Black @@ -84,7 +84,10 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); - var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame: !isEditor, showName: false, titleFont: GUIStyle.SubHeadingFont); + var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame: !isEditor, showName: false, titleFont: GUIStyle.SubHeadingFont) + { + Readonly = CircuitBox.Locked + }; fieldCount += componentEditor.Fields.Count; ic.CreateEditingHUD(componentEditor); diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabelNode.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabelNode.cs new file mode 100644 index 000000000..498b61481 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabelNode.cs @@ -0,0 +1,184 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal sealed partial class CircuitBoxLabelNode + { + private CircuitBoxLabel headerLabel; + private readonly GUITextBlock bodyLabel; + private const string PromptUserData = "LabelEditPrompt"; + + public override void DrawHeader(SpriteBatch spriteBatch, RectangleF rect, Color color) + { + GUI.DrawString(spriteBatch, new Vector2(rect.X + CircuitBoxSizes.NodeHeaderTextPadding, rect.Center.Y - headerLabel.Size.Y / 2f), headerLabel.Value, GUIStyle.TextColorNormal, font: GUIStyle.LargeFont); + } + + public override void DrawBody(SpriteBatch spriteBatch, RectangleF rect, Color color) + { + bodyLabel.TextOffset = rect.Location - bodyLabel.Rect.Location.ToVector2() + new Vector2(CircuitBoxSizes.NodeBodyTextPadding); + bodyLabel.DrawManually(spriteBatch); + } + + public override void OnResized(RectangleF rect) + => UpdateTextSizes(rect); + + private void UpdateTextSizes(RectangleF rect) + { + var size = new Point((int)rect.Width - CircuitBoxSizes.NodeBodyTextPadding * 2, (int)rect.Height - CircuitBoxSizes.NodeBodyTextPadding * 2); + bodyLabel.RectTransform.NonScaledSize = size; + bodyLabel.Text = GetLocalizedText(BodyText); + if (bodyLabel.Font != null) + { + bodyLabel.Text = ToolBox.LimitStringHeight(bodyLabel.WrappedText.Value, bodyLabel.Font!, size.Y); + } + headerLabel = new CircuitBoxLabel(ToolBox.LimitString(GetLocalizedText(HeaderText), GUIStyle.LargeFont, size.X), GUIStyle.LargeFont); + + static LocalizedString GetLocalizedText(NetLimitedString text) => TextManager.Get(text.Value).Fallback(text.Value); + } + + public void PromptEditText(GUIComponent parent) + { + Color newColor = Color; + CircuitBox.UI?.SetMenuVisibility(false); + GUIFrame backgroundBlocker = new(new RectTransform(Vector2.One, parent.RectTransform), style: "GUIBackgroundBlocker") + { + UserData = PromptUserData + }; + + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), backgroundBlocker.RectTransform, Anchor.Center), isHorizontal: false, childAnchor: Anchor.TopCenter); + + GUILayoutGroup colorLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), mainLayout.RectTransform)); + new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), colorLayout.RectTransform)) { IgnoreLayoutGroups = true }; + GUILayoutGroup colorArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), colorLayout.RectTransform), isHorizontal: true); + + GUIFrame labelArea = new(new RectTransform(new Vector2(1f, 0.65f), mainLayout.RectTransform, Anchor.Center)); + + GUIFrame header = new GUIFrame(new RectTransform(new Vector2(1f, 0.15f), labelArea.RectTransform, Anchor.TopLeft), style: "CircuitBoxTop"); + GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.86f), labelArea.RectTransform, Anchor.BottomLeft), style: "CircuitBoxFrame"); + header.Color = frame.Color = Color; + + GUITextBox headerTextBox = new GUITextBox(new RectTransform(Vector2.One, header.RectTransform, Anchor.Center), text: HeaderText.Value, font: headerLabel.Font, style: "GUITextBoxNoStyle") + { + MaxTextLength = NetLimitedString.MaxLength, + Text = HeaderText.Value + }; + + GUITextBox bodyTextBox = new GUITextBox(new RectTransform(ToolBox.PaddingSizeParentRelative(frame.RectTransform, 0.95f), frame.RectTransform, Anchor.Center), text: BodyText.Value, font: GUIStyle.Font, style: "GUITextBoxNoStyle", textAlignment: Alignment.TopLeft, wrap: true) + { + MaxTextLength = NetLimitedString.MaxLength + }; + + bodyTextBox.OnEnterPressed += (textBox, text) => + { + int caretIndex = textBox.CaretIndex; + textBox.Text = $"{text[..caretIndex]}\n{text[caretIndex..]}"; + textBox.CaretIndex = caretIndex + 1; + + return true; + }; + + static void UpdateLabelColor(GUITextBox box) + { + bool found = TextManager.ContainsTag(box.Text); + box.TextColor = found + ? GUIStyle.Orange + : GUIStyle.TextColorNormal; + + if (found) + { + box.ToolTip = TextManager.GetWithVariable("StringPropertyTranslate", "[translation]", TextManager.Get(box.Text)); + } + else + { + box.ToolTip = string.Empty; + } + } + + bodyTextBox.OnDeselected += (textBox, _) => UpdateLabelColor(textBox); + headerTextBox.OnDeselected += (textBox, _) => UpdateLabelColor(textBox); + UpdateLabelColor(bodyTextBox); + UpdateLabelColor(headerTextBox); + + mainLayout.Recalculate(); + headerTextBox.ForceUpdate(); + + new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), mainLayout.RectTransform), text: TextManager.Get("confirm")) + { + OnClicked = (_, _) => + { + CircuitBox.RenameLabel(this, newColor, new NetLimitedString(headerTextBox.Text), new NetLimitedString(bodyTextBox.Text)); + RemoveEditPrompt(parent); + return true; + } + }; + new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), mainLayout.RectTransform), text: TextManager.Get("cancel")) + { + OnClicked = (_, _) => + { + RemoveEditPrompt(parent); + return true; + } + }; + + LocalizedString[] colorComponentLabels = + { + TextManager.Get("spriteeditor.colorcomponentr"), + TextManager.Get("spriteeditor.colorcomponentg"), + TextManager.Get("spriteeditor.colorcomponentb") + }; + for (int i = 0; i <= 2; i++) + { + var element = new GUIFrame(new RectTransform(new Vector2(0.33f, 1), colorArea.RectTransform), style: null); + + var colorLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), colorComponentLabels[i], + font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + + var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Int) + { + Font = GUIStyle.SubHeadingFont, + MinValueInt = 0, + MaxValueInt = 255 + }; + switch (i) + { + case 0: + colorLabel.TextColor = GUIStyle.Red; + numberInput.IntValue = Color.R; + numberInput.OnValueChanged += numInput => + { + newColor.R = (byte)numInput.IntValue; + header.Color = frame.Color = newColor; + }; + break; + case 1: + colorLabel.TextColor = GUIStyle.Green; + numberInput.IntValue = Color.G; + numberInput.OnValueChanged += numInput => + { + newColor.G = (byte)numInput.IntValue; + header.Color = frame.Color = newColor; + }; + break; + case 2: + colorLabel.TextColor = GUIStyle.Blue; + numberInput.IntValue = Color.B; + numberInput.OnValueChanged += numInput => + { + newColor.B = (byte)numInput.IntValue; + header.Color = frame.Color = newColor; + }; + break; + } + } + } + + public void RemoveEditPrompt(GUIComponent parent) + { + if (parent.FindChild(PromptUserData) is not { } promptParent) { return; } + parent.RemoveChild(promptParent); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs index 71d74c142..59981ca5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs @@ -1,5 +1,6 @@ #nullable enable +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -15,7 +16,17 @@ namespace Barotrauma /// internal sealed class CircuitBoxMouseDragSnapshotHandler { - public IEnumerable Nodes => circuitBoxUi.CircuitBox.Components.Union(circuitBoxUi.CircuitBox.InputOutputNodes); + public IEnumerable Nodes + { + get + { + var cb = circuitBoxUi.CircuitBox; + + foreach (var label in cb.Labels) { yield return label; } + foreach (var component in cb.Components) { yield return component; } + foreach (var node in cb.InputOutputNodes) { yield return node; } + } + } private IReadOnlyList Wires => circuitBoxUi.CircuitBox.Wires; @@ -29,6 +40,8 @@ namespace Barotrauma // Nodes that should be moved when dragging moveAffectedComponents = ImmutableHashSet.Empty; + public Option<(CircuitBoxResizeDirection, CircuitBoxNode)> LastResizeAffectedNode = Option.None; + public ImmutableHashSet GetLastComponentsUnderCursor() => lastNodesUnderCursor; public ImmutableHashSet GetMoveAffectedComponents() => moveAffectedComponents; @@ -45,6 +58,11 @@ namespace Barotrauma /// public bool IsWiring { get; private set; } + /// + /// If the user grabbed a side of a node and is resizing a node + /// + public bool IsResizing { get; private set; } + private Vector2 startClick = Vector2.Zero; private readonly CircuitBoxUI circuitBoxUi; @@ -147,6 +165,41 @@ namespace Barotrauma lastNodesUnderCursor = FindNodesUnderCursor(cursorPos); LastConnectorUnderCursor = FindConnectorUnderCursor(cursorPos); LastWireUnderCursor = FindWireUnderCursor(cursorPos); + LastResizeAffectedNode = FindResizeBorderUnderCursor(lastNodesUnderCursor, cursorPos); + } + + private static Option<(CircuitBoxResizeDirection, CircuitBoxNode)> FindResizeBorderUnderCursor(ImmutableHashSet nodes, Vector2 cursorPos) + { + foreach (var node in nodes) + { + if (!node.IsResizable) { continue; } + + const float borderSize = 32f; + + var rect = node.Rect; + RectangleF bottomBorder = new(rect.X, rect.Top, rect.Width, borderSize); + RectangleF rightBorder = new(rect.Right - borderSize, rect.Y, borderSize, rect.Height); + RectangleF leftBorder = new(rect.X, rect.Y, borderSize, rect.Height); + + bool hoverBottom = bottomBorder.Contains(cursorPos), + hoverRight = rightBorder.Contains(cursorPos), + hoverLeft = leftBorder.Contains(cursorPos); + + var dir = CircuitBoxResizeDirection.None; + + if (hoverBottom) { dir |= CircuitBoxResizeDirection.Down; } + if (hoverRight) { dir |= CircuitBoxResizeDirection.Right; } + if (hoverLeft) { dir |= CircuitBoxResizeDirection.Left; } + + if (dir is CircuitBoxResizeDirection.None) + { + continue; + } + + return Option.Some((dir, node)); + } + + return Option.None; } /// @@ -193,6 +246,7 @@ namespace Barotrauma startClick = Vector2.Zero; IsDragging = false; IsWiring = false; + IsResizing = false; lastNodesUnderCursor = ImmutableHashSet.Empty; } @@ -210,23 +264,34 @@ namespace Barotrauma IsDragging = false; } + if (LastResizeAffectedNode.IsNone()) + { + IsResizing = false; + } + // startClick is set to zero when the user releases the mouse button, so we should be neither dragging nor wiring in this state if (startClick == Vector2.Zero) { IsDragging = false; IsWiring = false; + IsResizing = false; return; } - bool isDragTresholdExceeded = Vector2.DistanceSquared(startClick, cursorPos) > dragTreshold * dragTreshold; + if (circuitBoxUi.Locked) { return; } + bool isDragThresholdExceeded = Vector2.DistanceSquared(startClick, cursorPos) > dragTreshold * dragTreshold; - if (LastConnectorUnderCursor.IsNone()) + if (LastResizeAffectedNode.IsSome()) { - IsDragging |= isDragTresholdExceeded; + IsResizing |= isDragThresholdExceeded; + } + else if (LastConnectorUnderCursor.IsSome()) + { + IsWiring |= isDragThresholdExceeded; } else { - IsWiring |= isDragTresholdExceeded; + IsDragging |= isDragThresholdExceeded; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs index b470f97f9..ce877f013 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs @@ -7,10 +7,10 @@ namespace Barotrauma { internal partial class CircuitBoxNode { - private RectangleF DrawRect, - TopDrawRect; + public RectangleF DrawRect; + private RectangleF TopDrawRect; - private void UpdateDrawRects() + protected void UpdateDrawRects() { var drawRect = new RectangleF(Position - Size / 2f, Size); drawRect.Y = -drawRect.Y; @@ -26,6 +26,8 @@ namespace Barotrauma UpdatePositions(); } + public virtual void OnResized(RectangleF drawRect) { } + public void DrawBackground(SpriteBatch spriteBatch, RectangleF drawRect, RectangleF topDrawRect, Color color) { CircuitBox.NodeFrameSprite?.Draw(spriteBatch, drawRect, color); @@ -39,6 +41,7 @@ namespace Barotrauma DrawBackground(spriteBatch, drawRect, topDrawRect, color); DrawHeader(spriteBatch, topDrawRect, color); + DrawBody(spriteBatch, drawRect, color); DrawConnectors(spriteBatch, drawPos); } @@ -52,6 +55,7 @@ namespace Barotrauma } public virtual void DrawHeader(SpriteBatch spriteBatch, RectangleF rect, Color color) { } + public virtual void DrawBody(SpriteBatch spriteBatch, RectangleF rect, Color color) { } public void DrawConnectors(SpriteBatch spriteBatch, Vector2 drawPos) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs index ee7a45761..a595e7410 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs @@ -35,6 +35,8 @@ namespace Barotrauma public List VirtualWires = new(); + public bool Locked => CircuitBox.Locked; + public CircuitBoxUI(CircuitBox box) { camera = new Camera @@ -47,7 +49,7 @@ namespace Barotrauma MouseSnapshotHandler = new CircuitBoxMouseDragSnapshotHandler(this); } -#region UI + #region UI public void CreateGUI(GUIFrame parent) { @@ -63,7 +65,7 @@ namespace Barotrauma spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - DrawHUD(spriteBatch); + DrawHUD(spriteBatch, component.Rect); spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; @@ -82,6 +84,8 @@ namespace Barotrauma OnClicked = (btn, userdata) => { componentMenuOpen = !componentMenuOpen; + if (Locked) { componentMenuOpen = false; } + foreach (GUIComponent child in btn.Children) { child.SpriteEffects = componentMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically; @@ -139,65 +143,68 @@ namespace Barotrauma }; int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), parent.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, - style: "GUIButtonSettings") + style: "GUIButtonSettings") + { + OnClicked = (btn, userdata) => { - OnClicked = (btn, userdata) => - { - GUIContextMenu.CreateContextMenu( - new ContextMenuOption("circuitboxsetting.resetview", isEnabled: true, onSelected: ResetCamera) + GUIContextMenu.CreateContextMenu( + new ContextMenuOption("circuitboxsetting.resetview", isEnabled: true, onSelected: ResetCamera) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.resetview") + }, + new ContextMenuOption("circuitboxsetting.find", isEnabled: true, + new ContextMenuOption("circuitboxsetting.focusinput", isEnabled: true, onSelected: () => FindInputOutput(CircuitBoxInputOutputNode.Type.Input)) { - Tooltip = TextManager.Get("circuitboxsettingdescription.resetview") + Tooltip = TextManager.Get("circuitboxsettingdescription.focusinput") }, - new ContextMenuOption("circuitboxsetting.find", isEnabled: true, - new ContextMenuOption("circuitboxsetting.focusinput", isEnabled: true, onSelected: () => FindInputOuput(CircuitBoxInputOutputNode.Type.Input)) - { - Tooltip = TextManager.Get("circuitboxsettingdescription.focusinput") - }, - new ContextMenuOption("circuitboxsetting.focusoutput", isEnabled: true, onSelected: () => FindInputOuput(CircuitBoxInputOutputNode.Type.Output)) - { - Tooltip = TextManager.Get("circuitboxsettingdescription.focusoutput") - }, - new ContextMenuOption("circuitboxsetting.focuscircuits", isEnabled: CircuitBox.Components.Any(), onSelected: FindCircuit) - { - Tooltip = TextManager.Get("circuitboxsettingdescription.focuscircuits") - })); + new ContextMenuOption("circuitboxsetting.focusoutput", isEnabled: true, onSelected: () => FindInputOutput(CircuitBoxInputOutputNode.Type.Output)) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.focusoutput") + }, + new ContextMenuOption("circuitboxsetting.focuscircuits", isEnabled: CircuitBox.Components.Any(), onSelected: FindCircuit) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.focuscircuits") + })); - void ResetCamera() - { - // Vector2.One because Vector2.Zero means no value - camera.TargetPos = Vector2.One; - } - - void FindInputOuput(CircuitBoxInputOutputNode.Type type) - { - var input = CircuitBox.InputOutputNodes.FirstOrDefault(n => n.NodeType == type); - if (input is null) { return; } - - camera.TargetPos = input.Position; - } - - void FindCircuit() - { - var closestComponent = CircuitBox.Components.MinBy(c => Vector2.DistanceSquared(c.Position, camera.Position)); - if (closestComponent is null) { return; } - - camera.TargetPos = closestComponent.Position; - } - return true; + void ResetCamera() + { + // Vector2.One because Vector2.Zero means no value + camera.TargetPos = Vector2.One; } - }; + + void FindInputOutput(CircuitBoxInputOutputNode.Type type) + { + var input = CircuitBox.InputOutputNodes.FirstOrDefault(n => n.NodeType == type); + if (input is null) { return; } + + camera.TargetPos = input.Position; + } + + void FindCircuit() + { + var closestComponent = CircuitBox.Components.MinBy(c => Vector2.DistanceSquared(c.Position, camera.Position)); + if (closestComponent is null) { return; } + + camera.TargetPos = closestComponent.Position; + } + + return true; + } + }; MouseSnapshotHandler.UpdateConnections(); // update scales of everything foreach (var node in CircuitBox.Components) { node.OnUICreated(); } + foreach (var node in CircuitBox.InputOutputNodes) { node.OnUICreated(); } + foreach (var wire in CircuitBox.Wires) { wire.Update(); } } - private string GetInventoryText() - => CircuitBox.ComponentContainer is { } container + private string GetInventoryText() => + CircuitBox.ComponentContainer is { } container ? $"{container.Inventory.AllItems.Count()}/{container.Capacity}" : "0/0"; @@ -209,7 +216,8 @@ namespace Barotrauma } if (componentList is null) { return; } - var playerInventory = CircuitBox.GetSortedCircuitBoxSortedItemsFromPlayer(Character.Controlled); + + var playerInventory = CircuitBox.GetSortedCircuitBoxItemsFromPlayer(Character.Controlled); foreach (GUIComponent child in componentList.Content.Children) { @@ -304,9 +312,9 @@ namespace Barotrauma } } -#endregion + #endregion - private void DrawHUD(SpriteBatch spriteBatch) + private void DrawHUD(SpriteBatch spriteBatch, Rectangle screenRect) { float scale = GUI.Scale / 1.5f; Vector2 offset = new Vector2(20, 40) * scale; @@ -352,6 +360,16 @@ namespace Barotrauma { n.DrawHUD(spriteBatch, camera); } + + if (Locked) + { + LocalizedString lockedText = TextManager.Get("CircuitBoxLocked") + .Fallback(TextManager.Get("ConnectionLocked")); + + Vector2 size = GUIStyle.LargeFont.MeasureString(lockedText); + Vector2 pos = new Vector2(screenRect.Center.X - size.X / 2, screenRect.Top + screenRect.Height * 0.05f); + GUI.DrawString(spriteBatch, pos, lockedText, Color.Red, Color.Black, 8, GUIStyle.LargeFont); + } } private void DrawSelection(SpriteBatch spriteBatch, Vector2 pos1, Vector2 pos2, Color color) @@ -367,6 +385,12 @@ namespace Barotrauma private static float lineWidth; public static void DrawRectangleWithBorder(SpriteBatch spriteBatch, RectangleF rect, Color fillColor, Color borderColor) + { + GUI.DrawFilledRectangle(spriteBatch, rect, fillColor); + DrawRectangleOnlyBorder(spriteBatch, rect, borderColor); + } + + private static void DrawRectangleOnlyBorder(SpriteBatch spriteBatch, RectangleF rect, Color borderColor) { Vector2 topRight = new Vector2(rect.Right, rect.Top), topLeft = new Vector2(rect.Left, rect.Top), @@ -375,8 +399,6 @@ namespace Barotrauma Vector2 offset = new Vector2(0f, lineWidth / 2f); - GUI.DrawFilledRectangle(spriteBatch, rect, fillColor); - spriteBatch.DrawLine(topRight, topLeft, borderColor, thickness: lineWidth); spriteBatch.DrawLine(topLeft - offset, bottomLeft + offset, borderColor, thickness: lineWidth); spriteBatch.DrawLine(bottomLeft, bottomRight, borderColor, thickness: lineWidth); @@ -393,6 +415,16 @@ namespace Barotrauma Vector2 mousePos = GetCursorPosition(); mousePos.Y = -mousePos.Y; + foreach (var label in CircuitBox.Labels) + { + if (label.IsSelected) + { + label.DrawSelection(spriteBatch, GetSelectionColor(label)); + } + + label.Draw(spriteBatch, label.Position, label.Color); + } + foreach (CircuitBoxWire wire in CircuitBox.Wires) { wire.Renderer.Draw(spriteBatch, GetSelectionColor(wire)); @@ -428,6 +460,7 @@ namespace Barotrauma Color color = moveable switch { CircuitBoxComponent node => node.Item.Prefab.SignalComponentColor, + CircuitBoxLabelNode label => label.Color, CircuitBoxInputOutputNode ioNode => ioNode.NodeType is CircuitBoxInputOutputNode.Type.Input ? GUIStyle.Green : GUIStyle.Red, _ => Color.White }; @@ -435,17 +468,49 @@ namespace Barotrauma } } + if (MouseSnapshotHandler.IsResizing && MouseSnapshotHandler.LastResizeAffectedNode.TryUnwrap(out var resize)) + { + var (dir, node) = resize; + Vector2 dragOffset = MouseSnapshotHandler.GetDragAmount(GetCursorPosition()); + + var rect = node.Rect; + rect.Y = -rect.Y; + rect.Y -= rect.Height; + + if (dir.HasFlag(CircuitBoxResizeDirection.Down)) + { + rect.Height -= dragOffset.Y; + rect.Height = Math.Max(rect.Height, CircuitBoxLabelNode.MinSize.Y + CircuitBoxSizes.NodeHeaderHeight); + } + + if (dir.HasFlag(CircuitBoxResizeDirection.Right)) + { + rect.Width += dragOffset.X; + rect.Width = Math.Max(rect.Width, CircuitBoxLabelNode.MinSize.X); + } + + if (dir.HasFlag(CircuitBoxResizeDirection.Left)) + { + float oldWidth = rect.Width; + rect.Width -= dragOffset.X; + rect.Width = Math.Max(rect.Width, CircuitBoxLabelNode.MinSize.X); + + float actualResize = rect.Width - oldWidth; + rect.X -= actualResize; + } + + DrawRectangleOnlyBorder(spriteBatch, rect, GUIStyle.Yellow); + } + if (DraggedWire.TryUnwrap(out CircuitBoxWireRenderer? draggedWire)) { draggedWire.Draw(spriteBatch, GUIStyle.Yellow); } } - private Color GetSelectionColor(CircuitBoxNode node) - => GetSelectionColor(node.SelectedBy, node.IsSelectedByMe); + private Color GetSelectionColor(CircuitBoxNode node) => GetSelectionColor(node.SelectedBy, node.IsSelectedByMe); - private Color GetSelectionColor(CircuitBoxWire wire) - => GetSelectionColor(wire.SelectedBy, wire.IsSelectedByMe); + private Color GetSelectionColor(CircuitBoxWire wire) => GetSelectionColor(wire.SelectedBy, wire.IsSelectedByMe); private Color GetSelectionColor(ushort selectedBy, bool isSelectedByMe) { @@ -489,6 +554,7 @@ namespace Barotrauma { node.UpdateEditing(circuitComponent.RectTransform); } + break; } @@ -503,6 +569,7 @@ namespace Barotrauma { Character.DisableControls = true; } + camera.MoveCamera(deltaTime, allowMove: true, allowZoom: isMouseOn, allowInput: isMouseOn, followSub: false); if (camera.TargetPos != Vector2.Zero && MathUtils.NearlyEqual(camera.Position, camera.TargetPos, 0.01f)) @@ -547,7 +614,7 @@ namespace Barotrauma } else { - DraggedWire = Option.Some(new CircuitBoxWireRenderer(Option.None,start, end, GUIStyle.Red, CircuitBox.WireSprite)); + DraggedWire = Option.Some(new CircuitBoxWireRenderer(Option.None, start, end, GUIStyle.Red, CircuitBox.WireSprite)); } } else @@ -562,6 +629,12 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonClicked()) { + if (MouseSnapshotHandler.IsResizing && MouseSnapshotHandler.LastResizeAffectedNode.TryUnwrap(out var r)) + { + var (dir, node) = r; + CircuitBox.ResizeNode(node, dir, MouseSnapshotHandler.GetDragAmount(cursorPos)); + } + if (CircuitBox.HeldComponent.TryUnwrap(out ItemPrefab? prefab)) { CircuitBox.AddComponent(prefab, cursorPos); @@ -604,22 +677,32 @@ namespace Barotrauma { CircuitBox.RemoveComponents(CircuitBox.Components.Where(static node => node.IsSelectedByMe).ToArray()); CircuitBox.RemoveWires(CircuitBox.Wires.Where(static wire => wire.IsSelectedByMe).ToImmutableArray()); + CircuitBox.RemoveLabel(CircuitBox.Labels.Where(static label => label.IsSelectedByMe).ToImmutableArray()); } } if (componentMenu is { } menu && toggleMenuButton is { } button) { - componentMenuOpenState = componentMenuOpen ? Math.Min(componentMenuOpenState + deltaTime * 5.0f, 1.0f) : Math.Max(componentMenuOpenState - deltaTime * 5.0f, 0.0f); + button.Enabled = !Locked; + componentMenuOpenState = componentMenuOpen && !Locked ? Math.Min(componentMenuOpenState + deltaTime * 5.0f, 1.0f) : Math.Max(componentMenuOpenState - deltaTime * 5.0f, 0.0f); menu.RectTransform.ScreenSpaceOffset = Vector2.Lerp(new Vector2(0.0f, menu.Rect.Height - 10), Vector2.Zero, componentMenuOpenState).ToPoint(); button.RectTransform.AbsoluteOffset = new Point(menu.Rect.X + ((menu.Rect.Width / 2) - (button.Rect.Width / 2)), menu.Rect.Y - button.Rect.Height); } + + if (selectedWireFrame is { } wireFrame) + { + wireFrame.Visible = !Locked; + } camera.Position = Vector2.Clamp(camera.Position, new Vector2(-CircuitBoxSizes.PlayableAreaSize / 2f), new Vector2(CircuitBoxSizes.PlayableAreaSize / 2f)); } + public void SetMenuVisibility(bool state) + => componentMenuOpen = state; + private void UpdateSelection() { if (!PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonDown()) @@ -662,35 +745,55 @@ namespace Barotrauma var wireSelection = CircuitBox.Wires.Where(static w => w.IsSelectedByMe).ToImmutableArray(); var nodeOption = GetTopmostNode(MouseSnapshotHandler.FindNodesUnderCursor(cursorPos)); var nodeSelection = CircuitBox.Components.Where(static n => n.IsSelectedByMe).ToImmutableArray(); + var labels = CircuitBox.Labels.Where(static l => l.IsSelectedByMe).ToImmutableArray(); - var option = new ContextMenuOption(TextManager.Get("delete"), isEnabled: wireOption.IsSome() || nodeOption is CircuitBoxComponent, () => + var option = new ContextMenuOption(TextManager.Get("delete"), isEnabled: (wireOption.IsSome() || nodeOption is CircuitBoxComponent or CircuitBoxLabelNode) && !Locked, () => { if (wireOption.TryUnwrap(out var wire)) { CircuitBox.RemoveWires(wire.IsSelected ? wireSelection : ImmutableArray.Create(wire)); } - if (nodeOption is CircuitBoxComponent node) + switch (nodeOption) { - CircuitBox.RemoveComponents(node.IsSelected ? nodeSelection : ImmutableArray.Create(node)); + case CircuitBoxComponent node: + CircuitBox.RemoveComponents(node.IsSelected ? nodeSelection : ImmutableArray.Create(node)); + break; + case CircuitBoxLabelNode label: + CircuitBox.RemoveLabel(label.IsSelected ? labels : ImmutableArray.Create(label)); + break; } }); + var editLabel = new ContextMenuOption(TextManager.Get("circuitboxeditlabel"), isEnabled: nodeOption is CircuitBoxLabelNode && !Locked, () => + { + if (nodeOption is not CircuitBoxLabelNode label || circuitComponent is null) { return; } + + label.PromptEditText(circuitComponent); + }); + + var addLabelOption = new ContextMenuOption(TextManager.Get("circuitboxaddlabel"), isEnabled: !Locked, () => + { + CircuitBox.AddLabel(cursorPos); + }); + + ContextMenuOption[] allOptions = { addLabelOption, editLabel, option }; + // show component name in the header to better indicate what is about to be deleted if (nodeOption is CircuitBoxComponent comp) { - GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, comp.Item.Name, comp.Item.Prefab.SignalComponentColor, option); + GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, comp.Item.Name, comp.Item.Prefab.SignalComponentColor, allOptions); return; } // also check if a wire is being deleted if (wireOption.TryUnwrap(out var foundWire)) { - GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, foundWire.UsedItemPrefab.Name, foundWire.Color, option); + GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, foundWire.UsedItemPrefab.Name, foundWire.Color, allOptions); return; } - GUIContextMenu.CreateContextMenu(option); + GUIContextMenu.CreateContextMenu(allOptions); } public CircuitBoxNode? GetTopmostNode(ImmutableHashSet nodes) diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 8c47bb289..c53433a68 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -2434,6 +2434,29 @@ namespace Barotrauma } })); + commands.Add(new Command("converttowreck", "", (string[] args) => + { + if (Screen.Selected is not SubEditorScreen) + { + ThrowError("The command can only be used in the submarine editor."); + return; + } + if (Submarine.MainSub == null) + { + ThrowError("Load a submarine first to convert it to a wreck."); + return; + } + if (Submarine.MainSub.Info.SubmarineElement == null) + { + ThrowError("The submarine must be saved before you can convert it to a wreck."); + return; + } + var wreckedSubmarineInfo = new SubmarineInfo(filePath: string.Empty, element: WreckConverter.ConvertToWreck(Submarine.MainSub.Info.SubmarineElement)); + wreckedSubmarineInfo.Name += "_Wrecked"; + wreckedSubmarineInfo.Type = SubmarineType.Wreck; + GameMain.SubEditorScreen.LoadSub(wreckedSubmarineInfo); + })); + commands.Add(new Command("camerasettings", "camerasettings [defaultzoom] [zoomsmoothness] [movesmoothness] [minzoom] [maxzoom]: debug command for testing camera settings. The values default to 1.1, 8.0, 8.0, 0.1 and 2.0.", (string[] args) => { float defaultZoom = Screen.Selected.Cam.DefaultZoom; @@ -2485,6 +2508,49 @@ namespace Barotrauma } })); + commands.Add(new Command("listcontainertags", "Lists all container tags on the submarine.", (string[] args) => + { + if (Screen.Selected != GameMain.SubEditorScreen) + { + ThrowError("This command can only be used in the sub editor."); + return; + } + + HashSet allContainerTagsInTheGame = new(); + + foreach (var itemPrefab in ItemPrefab.Prefabs) + { + foreach (var pc in itemPrefab.PreferredContainers) + { + foreach (Identifier identifier in Enumerable.Union(pc.Primary, pc.Secondary)) + { + allContainerTagsInTheGame.Add(identifier); + } + } + } + + Dictionary prefab = new(); + + foreach (Item it in Item.ItemList) + { + foreach (var tag in allContainerTagsInTheGame) + { + if (it.GetTags().All(t => tag != t)) { continue; } + + prefab.TryAdd(tag, 0.0f); + prefab[tag]++; + } + } + + StringBuilder sb = new(); + foreach (var (tag, amount) in prefab.OrderByDescending(kvp => kvp.Value)) + { + sb.AppendLine($"{tag}: {amount}"); + } + + NewMessage(sb.ToString()); + }, isCheat: false)); + commands.Add(new Command("refreshrect", "Updates the dimensions of the selected items to match the ones defined in the prefab. Applied only in the subeditor.", (string[] args) => { //TODO: maybe do this automatically during loading when possible? @@ -3015,6 +3081,46 @@ namespace Barotrauma ContentPackageManager.EnabledPackages.ReloadCore(); })); + commands.Add(new Command("reloadpackage", "reloapackage [name]: reloads a content package.", (string[] args) => + { + if (args.Length < 1) + { + ThrowError("Please specify the name of the package to reload."); + return; + } + + if (args.Length < 2) + { + if (Screen.Selected == GameMain.GameScreen) + { + ThrowError("Reloading the package while in GameScreen may break things; to do it anyway, type 'reloadcorepackage [name] force'"); + return; + } + if (Screen.Selected == GameMain.SubEditorScreen) + { + ThrowError("Reloading the core package while in sub editor may break thingg; to do it anyway, type 'reloadcorepackage [name] force'"); + return; + } + } + + if (GameMain.NetworkMember != null) + { + ThrowError("Cannot change content packages while playing online"); + return; + } + + var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]); + if (package == null) + { + ThrowError($"Could not find the package {args[0]}!"); + return; + } + ContentPackageManager.EnabledPackages.ReloadPackage(package); + }, getValidArgs: () => new[] + { + ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray() + })); + #if WINDOWS commands.Add(new Command("startdedicatedserver", "", (string[] args) => { @@ -3395,6 +3501,29 @@ namespace Barotrauma } character.AnimController.ResetRagdoll(forceReload: true); }, isCheat: true)); + + commands.Add(new Command("loadanimation", "Loads an animation variation by name for the controlled character. The animation file has to be in the correct animations folder. Note: the changes are not saved!", (string[] args) => + { + var character = Character.Controlled; + if (character == null) + { + ThrowError("Not controlling any character!"); + return; + } + if (args.Length < 2) + { + ThrowError("Insufficient parameters: Have to pass the type of animation (Walk, Run, SwimSlow, SwimFast, or Crouch) and the filename!"); + return; + } + string type = args[0]; + if (!Enum.TryParse(type, ignoreCase: true, out AnimationType animationType)) + { + ThrowError($"Failed to parse animation type from {type}. Supported types are Walk, Run, SwimSlow, SwimFast, and Crouch!"); + return; + } + string fileName = args[1]; + character.AnimController.TryLoadAnimation(animationType, Path.GetFileNameWithoutExtension(fileName), out _, throwErrors: true); + }, isCheat: true)); commands.Add(new Command("reloadwearables", "Reloads the sprites of all limbs and wearable sprites (clothing) of the controlled character. Provide id or name if you want to target another character.", args => { @@ -3545,7 +3674,10 @@ namespace Barotrauma } try { - var subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + var subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => + //accept both the localized and the non-localized name of the sub + s.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase) || + s.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (subInfo == null) { ThrowError($"Could not find a submarine with the name \"{args[0]}\"."); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs index 55f82a1d4..1b8b52ee0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs @@ -1,8 +1,10 @@ -using Barotrauma.Tutorials; using Segment = Barotrauma.ObjectiveManager.Segment; namespace Barotrauma; +/// +/// Checks the state of an Objective created using . +/// partial class CheckObjectiveAction : BinaryOptionAction { public enum CheckType @@ -12,10 +14,10 @@ partial class CheckObjectiveAction : BinaryOptionAction Incomplete } - [Serialize(CheckType.Completed, IsPropertySaveable.Yes)] + [Serialize(CheckType.Completed, IsPropertySaveable.Yes, description: "The objective must be in this state for the check to succeed.")] public CheckType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The identifier of the objective to check.")] public Identifier Identifier { get; set; } partial void DetermineSuccessProjSpecific(ref bool success) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs index 0929a180f..079fb7529 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs @@ -1,4 +1,3 @@ -using Barotrauma.Tutorials; using System; using System.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 3e9fdd7cb..71c382d10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -12,7 +12,7 @@ namespace Barotrauma { LocalizedString rewardText = GetRewardAmountText(sub); LocalizedString retVal; - if (rewardPerCrate.HasValue) + if (rewardPerCrate.HasValue) // If every crate has the same value { LocalizedString rewardPerCrateText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", rewardPerCrate.Value)); retVal = TextManager.GetWithVariables("missionrewardcargopercrate", @@ -21,7 +21,7 @@ namespace Barotrauma ("[maxitemcount]", maxItemCount.ToString()), ("[totalreward]", $"‖color:gui.orange‖{rewardText}‖end‖")); } - else + else // Crates have differing values, so only show the total value { retVal = TextManager.GetWithVariables("missionrewardcargo", ("[totalreward]", $"‖color:gui.orange‖{rewardText}‖end‖"), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index c244b7a22..9081d1751 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -151,10 +151,10 @@ namespace Barotrauma message = ModifyMessage(message); } - CoroutineManager.StartCoroutine(ShowMessageBoxAfterRoundSummary(header, message)); + CoroutineManager.StartCoroutine(ShowMessageBoxWhenRoundSummaryIsNotActive(header, message)); } - private IEnumerable ShowMessageBoxAfterRoundSummary(LocalizedString header, LocalizedString message) + private IEnumerable ShowMessageBoxWhenRoundSummaryIsNotActive(LocalizedString header, LocalizedString message) { while (GUIMessageBox.VisibleBox?.UserData is RoundSummary) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index be7a49430..f4af6fdcb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -8,6 +8,22 @@ namespace Barotrauma public override bool DisplayAsCompleted => false; public override bool DisplayAsFailed => false; + private void TryShowRetrievedMessage() + { + if (DetermineCompleted()) + { + if (!allRetrievedMessage.IsNullOrEmpty()) { CreateMessageBox(string.Empty, allRetrievedMessage); } + //no need to show this again, clear it + allRetrievedMessage = string.Empty; + } + else + { + if (!partiallyRetrievedMessage.IsNullOrEmpty()) { CreateMessageBox(string.Empty, partiallyRetrievedMessage); } + //no need to show this again, clear it + partiallyRetrievedMessage = string.Empty; + } + } + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); @@ -30,6 +46,15 @@ namespace Barotrauma else { target.Item = Item.ReadSpawnData(msg); + target.Item.HighlightColor = GUIStyle.Orange; + target.Item.ExternalHighlight = true; + + ushort parentTargetId = msg.ReadUInt16(); + if (parentTargetId != Entity.NullEntityID) + { + target.OriginalContainer = Entity.FindEntityByID(parentTargetId) as Item; + } + if (target.Item == null) { throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); @@ -45,7 +70,7 @@ namespace Barotrauma target.Item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: target.Item.Position); } - if (target.Item.body != null) + if (target.Item.body != null && target.Item.CurrentHull == null) { target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; } @@ -55,15 +80,25 @@ namespace Barotrauma public override void ClientRead(IReadMessage msg) { base.ClientRead(msg); + bool atLeastOneTargetWasRetrieved = false; int targetCount = msg.ReadByte(); for (int i = 0; i < targetCount; i++) { var state = (Target.RetrievalState)msg.ReadByte(); if (i < targets.Count) { + bool wasRetrieved = targets[i].Retrieved; targets[i].State = state; + if (!wasRetrieved && targets[i].Retrieved) + { + atLeastOneTargetWasRetrieved = true; + } } } + if (atLeastOneTargetWasRetrieved) + { + TryShowRetrievedMessage(); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index 16ea7caa6..56bf2ac1d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -128,6 +128,11 @@ namespace Barotrauma { case "sprite": UISprite newSprite = new UISprite(subElement); + Rectangle sourceRect = newSprite.Sprite.SourceRect; + if ((sourceRect.Width <= 1 || sourceRect.Height <= 1) && newSprite.Tile) + { + DebugConsole.AddWarning($"Sprite \"{subElement.GetAttributeString("name", Name)}\" has a size of 1 or less which may cause performance problems.", contentPackage: element.ContentPackage); + } GUIComponent.ComponentState spriteState = GUIComponent.ComponentState.None; if (subElement.GetAttribute("state") != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 374e92d5e..d43f789b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -187,7 +187,16 @@ namespace Barotrauma Spacing = 1 }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUIStyle.SubHeadingFont); + var crewHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUIStyle.SubHeadingFont); + + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), crewHeader.RectTransform, Anchor.CenterRight), string.Empty, textAlignment: Alignment.CenterRight) + { + TextGetter = () => + { + int crewSize = campaign?.CrewManager?.GetCharacterInfos()?.Count() ?? 0; + return $"{crewSize}/{CrewManager.MaxCrewSize}"; + } + }; crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform)) { Spacing = 1 @@ -207,7 +216,7 @@ namespace Barotrauma { ClickSound = GUISoundType.ConfirmTransaction, ForceUpperCase = ForceUpperCase.Yes, - OnClicked = (b, o) => ValidateHires(PendingHires, true) + OnClicked = (b, o) => ValidateHires(PendingHires, createNetworkEvent: true) }; clearAllButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall")) { @@ -647,7 +656,7 @@ namespace Barotrauma private bool AddPendingHire(CharacterInfo characterInfo, bool createNetworkMessage = true) { - if (PendingHires.Count + campaign.CrewManager.GetCharacters().Count() >= CrewManager.MaxCrewSize) + if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize) { return false; } @@ -703,7 +712,7 @@ namespace Barotrauma validateHiresButton.Enabled = enoughMoney && HasPermission && pendingList.Content.RectTransform.Children.Any(); } - public bool ValidateHires(List hires, bool createNetworkEvent = false) + public bool ValidateHires(List hires, bool takeMoney = true, bool createNetworkEvent = false) { if (hires == null || hires.None()) { return false; } @@ -718,14 +727,16 @@ namespace Barotrauma if (nonDuplicateHires.None()) { return false; } - int total = HireManager.GetSalaryFor(nonDuplicateHires); - - if (!campaign.CanAfford(total)) { return false; } + if (takeMoney) + { + 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, Character.Controlled)) + if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci, takeMoney: takeMoney)) { atLeastOneHired = true; } @@ -951,8 +962,8 @@ namespace Barotrauma CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.GetIdentifierUsingOriginalName() == identifier); if (match != null) { - PendingHires.Add(match); AddPendingHire(match, createNetworkMessage: false); + System.Diagnostics.Debug.Assert(PendingHires.Contains(match)); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index cc59e8d2b..a9cb78909 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using Barotrauma.IO; @@ -641,19 +641,9 @@ namespace Barotrauma sprite?.Draw(spriteBatch, PlayerInput.MousePosition, scale: Math.Min(64 / sprite.size.X, 64 / sprite.size.Y) * Scale); break; } - case ItemAssemblyPrefab iPrefab: + case ItemAssemblyPrefab itemAssemblyPrefab: { - var (x, y) = PlayerInput.MousePosition; - foreach (var pair in iPrefab.DisplayEntities) - { - Rectangle dRect = pair.Item2; - dRect = new Rectangle(x: (int)(dRect.X * iPrefab.Scale + x), - y: (int)(dRect.Y * iPrefab.Scale - y), - width: (int)(dRect.Width * iPrefab.Scale), - height: (int)(dRect.Height * iPrefab.Scale)); - MapEntityPrefab prefab = MapEntityPrefab.Find("", pair.Item1); - prefab.DrawPlacing(spriteBatch, dRect, prefab.Scale * iPrefab.Scale); - } + itemAssemblyPrefab.Draw(spriteBatch, PlayerInput.MousePosition.FlipY()); break; } } @@ -716,7 +706,7 @@ namespace Barotrauma spriteBatch.Draw(backgroundSprite.Texture, area.Center.ToVector2() + pos, - null, color, 0.0f, backgroundSprite.size / 2, + backgroundSprite.SourceRect, color, 0.0f, backgroundSprite.size / 2, scale, spriteEffects, 0.0f); } @@ -1050,12 +1040,31 @@ namespace Barotrauma { return dragHandle.Dragging ? CursorState.Dragging : CursorState.Hand; } + //do not show the hover cursor when the cursor is on a listbox (on the listbox itself, not any of elements inside it!) + if (c is GUIListBox && (parent == null || parent == c)) + { + return CursorState.Default; + } // Some parent elements take priority // but not when the child is a GUIButton or GUITickBox - if (!(parent is GUIButton) && !(parent is GUIListBox) || + if (parent is not GUIButton && parent is not GUIListBox || (c is GUIButton) || (c is GUITickBox)) { - if (!c.Rect.Equals(monitorRect)) { return c.HoverCursor; } + if (!c.Rect.Equals(monitorRect)) + { + if (c is GUITickBox) + { + //tickboxes have some special logic: not all of the component is hoverable (just the box and the text area) + if (c.State is GUIComponent.ComponentState.Hover or GUIComponent.ComponentState.HoverSelected) + { + return c.HoverCursor; + } + } + else + { + return c.HoverCursor; + } + } } } @@ -2399,30 +2408,31 @@ namespace Barotrauma } iterations++; } - - static Vector2 ClampMoveAmount(Rectangle Rect, Rectangle clampTo, Vector2 moveAmount) - { - if (Rect.Y < clampTo.Y) - { - moveAmount.Y = Math.Max(moveAmount.Y, 0.0f); - } - else if (Rect.Bottom > clampTo.Bottom) - { - moveAmount.Y = Math.Min(moveAmount.Y, 0.0f); - } - if (Rect.X < clampTo.X) - { - moveAmount.X = Math.Max(moveAmount.X, 0.0f); - } - else if (Rect.Right > clampTo.Right) - { - moveAmount.X = Math.Min(moveAmount.X, 0.0f); - } - return moveAmount; - } } -#endregion + private static Vector2 ClampMoveAmount(Rectangle rect, Rectangle clampTo, Vector2 moveAmount) + { + if (rect.Y < clampTo.Y) + { + moveAmount.Y = Math.Max(moveAmount.Y, 0.0f); + } + else if (rect.Bottom > clampTo.Bottom) + { + moveAmount.Y = Math.Min(moveAmount.Y, 0.0f); + } + if (rect.X < clampTo.X) + { + moveAmount.X = Math.Max(moveAmount.X, 0.0f); + } + else if (rect.Right > clampTo.Right) + { + moveAmount.X = Math.Min(moveAmount.X, 0.0f); + } + return moveAmount; + } + + + #endregion #region Misc public static void TogglePauseMenu() @@ -2477,7 +2487,7 @@ namespace Barotrauma GameMain.GameSession.LoadPreviousSave(); }); - if (IsFriendlyOutpostLevel()) + if (IsFriendlyOutpostLevel() && !spMode.CrewDead) { CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToMainMenuVerification", action: () => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index d26b86738..0be5b3a17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Collections.Generic; using System.Linq; @@ -734,7 +734,7 @@ namespace Barotrauma DrawToolTip(spriteBatch, ToolTip, Rect); } - public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos) + public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos, Color? textColor = null, Color? backgroundColor = null) { if (ObjectiveManager.ContentRunning) { return; } @@ -745,6 +745,8 @@ namespace Barotrauma if (toolTipBlock == null || (RichString)toolTipBlock.UserData != toolTip) { toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), toolTip, font: GUIStyle.SmallFont, wrap: true, style: "GUIToolTip"); + if (textColor != null) { toolTipBlock.TextColor = textColor.Value; } + if (backgroundColor != null) { toolTipBlock.Color = backgroundColor.Value; } toolTipBlock.RectTransform.NonScaledSize = new Point( (int)(GUIStyle.SmallFont.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), (int)(GUIStyle.SmallFont.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); @@ -754,6 +756,15 @@ namespace Barotrauma toolTipBlock.RectTransform.AbsoluteOffset = pos.ToPoint(); toolTipBlock.SetTextPos(); + if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10) + { + toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width,0); + } + if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10) + { + toolTipBlock.RectTransform.AbsoluteOffset -= new Point(0, toolTipBlock.Rect.Height); + } + toolTipBlock.DrawManually(spriteBatch); } @@ -982,6 +993,24 @@ namespace Barotrauma } } + /// + /// Sets the minimum height of the transfrom to equal to the sum of the minimum heights of the children + /// (i.e. makes the element at least large enough to fit all the children vertically) + /// + public void InheritTotalChildrenMinHeight() + { + RectTransform.InheritTotalChildrenMinHeight(); + } + + /// + /// Sets the minimum height of the transfrom to equal to the sum of the heights of the children + /// (i.e. makes the element at least large enough to fit all the children vertically) + /// + public void InheritTotalChildrenHeight() + { + RectTransform.InheritTotalChildrenHeight(); + } + public static GUIComponent FromXML(ContentXElement element, RectTransform parent) { GUIComponent component = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 488f9a83d..019f31d92 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -15,20 +15,73 @@ namespace Barotrauma public GUITextBox TextBox { get; private set; } + public override RichString ToolTip + { + get + { + return base.ToolTip; + } + set + { + base.ToolTip = value; + TextBox.ToolTip = value; + } + } + public GUIButton PlusButton { get; private set; } public GUIButton MinusButton { get; private set; } + public enum ButtonVisibility { Automatic, Manual, ForceVisible, ForceHidden } + private ButtonVisibility _plusMinusButtonVisibility; + /// + /// Whether or not the default +- buttons should be shown. Defaults to Automatic, + /// which enables it for all integers and for those floats that have a defined + /// range, because for these it is implicitly more obvious how to increment them. + /// + public ButtonVisibility PlusMinusButtonVisibility + { + get { return _plusMinusButtonVisibility; } + set + { + if (_plusMinusButtonVisibility != value) + { + _plusMinusButtonVisibility = value; + UpdatePlusMinusButtonVisibility(); + } + } + } + private void UpdatePlusMinusButtonVisibility() { - if (ForceShowPlusMinusButtons - || inputType == NumberType.Int - || (inputType == NumberType.Float && MinValueFloat > float.MinValue && MaxValueFloat < float.MaxValue)) + switch (PlusMinusButtonVisibility) { - ShowPlusMinusButtons(); - } - else - { - HidePlusMinusButtons(); + case ButtonVisibility.ForceHidden: + { + HidePlusMinusButtons(); + break; + } + case ButtonVisibility.ForceVisible: + { + ShowPlusMinusButtons(); + break; + } + case ButtonVisibility.Automatic: + { + if (inputType == NumberType.Int + || (inputType == NumberType.Float + && MinValueFloat > float.MinValue + && MaxValueFloat < float.MaxValue)) + { + ShowPlusMinusButtons(); + } + else + { + HidePlusMinusButtons(); + } + break; + } + case ButtonVisibility.Manual: + return; } } @@ -86,19 +139,6 @@ 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 { @@ -162,6 +202,17 @@ namespace Barotrauma } } } + + public bool Readonly + { + get { return TextBox.Readonly; } + set + { + TextBox.Readonly = value; + PlusButton.Enabled = !value; + MinusButton.Enabled = !value; + } + } public override GUIFont Font { @@ -189,11 +240,19 @@ namespace Barotrauma public float ValueStep; + // Enable holding to scroll through values faster private float pressedTimer; private readonly float pressedDelay = 0.5f; private bool IsPressedTimerRunning { get { return pressedTimer > 0; } } - public GUINumberInput(RectTransform rectT, NumberType inputType, string style = "", Alignment textAlignment = Alignment.Center, float? relativeButtonAreaWidth = null, bool hidePlusMinusButtons = false) : base(style, rectT) + public GUINumberInput( + RectTransform rectT, + NumberType inputType, + string style = "", + Alignment textAlignment = Alignment.Center, + float? relativeButtonAreaWidth = null, + ButtonVisibility buttonVisibility = ButtonVisibility.Automatic, + (GUIButton PlusButton, GUIButton MinusButton)? customPlusMinusButtons = null) : base(style, rectT) { LayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, rectT), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; @@ -233,9 +292,23 @@ namespace Barotrauma return true; }; - var buttonArea = new GUIFrame(new RectTransform(new Vector2(_relativeButtonAreaWidth, 1.0f), LayoutGroup.RectTransform, Anchor.CenterRight), style: null); - PlusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform), style: null); - GUIStyle.Apply(PlusButton, "PlusButton", this); + if (customPlusMinusButtons.HasValue) + { + PlusButton = customPlusMinusButtons.Value.PlusButton; + MinusButton = customPlusMinusButtons.Value.MinusButton; + } + else // generate the default +- buttons + { + var buttonArea = new GUIFrame(new RectTransform(new Vector2(_relativeButtonAreaWidth, 1.0f), LayoutGroup.RectTransform, Anchor.CenterRight), style: null); + + PlusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform), style: null); + GUIStyle.Apply(PlusButton, "PlusButton", this); + + MinusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform, Anchor.BottomRight), style: null); + GUIStyle.Apply(MinusButton, "MinusButton", this); + } + + // Set up default and custom +- buttons the same way to ensure uniform functionality PlusButton.ClickSound = GUISoundType.Increase; PlusButton.OnButtonDown += () => { @@ -255,9 +328,6 @@ namespace Barotrauma } return true; }; - - MinusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform, Anchor.BottomRight), style: null); - GUIStyle.Apply(MinusButton, "MinusButton", this); MinusButton.ClickSound = GUISoundType.Decrease; MinusButton.OnButtonDown += () => { @@ -278,10 +348,7 @@ namespace Barotrauma return true; }; - if (inputType != NumberType.Int || hidePlusMinusButtons) - { - HidePlusMinusButtons(); - } + PlusMinusButtonVisibility = buttonVisibility; if (inputType == NumberType.Int) { @@ -324,16 +391,16 @@ namespace Barotrauma private void HidePlusMinusButtons() { - PlusButton.Parent.Visible = false; - PlusButton.Parent.IgnoreLayoutGroups = true; + PlusButton.Parent.Visible = MinusButton.Parent.Visible = false; + PlusButton.Parent.IgnoreLayoutGroups = MinusButton.Parent.IgnoreLayoutGroups = true; TextBox.RectTransform.RelativeSize = Vector2.One; LayoutGroup.Recalculate(); } private void ShowPlusMinusButtons() { - PlusButton.Parent.Visible = true; - PlusButton.Parent.IgnoreLayoutGroups = false; + PlusButton.Parent.Visible = MinusButton.Parent.Visible = true; + PlusButton.Parent.IgnoreLayoutGroups = MinusButton.Parent.IgnoreLayoutGroups = false; TextBox.RectTransform.RelativeSize = new Vector2(1.0f - PlusButton.Parent.RectTransform.RelativeSize.X, 1.0f); LayoutGroup.Recalculate(); } @@ -427,6 +494,11 @@ namespace Barotrauma Math.Min(floatValue, MaxValueFloat.Value); PlusButton.Enabled = WrapAround || floatValue < MaxValueFloat; } + + if (Readonly) + { + PlusButton.Enabled = MinusButton.Enabled = false; + } } private void ClampIntValue() @@ -441,8 +513,16 @@ namespace Barotrauma intValue = WrapAround && MinValueInt.HasValue ? MinValueInt.Value : Math.Min(intValue, MaxValueInt.Value); UpdateText(); } - PlusButton.Enabled = WrapAround || MaxValueInt == null || intValue < MaxValueInt; - MinusButton.Enabled = WrapAround || MinValueInt == null || intValue > MinValueInt; + + if (Readonly) + { + PlusButton.Enabled = MinusButton.Enabled = false; + } + else + { + PlusButton.Enabled = WrapAround || MaxValueInt == null || intValue < MaxValueInt; + MinusButton.Enabled = WrapAround || MinValueInt == null || intValue > MinValueInt; + } } private void UpdateText() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index 3080c482e..2486e7833 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -135,7 +135,8 @@ namespace Barotrauma if (subElement.NameAsIdentifier() != "override") { continue; } if (ScalableFont.ExtractShccFromXElement(subElement).HasFlag(flag)) { - return new ScalableFont(subElement, font?.Size ?? 14, GameMain.Instance.GraphicsDevice); + uint overrideFontSize = GetFontSize(subElement, defaultSize: font?.Size ?? 14); + return new ScalableFont(subElement, overrideFontSize, GameMain.Instance.GraphicsDevice); } } @@ -380,6 +381,11 @@ namespace Barotrauma public static implicit operator UISprite?(GUISprite reference) => reference.Value; + public void Draw(SpriteBatch spriteBatch, RectangleF rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None) + { + Value?.Draw(spriteBatch, rect, color, spriteEffects); + } + public void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None) { Value?.Draw(spriteBatch, rect, color, spriteEffects); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs index 30ec4af6c..27bb90853 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs @@ -214,7 +214,7 @@ namespace Barotrauma Bar.HoverCursor = CursorState.Default; break; case "GUISlider": - HoverCursor = CursorState.Default; + HoverCursor = CursorState.Hand; Bar.HoverCursor = CursorState.Hand; break; default: diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUISelectionCarousel.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUISelectionCarousel.cs new file mode 100644 index 000000000..069c50c80 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUISelectionCarousel.cs @@ -0,0 +1,191 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + /// + /// Allows accessing the element selected in the carousel in contexts where the type of T isn't known. + /// Pretty hacky, but I could not think of a better way to do this ( in which this is used). + /// + public interface IGUISelectionCarouselAccessor + { + object? GetSelectedElement(); + void SelectElement(object? value); + } + + /// + /// An UI element that allows toggling through a set of options with buttons to the left and right + /// + public class GUISelectionCarousel : GUIComponent, IGUISelectionCarouselAccessor + { + public record class Element(T value, LocalizedString text, LocalizedString toolTip); + + public delegate void OnValueChangedHandler(GUISelectionCarousel carousel); + public OnValueChangedHandler? OnValueChanged; + + public GUITextBlock TextBlock { get; private set; } + + public GUIButton RightButton { get; private set; } + public GUIButton LeftButton { get; private set; } + + private readonly List elements = new List(); + + private readonly GUILayoutGroup layoutGroup; + + public Element? SelectedElement { get; private set; } + public T? SelectedValue => SelectedElement == null ? default : SelectedElement.value; + public LocalizedString SelectedText => SelectedElement?.text ?? string.Empty; + + public override bool Enabled + { + get => base.Enabled; + set + { + base.Enabled = RightButton.Enabled = LeftButton.Enabled = TextBlock.Enabled = value; + } + } + + + public override Color Color + { + get { return color; } + set + { + color = value; + TextBlock.Color = color; + } + } + + public Color TextColor + { + get { return TextBlock.TextColor; } + set { TextBlock.TextColor = value; } + } + + public override Color HoverColor + { + get => base.HoverColor; + set + { + base.HoverColor = value; + TextBlock.HoverColor = value; + } + } + + public GUISelectionCarousel(RectTransform rectT, string style = "", params (T value, LocalizedString text)[] newElements) : base(style, rectT) + { + layoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, rectT), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + LeftButton = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), layoutGroup.RectTransform), style: "GUIButtonToggleLeft"); + GUIStyle.Apply(LeftButton, "LeftButton", this); + TextBlock = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), layoutGroup.RectTransform), "", textAlignment: Alignment.Center, style: "GUITextBox"); + GUIStyle.Apply(TextBlock, "TextBlock", this); + RightButton = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), layoutGroup.RectTransform), style: "GUIButtonToggleRight"); + GUIStyle.Apply(RightButton, "RightButton", this); + + RightButton.OnClicked += (btn, userData) => + { + if (elements.Count < 2) { return false; } + if (SelectedElement == null) + { + SelectElement(elements.First()); + } + else + { + int newIndex = (elements.IndexOf(SelectedElement) + 1) % elements.Count; + SelectElement(elements[newIndex]); + } + return true; + }; + LeftButton.OnClicked += (btn, userData) => + { + if (elements.Count < 2) { return false; } + if (SelectedElement == null) + { + SelectElement(elements.First()); + } + else + { + int newIndex = MathUtils.PositiveModulo((elements.IndexOf(SelectedElement) - 1), elements.Count); + SelectElement(elements[newIndex]); + } + return true; + }; + + if (newElements != null && newElements.Any()) + { + SetElements(newElements); + } + } + + public object? GetSelectedElement() + { + return SelectedValue; + } + + /// + /// Select the element whose value matches the specified value. If null, deselects the currently selected element. + /// + public void SelectElement(object? value) + { + if (value == null) + { + SelectElement(null); + return; + } + if (elements.FirstOrDefault(e => value.Equals(e.value)) is { } element) + { + SelectElement(element); + } + } + + public void SelectElement(Element? element) + { + SelectedElement = element; + TextBlock.Text = element?.text ?? string.Empty; + TextBlock.ToolTip = element?.toolTip ?? string.Empty; + OnValueChanged?.Invoke(this); + } + + /// + /// Clears all existing elements from the carousels and adds the specified new elements to it + /// + public void SetElements(params (T value, LocalizedString text)[] elements) + { + this.elements.Clear(); + foreach ((T value, LocalizedString text) in elements) + { + AddElement(value, text); + } + } + + /// + /// Clears all existing elements from the carousels and adds the specified new elements to it + /// + public void SetElements(params (T value, LocalizedString text, LocalizedString toolTip)[] elements) + { + this.elements.Clear(); + foreach ((T value, LocalizedString text, LocalizedString toolTip) in elements) + { + AddElement(value, text, toolTip); + } + } + + public void AddElement(T value, LocalizedString text, LocalizedString? tooltip = null) + { + var newElement = new Element(value, text, tooltip ?? string.Empty); + elements.Add(newElement); + if (SelectedElement == null) + { + SelectElement(newElement); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index f6e5612d9..ffb851a97 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -1,6 +1,6 @@ -using System; -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; +using System; using System.Collections.Immutable; using System.Linq; using System.Reflection; @@ -47,6 +47,8 @@ namespace Barotrauma public readonly static GUISprite SubmarineLocationIcon = new GUISprite("SubmarineLocationIcon"); public readonly static GUISprite Arrow = new GUISprite("Arrow"); public readonly static GUISprite SpeechBubbleIcon = new GUISprite("SpeechBubbleIcon"); + public readonly static GUISprite SpeechBubbleIconSliced = new GUISprite("SpeechBubbleIconSliced"); + public readonly static GUISprite InteractionLabelBackground = new GUISprite("InteractionLabelBackground"); public readonly static GUISprite BrokenIcon = new GUISprite("BrokenIcon"); public readonly static GUISprite YouAreHereCircle = new GUISprite("YouAreHereCircle"); @@ -124,6 +126,9 @@ namespace Barotrauma 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)); + + public readonly static GUIColor InteractionLabelColor = new GUIColor("InteractionLabelColor", new Color(255, 255, 255, 255)); + public readonly static GUIColor InteractionLabelHoverColor = new GUIColor("InteractionLabelHoverColor", new Color(0, 255, 255, 255)); // Inventory public readonly static GUIColor EquipmentSlotIconColor = new GUIColor("EquipmentSlotIconColor", new Color(99, 70, 64, 255)); @@ -195,8 +200,7 @@ namespace Barotrauma if (parentStyle == null) { - Identifier parentStyleName = parent.GetType().Name.ToIdentifier(); - + Identifier parentStyleName = ReflectionUtils.GetTypeNameWithoutGenericArity(parent.GetType()); if (!ComponentStyles.ContainsKey(parentStyleName)) { DebugConsole.ThrowError($"Couldn't find a GUI style \"{parentStyleName}\""); @@ -204,7 +208,7 @@ namespace Barotrauma } parentStyle = ComponentStyles[parentStyleName]; } - Identifier childStyleName = styleName.IsEmpty ? targetComponent.GetType().Name.ToIdentifier() : styleName; + Identifier childStyleName = styleName.IsEmpty ? ReflectionUtils.GetTypeNameWithoutGenericArity(targetComponent.GetType()) : styleName; parentStyle.ChildStyles.TryGetValue(childStyleName, out componentStyle); } else @@ -212,7 +216,7 @@ namespace Barotrauma Identifier styleIdentifier = styleName.ToIdentifier(); if (styleIdentifier == Identifier.Empty) { - styleIdentifier = targetComponent.GetType().Name.ToIdentifier(); + styleIdentifier = ReflectionUtils.GetTypeNameWithoutGenericArity(targetComponent.GetType()); } if (!ComponentStyles.ContainsKey(styleIdentifier)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 600d92bb5..7dca6fcbc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -143,6 +143,9 @@ namespace Barotrauma } } + /// + /// When enabled, clips the left side of the text if it's too long to fit in the box (i.e. allows you to enter longer texts without the text overflowing from the box). + /// public bool OverflowClip { get { return textBlock.OverflowClip; } @@ -325,7 +328,7 @@ namespace Barotrauma textBlock.Text = text; ClearSelection(); if (Text == null) textBlock.Text = ""; - if (Text != "" && !Wrap) + if (Text != "") { if (maxTextLength != null) { @@ -334,7 +337,7 @@ namespace Barotrauma textBlock.Text = Text.Substring(0, (int)maxTextLength); } } - else + else if (!Wrap) { while (ClampText && textBlock.Text.Length > 0 && Font.MeasureString(textBlock.Text).X * TextBlock.TextScale > (int)(textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs index cc7359b59..a1a5f5b47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; namespace Barotrauma @@ -198,7 +198,9 @@ namespace Barotrauma base.Update(deltaTime); - if (GUI.MouseOn == this && Enabled) + if (GUI.MouseOn == this && Enabled && + //allow clicking on the text area, but not further to the right (the dimensions of the component itself can extend further than the text) + PlayerInput.MousePosition.X < Rect.X + ContentWidth) { State = Selected ? ComponentState.HoverSelected : diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 06a9b05a4..c92b557c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -779,6 +779,24 @@ namespace Barotrauma NonScaledSize = targetSize; yield return CoroutineStatus.Success; } + + /// + /// Sets the minimum height of the transfrom to equal to the sum of the minimum heights of the children + /// (i.e. makes the rect at least large enough to fit all the children vertically) + /// + public void InheritTotalChildrenMinHeight() + { + MinSize = new Point(MinSize.X, children.Sum(c => c.MinSize.Y)); + } + + /// + /// Sets the minimum height of the transfrom to equal to the sum of the heights of the children + /// (i.e. makes the rect at least large enough to fit all the children vertically) + /// + public void InheritTotalChildrenHeight() + { + MinSize = new Point(MinSize.X, children.Sum(c => c.Rect.Height)); + } #endregion #region Static methods diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 5264b13b5..daceed791 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -1733,6 +1733,9 @@ namespace Barotrauma if (!subItem.Components.All(c => c is not Holdable h || !h.Attachable || !h.Attached)) { continue; } if (!subItem.Components.All(c => c is not Wire w || w.Connections.All(c => c == null))) { continue; } if (!ItemAndAllContainersInteractable(subItem)) { continue; } + //don't list items in a character inventory (the ones in a crew member's inventory are counted below) + var rootInventoryOwner = subItem.GetRootInventoryOwner(); + if (rootInventoryOwner != null) { continue; } AddOwnedItem(subItem); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 4e78f979b..b132d18f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1066,7 +1066,7 @@ namespace Barotrauma GUIButton centerButton = new GUIButton(new RectTransform(new Vector2(1f), centerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight, anchor: Anchor.Center), style: "GUIButtonTransferArrow"); GUILayoutGroup inputLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedTransferMenuLayout.RectTransform), childAnchor: Anchor.Center); - GUINumberInput transferAmountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), inputLayout.RectTransform), NumberType.Int, hidePlusMinusButtons: true) + GUINumberInput transferAmountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), inputLayout.RectTransform), NumberType.Int, buttonVisibility: GUINumberInput.ButtonVisibility.ForceHidden) { MinValueInt = 0 }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index 6f440255e..aa8ab3f3a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -131,7 +131,7 @@ namespace Barotrauma GUIFrame characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null) { Visible = false }; GUILayoutGroup characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform)); GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null); - GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null); + GUILayoutGroup playerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), containerFrame.RectTransform, Anchor.TopCenter)); GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), @@ -423,7 +423,7 @@ namespace Barotrauma GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null); GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) { - ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)), + ToolTip = CreateTooltip(talent, characterInfo), UserData = talent.Identifier, PressedColor = pressedColor, Enabled = info.Character != null, @@ -489,6 +489,24 @@ namespace Barotrauma }, }; + static RichString CreateTooltip(TalentPrefab talent, CharacterInfo? character) + { + LocalizedString progress = string.Empty; + + if (character is not null && talent.TrackedStat.TryUnwrap(out var stat)) + { + var statValue = character.GetSavedStatValue(StatTypes.None, stat.PermanentStatIdentifier); + var intValue = (int)MathF.Round(statValue); + progress = "\n\n"; + progress += statValue < stat.Max + ? TextManager.GetWithVariables("talentprogress", ("[amount]", intValue.ToString()), ("[max]", stat.Max.ToString())) + : TextManager.Get("talentprogresscompleted"); + } + + RichString tooltip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖\n\n{ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)}{progress}"); + return tooltip; + } + talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent; GUIComponent iconImage; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 3a8e2cb68..192fbe7d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -1305,7 +1305,7 @@ namespace Barotrauma const int maxUpgrades = 4; Item? item = entity as Item; - itemName.Text = item?.Name ?? TextManager.Get("upgradecategory.walls"); + itemName.Text = item?.Prefab.Name ?? TextManager.Get("upgradecategory.walls"); if (slotIndex > -1) { itemName.Text = TextManager.GetWithVariables("weaponslotwithname", ("[number]", slotIndex.ToString()), ("[weaponname]", itemName.Text)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs index a7a5216d2..b1d25586c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs @@ -7,46 +7,46 @@ using Barotrauma.Extensions; namespace Barotrauma { - class Widget + public enum WidgetShape { - public enum Shape - { - Rectangle, - Circle, - Cross - } + Rectangle, + Circle, + Cross + } - public Shape shape; - public LocalizedString tooltip; - public bool showTooltip = true; - public Rectangle DrawRect => new Rectangle((int)(DrawPos.X - (float)size / 2), (int)(DrawPos.Y - (float)size / 2), size, size); + internal class Widget + { + public WidgetShape Shape; + public LocalizedString Tooltip; + public bool ShowTooltip = true; + public Rectangle DrawRect => new Rectangle((int)(DrawPos.X - (float)Size / 2), (int)(DrawPos.Y - (float)Size / 2), Size, Size); public Rectangle InputRect { get { var inputRect = DrawRect; - inputRect.Inflate(inputAreaMargin, inputAreaMargin); + inputRect.Inflate(InputAreaMargin, InputAreaMargin); return inputRect; } } public Vector2 DrawPos { get; set; } - public int size = 10; - public float thickness = 1f; + public int Size = 10; + public float Thickness = 1f; /// /// Used only for circles. /// - public int sides = 40; + public int Sides = 40; /// /// Currently used only for rectangles. /// - public bool isFilled; - public int inputAreaMargin; - public Color color = GUIStyle.Red; - public Color? secondaryColor; - public Color textColor = Color.White; - public Color textBackgroundColor = Color.Black * 0.5f; - public readonly string id; + public bool IsFilled; + public int InputAreaMargin; + public Color Color = GUIStyle.Red; + public Color? SecondaryColor; + public Color TextColor = Color.White; + public Color TextBackgroundColor = Color.Black * 0.5f; + public readonly string Id; public event Action Selected; public event Action Deselected; @@ -61,11 +61,11 @@ namespace Barotrauma public bool RequireMouseOn = true; - public Action refresh; + public Action Refresh; - public object data; + public object Data; - public bool IsSelected => enabled && selectedWidgets.Contains(this); + public bool IsSelected => enabled && SelectedWidgets.Contains(this); public bool IsControlled => IsSelected && PlayerInput.PrimaryMouseButtonHeld(); public bool IsMouseOver => GUI.MouseOn == null && InputRect.Contains(PlayerInput.MousePosition); private bool enabled = true; @@ -75,9 +75,9 @@ namespace Barotrauma set { enabled = value; - if (!enabled && selectedWidgets.Contains(this)) + if (!enabled && SelectedWidgets.Contains(this)) { - selectedWidgets.Remove(this); + SelectedWidgets.Remove(this); } } } @@ -89,46 +89,46 @@ namespace Barotrauma set { multiselect = value; - if (!multiselect && selectedWidgets.Multiple()) + if (!multiselect && SelectedWidgets.Multiple()) { - selectedWidgets = selectedWidgets.Take(1).ToList(); + SelectedWidgets = SelectedWidgets.Take(1).ToList(); } } } - public Vector2? tooltipOffset; + public Vector2? TooltipOffset; - public Widget linkedWidget; + public Widget LinkedWidget; - public static List selectedWidgets = new List(); + public static List SelectedWidgets = new List(); - public Widget(string id, int size, Shape shape) + public Widget(string id, int size, WidgetShape shape) { - this.id = id; - this.size = size; - this.shape = shape; + Id = id; + Size = size; + Shape = shape; } public virtual void Update(float deltaTime) { PreUpdate?.Invoke(deltaTime); if (!enabled) { return; } - if (IsMouseOver || (!RequireMouseOn && selectedWidgets.Contains(this) && PlayerInput.PrimaryMouseButtonHeld())) + if (IsMouseOver || (!RequireMouseOn && SelectedWidgets.Contains(this) && PlayerInput.PrimaryMouseButtonHeld())) { Hovered?.Invoke(); System.Diagnostics.Debug.WriteLine("hovered"); if (RequireMouseOn || PlayerInput.PrimaryMouseButtonDown()) { - if ((multiselect && !selectedWidgets.Contains(this)) || selectedWidgets.None()) + if ((multiselect && !SelectedWidgets.Contains(this)) || SelectedWidgets.None()) { - selectedWidgets.Add(this); + SelectedWidgets.Add(this); Selected?.Invoke(); } } } - else if (selectedWidgets.Contains(this)) + else if (SelectedWidgets.Contains(this)) { System.Diagnostics.Debug.WriteLine("selectedWidgets.Contains(this) -> remove"); - selectedWidgets.Remove(this); + SelectedWidgets.Remove(this); Deselected?.Invoke(); } if (IsSelected) @@ -153,40 +153,40 @@ namespace Barotrauma { PreDraw?.Invoke(spriteBatch, deltaTime); var drawRect = DrawRect; - switch (shape) + switch (Shape) { - case Shape.Rectangle: - if (secondaryColor.HasValue) + case WidgetShape.Rectangle: + if (SecondaryColor.HasValue) { - GUI.DrawRectangle(spriteBatch, drawRect, secondaryColor.Value, isFilled, thickness: 2); + GUI.DrawRectangle(spriteBatch, drawRect, SecondaryColor.Value, IsFilled, thickness: 2); } - GUI.DrawRectangle(spriteBatch, drawRect, color, isFilled, thickness: IsSelected ? (int)(thickness * 3) : (int)thickness); + GUI.DrawRectangle(spriteBatch, drawRect, Color, IsFilled, thickness: IsSelected ? (int)(Thickness * 3) : (int)Thickness); break; - case Shape.Circle: - if (secondaryColor.HasValue) + case WidgetShape.Circle: + if (SecondaryColor.HasValue) { - ShapeExtensions.DrawCircle(spriteBatch, DrawPos, size / 2, sides, secondaryColor.Value, thickness: 2); + ShapeExtensions.DrawCircle(spriteBatch, DrawPos, Size / 2, Sides, SecondaryColor.Value, thickness: 2); } - ShapeExtensions.DrawCircle(spriteBatch, DrawPos, size / 2, sides, color, thickness: IsSelected ? 3 : 1); + ShapeExtensions.DrawCircle(spriteBatch, DrawPos, Size / 2, Sides, Color, thickness: IsSelected ? 3 : 1); break; - case Shape.Cross: - float halfSize = size / 2; - if (secondaryColor.HasValue) + case WidgetShape.Cross: + float halfSize = Size / 2; + if (SecondaryColor.HasValue) { - GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitY * halfSize, DrawPos - Vector2.UnitY * halfSize, secondaryColor.Value, width: 2); - GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitX * halfSize, DrawPos - Vector2.UnitX * halfSize, secondaryColor.Value, width: 2); + GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitY * halfSize, DrawPos - Vector2.UnitY * halfSize, SecondaryColor.Value, width: 2); + GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitX * halfSize, DrawPos - Vector2.UnitX * halfSize, SecondaryColor.Value, width: 2); } - GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitY * halfSize, DrawPos - Vector2.UnitY * halfSize, color, width: IsSelected ? 3 : 1); - GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitX * halfSize, DrawPos - Vector2.UnitX * halfSize, color, width: IsSelected ? 3 : 1); + GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitY * halfSize, DrawPos - Vector2.UnitY * halfSize, Color, width: IsSelected ? 3 : 1); + GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitX * halfSize, DrawPos - Vector2.UnitX * halfSize, Color, width: IsSelected ? 3 : 1); break; - default: throw new NotImplementedException(shape.ToString()); + default: throw new NotImplementedException(Shape.ToString()); } if (IsSelected) { - if (showTooltip && !tooltip.IsNullOrEmpty()) + if (ShowTooltip && !Tooltip.IsNullOrEmpty()) { - var offset = tooltipOffset ?? new Vector2(size, -size / 2f); - GUI.DrawString(spriteBatch, DrawPos + offset, tooltip, textColor, textBackgroundColor); + var offset = TooltipOffset ?? new Vector2(Size, -Size / 2f); + GUIComponent.DrawToolTip(spriteBatch, Tooltip, DrawPos + offset, TextColor, TextBackgroundColor); } } PostDraw?.Invoke(spriteBatch, deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 57dd6dea9..ae9476740 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -1,4 +1,4 @@ -using Barotrauma.IO; +using Barotrauma.IO; using Barotrauma.Media; using Barotrauma.Networking; using Barotrauma.Particles; @@ -131,7 +131,7 @@ namespace Barotrauma /// public event Action ResolutionChanged; - private bool exiting; + public static bool IsExiting { get; private set; } public static bool IsFirstLaunch { @@ -175,7 +175,7 @@ namespace Barotrauma { try { - return Instance != null && !Instance.exiting && Instance.IsActive; + return Instance != null && !IsExiting && Instance.IsActive; } catch (NullReferenceException) { @@ -462,7 +462,15 @@ namespace Barotrauma { Thread.Sleep((int)(Timing.Step * 1000)); } + LanguageIdentifier selectedLanguage = GameSettings.CurrentConfig.Language; + //unload text files at this point - we only loaded for the purposes of the language selection screen, + //they will be loaded "normally" with the rest of the files later ContentPackageManager.VanillaCorePackage.UnloadFilesOfType(); + //the selected language got unloaded, need to reselect it + var config = GameSettings.CurrentConfig; + config.Language = selectedLanguage; + GameSettings.SetCurrentConfig(config); + GameSettings.SaveCurrentConfig(); } SoundManager = new Sounds.SoundManager(); @@ -485,8 +493,10 @@ namespace Barotrauma .Select(p => p.Result).Successes()) { const float min = 1f, max = 70f; + if (IsExiting) { break; } TitleScreen.LoadState = MathHelper.Lerp(min, max, progress); } + if (IsExiting) { return; } var corePackage = ContentPackageManager.EnabledPackages.Core; if (corePackage.EnableError.TryUnwrap(out var error)) @@ -578,6 +588,8 @@ namespace Barotrauma MainMenuScreen.Select(); + ContainerTagPrefab.CheckForContainerTagErrors(); + foreach (Identifier steamError in SteamManager.InitializationErrors) { new GUIMessageBox(TextManager.Get("Error"), TextManager.Get(steamError)); @@ -1205,7 +1217,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), linkHolder.RectTransform), TextManager.Get("bugreportgithubform"), style: "MainMenuGUIButton", textAlignment: Alignment.Left) { - UserData = "https://github.com/Regalis11/Barotrauma/issues/new/choose", + UserData = "https://github.com/FakeFishGames/Barotrauma/discussions/new?category=bug-reports", OnClicked = (btn, userdata) => { ShowOpenUriPrompt(userdata as string); @@ -1229,7 +1241,7 @@ namespace Barotrauma protected override void OnExiting(object sender, EventArgs args) { - exiting = true; + IsExiting = true; CreatureMetrics.Save(); DebugConsole.NewMessage("Exiting..."); Client?.Quit(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index b8966494d..5f5862e60 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -157,6 +157,7 @@ namespace Barotrauma Character.Controlled.Info.Name, msg, messageType, Character.Controlled); + Character.Controlled.ShowSpeechBubble(ChatMessage.MessageColor[(int)messageType], text); if (messageType == ChatMessageType.Radio && headset != null) { Signal s = new Signal(msg, sender: Character.Controlled, source: headset.Item); @@ -620,7 +621,7 @@ namespace Barotrauma private void OnCrewListRearranged(GUIListBox crewList, object draggedElementData) { if (crewList != this.crewList) { return; } - if (!(draggedElementData is Character)) { return; } + if (draggedElementData is not Character) { return; } if (!IsSinglePlayer) { return; } if (crewList.HasDraggedElementIndexChanged) { @@ -645,7 +646,7 @@ namespace Barotrauma for (int i = 0; i < crewList.Content.CountChildren; i++) { var characterComponent = crewList.Content.GetChild(i); - if (!(characterComponent?.UserData is Character c)) { continue; } + if (characterComponent?.UserData is not Character c) { continue; } if (c.Info == null) { continue; } c.Info.CrewListIndex = i; } @@ -681,6 +682,7 @@ namespace Barotrauma { AddSinglePlayerChatMessage(senderName.Value, text.Value, messageType, sender); } + public void AddSinglePlayerChatMessage(string senderName, string text, ChatMessageType messageType, Character sender) { if (!IsSinglePlayer) @@ -798,12 +800,27 @@ namespace Barotrauma hull ??= order.OrderGiver.CurrentHull; AddOrder(order.WithTargetEntity(hull), order.FadeOutTime); } + if (order.IsDeconstructOrder) + { + if (order.TargetEntity is Item item) + { + if (order.Identifier == Tags.DeconstructThis) + { + Item.DeconstructItems.Add(item); + HintManager.OnItemMarkedForDeconstruction(order.OrderGiver); + } + else + { + Item.DeconstructItems.Remove(item); + } + } + } else if (order.IsIgnoreOrder) { WallSection ws = null; if (order.TargetType == Order.OrderTargetType.Entity && order.TargetEntity is IIgnorable ignorable) { - ignorable.OrderedToBeIgnored = order.Identifier == "ignorethis"; + ignorable.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis; AddOrder(order.Clone(), null); } else if (order.TargetType == Order.OrderTargetType.WallSection && order.TargetEntity is Structure s) @@ -812,7 +829,7 @@ namespace Barotrauma ws = s.GetSection(wallSectionIndex); if (ws != null) { - ws.OrderedToBeIgnored = order.Identifier == "ignorethis"; + ws.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis; AddOrder(order.WithWallSection(s, wallSectionIndex), null); } } @@ -835,7 +852,9 @@ namespace Barotrauma } if (IsSinglePlayer) { - order.OrderGiver?.Speak(order.GetChatMessage("", hull?.DisplayName?.Value, givingOrderToSelf: character == order.OrderGiver, isNewOrder: isNewOrder), ChatMessageType.Order); + order.OrderGiver?.Speak( + order.GetChatMessage("", hull?.DisplayName?.Value, givingOrderToSelf: character == order.OrderGiver, isNewOrder: isNewOrder), + ChatMessageType.Order); } else { @@ -1404,9 +1423,10 @@ namespace Barotrauma if (PlayerInput.KeyDown(InputType.Command) && (GUI.KeyboardDispatcher.Subscriber == null || (GUI.KeyboardDispatcher.Subscriber is GUIComponent component && (component == crewList || component.IsChildOf(crewList)))) && commandFrame == null && !clicklessSelectionActive && CanIssueOrders && !(GameMain.GameSession?.Campaign?.ShowCampaignUI ?? false) && - Character.Controlled?.SelectedItem?.Prefab is not { DisableCommandMenuWhenSelected: true }) + Character.Controlled?.SelectedItem?.Prefab is not { DisableCommandMenuWhenSelected: true } && + !Inventory.IsMouseOnInventory) { - if (PlayerInput.IsShiftDown()) + if (PlayerInput.KeyDown(InputType.ContextualCommand)) { CreateCommandUI(FindEntityContext(), true); } @@ -1535,14 +1555,14 @@ namespace Barotrauma { if (node.Keys != Keys.None && PlayerInput.KeyHit(node.Keys)) { - var b = node.Button as GUIButton; - if (PlayerInput.IsShiftDown() && b?.OnSecondaryClicked != null) + var button = node.Button; + if (PlayerInput.IsShiftDown() && button?.OnSecondaryClicked != null) { - b.OnSecondaryClicked.Invoke(node.Button as GUIButton, node.Button.UserData); + button.OnSecondaryClicked.Invoke(button, button.UserData); } else { - b?.OnClicked?.Invoke(node.Button as GUIButton, node.Button.UserData); + button?.OnClicked?.Invoke(button, button.UserData); } ResetNodeSelection(); hotkeyHit = true; @@ -1612,7 +1632,7 @@ namespace Barotrauma { foreach (var orderIcon in currentOrderIconList.Content.Children) { - if (!(orderIcon.UserData is Order order)) { continue; } + if (orderIcon.UserData is not Order order) { continue; } if (order.ColoredWhenControllingGiver && order.OrderGiver != Character.Controlled) { orderIcon.Color = AIObjective.ObjectiveIconColor; @@ -1701,7 +1721,7 @@ namespace Barotrauma bool foundMatch = false; foreach (var orderIcon in currentOrderIconList.Content.Children) { - if (!(orderIcon.GetChildByUserData("glow") is GUIComponent glowComponent)) { continue; } + if (orderIcon.GetChildByUserData("glow") is not GUIComponent glowComponent) { continue; } glowComponent.Color = orderIcon.Color; if (foundMatch) { @@ -1946,6 +1966,13 @@ namespace Barotrauma } } + public void OpenCommandUI(Entity entityContext = null, bool forceContextual = false) + { + CreateCommandUI(entityContext, forceContextual); + SoundPlayer.PlayUISound(GUISoundType.PopupMenu); + clicklessSelectionActive = isOpeningClick = true; + } + private void CreateCommandUI(Entity entityContext = null, bool forceContextual = false) { if (commandFrame != null) { DisableCommandUI(); } @@ -2018,7 +2045,7 @@ namespace Barotrauma new RectTransform(Vector2.One, startNode.RectTransform, anchor: Anchor.Center), (spriteBatch, _) => { - if (!(entityContext is Character character) || character?.Info == null) { return; } + if (entityContext is not Character character || character?.Info == null) { return; } var node = startNode; character.Info.DrawJobIcon(spriteBatch, new Rectangle((int)(node.Rect.X + node.Rect.Width * 0.5f), (int)(node.Rect.Y + node.Rect.Height * 0.1f), (int)(node.Rect.Width * 0.6f), (int)(node.Rect.Height * 0.8f))); @@ -2082,7 +2109,7 @@ namespace Barotrauma returnNodeMargin = returnNodeSize.X * 0.5f; nodeDistance = (int)(150 * GUI.Scale); - shorcutCenterNodeOffset = new Point(0, (int)(1.25f * nodeDistance)); + shorcutCenterNodeOffset = new Point(0, (int)(1.35f * nodeDistance)); } private List GetAvailableCategories() @@ -2115,7 +2142,7 @@ namespace Barotrauma if (centerNode == null || optionNodes == null) { return; } var startNodePos = centerNode.Rect.Center.ToVector2(); // Don't draw connectors for assignment nodes - if (!(optionNodes.FirstOrDefault()?.Button.UserData is Character)) + if (optionNodes.FirstOrDefault()?.Button.UserData is not Character) { // Regular option nodes if (targetFrame == null || !targetFrame.Visible) @@ -2194,7 +2221,7 @@ namespace Barotrauma private bool NavigateForward(GUIButton node, object userData) { if (commandFrame == null) { return false; } - if (!(optionNodes.Find(n => n.Button == node) is OptionNode optionNode) || !optionNodes.Remove(optionNode)) + if (optionNodes.Find(n => n.Button == node) is not OptionNode optionNode || !optionNodes.Remove(optionNode)) { shortcutNodes.Remove(node); }; @@ -2352,7 +2379,7 @@ namespace Barotrauma matchingItems = nodeOrder.GetMatchingItems(submarine, true, interactableFor: characterContext ?? Character.Controlled); } //more than one target item -> create a minimap-like selection with a pic of the sub - if (itemContext == null && !(nodeOrder.TargetEntity is Item) && matchingItems != null && matchingItems.Count > 1) + if (itemContext == null && nodeOrder.TargetEntity is not Item && matchingItems != null && matchingItems.Count > 1) { CreateMinimapNodes(nodeOrder, submarine, matchingItems); } @@ -2417,7 +2444,7 @@ namespace Barotrauma node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); var icon = OrderCategoryIcon.OrderCategoryIcons.FirstOrDefault(ic => ic.Category == category); - if (!(icon is null)) + if (icon is not null) { var tooltip = TextManager.Get($"ordercategorytitle.{category}"); var categoryDescription = TextManager.Get($"ordercategorydescription.{category}"); @@ -2484,7 +2511,7 @@ namespace Barotrauma // TODO: Doesn't work for player issued reports, because they don't have a target. bool useSpecificRepairOrder = false; if (CanFitMoreNodes() && ShouldDelegateOrder("repairelectrical") && - ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) + ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.RequiredSkills.Any(s => s.Identifier == "electrical"))) { if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["repairelectrical"])) { @@ -2493,7 +2520,7 @@ namespace Barotrauma useSpecificRepairOrder = true; } if (CanFitMoreNodes() && ShouldDelegateOrder("repairmechanical") && - ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) + ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.RequiredSkills.Any(s => s.Identifier == "mechanical"))) { if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["repairmechanical"])) { @@ -2565,7 +2592,7 @@ namespace Barotrauma static bool ShouldDelegateOrder(string orderIdentifier) => ShouldDelegateOrderId(orderIdentifier.ToIdentifier()); static bool ShouldDelegateOrderId(Identifier orderIdentifier) { - return !(Character.Controlled is Character c) || !(c?.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(orderIdentifier)); + return Character.Controlled is not Character c || !(c?.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(orderIdentifier)); } bool IsNonDuplicateOrder(Order order) => IsNonDuplicateOrderPrefab(order.Prefab, order.Option); bool IsNonDuplicateOrderPrefab(OrderPrefab orderPrefab, Identifier option = default) @@ -2642,6 +2669,7 @@ namespace Barotrauma new Order(p, itemContext, targetComponent)); } } + // If targeting a periscope connected to a turret, show the 'operateweapons' order var operateWeaponsPrefab = OrderPrefab.Prefabs["operateweapons"]; if (contextualOrders.None(o => o.Identifier == "operateweapons") && itemContext.Components.Any(c => c is Controller)) @@ -2656,11 +2684,11 @@ namespace Barotrauma // If targeting a repairable item with condition below the repair threshold, show the 'repairsystems' order if (contextualOrders.None(order => order.Identifier == "repairsystems") && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold)) { - if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) + if (itemContext.Repairables.Any(r => r != null && r.RequiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) { contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairelectrical"], itemContext, targetItem: null)); } - else if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) + else if (itemContext.Repairables.Any(r => r != null && r.RequiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) { contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairmechanical"], itemContext, targetItem: null)); } @@ -2694,14 +2722,14 @@ namespace Barotrauma } void AddIgnoreOrder(IIgnorable target) { - var orderIdentifier = "ignorethis"; + var orderIdentifier = Tags.IgnoreThis; if (!target.OrderedToBeIgnored && contextualOrders.None(order => order.Identifier == orderIdentifier)) { AddOrder(); } else { - orderIdentifier = "unignorethis"; + orderIdentifier = Tags.UnignoreThis; if (target.OrderedToBeIgnored && contextualOrders.None(order => order.Identifier == orderIdentifier)) { AddOrder(); @@ -2753,20 +2781,6 @@ namespace Barotrauma } } - // TODO: there's duplicate logic here and above -> would be better to refactor so that the conditions are only defined in one place - public static bool DoesItemHaveContextualOrders(Item item) - { - if (OrderPrefab.Prefabs.Any(o => o.TargetItemsMatchItem(item))) { return true; } - if (OrderPrefab.Prefabs.Any(o => o.TryGetTargetItemComponent(item, out _))) { return true; } - if (AIObjectiveCleanupItems.IsValidTarget(item, Character.Controlled, checkInventory: false)) { return true; } - if (AIObjectiveCleanupItems.IsValidContainer(item, Character.Controlled)) { return true; } - if (OrderPrefab.Prefabs.TryGet("loaditems", out OrderPrefab loadItemsPrefab) && AIObjectiveLoadItems.IsValidTarget(item, Character.Controlled, targetContainerTags: loadItemsPrefab.GetTargetItems())) { return true; } - if (item.Repairables.Any(r => r.IsBelowRepairThreshold)) { return true; } - return OrderPrefab.Prefabs.TryGet("operateweapons", out OrderPrefab operateWeaponsPrefab) && item.Components.Any(c => c is Controller) && - (item.GetConnectedComponents().Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) || - item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item))); - } - /// Use a negative value (e.g. -1) if there should be no hotkey associated with the node private GUIButton CreateOrderNode(Point size, RectTransform parent, Point offset, Order order, int hotkey, bool disableNode = false, bool checkIfOrderCanBeHeard = true) { @@ -3719,16 +3733,16 @@ namespace Barotrauma switch (order.TargetType) { case Order.OrderTargetType.Entity: - if (!(order.TargetEntity is IIgnorable ignorableEntity)) { break; } - ignorableEntity.OrderedToBeIgnored = order.Identifier == "ignorethis"; + if (order.TargetEntity is not IIgnorable ignorableEntity) { break; } + ignorableEntity.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis; break; case Order.OrderTargetType.Position: throw new NotImplementedException(); case Order.OrderTargetType.WallSection: if (!order.WallSectionIndex.HasValue) { break; } - if (!(order.TargetEntity is Structure s)) { break; } - if (!(s.GetSection(order.WallSectionIndex.Value) is IIgnorable ignorableWall)) { break; } - ignorableWall.OrderedToBeIgnored = order.Identifier == "ignorethis"; + if (order.TargetEntity is not Structure s) { break; } + if (s.GetSection(order.WallSectionIndex.Value) is not IIgnorable ignorableWall) { break; } + ignorableWall.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis; break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index feb6eb7d9..6571cebd7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -3,16 +3,19 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Barotrauma { - abstract partial class CampaignMode : GameMode + internal abstract partial class CampaignMode : GameMode { - protected bool crewDead; + public bool CrewDead + { + get; + protected set; + } protected Color overlayColor; protected Sprite overlaySprite; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index ce6972067..f0b7616cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -67,10 +67,11 @@ namespace Barotrauma var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), layout.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) }, isHorizontal: true) { + Stretch = true, RelativeSpacing = 0.02f }; - var campaignContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), layout.RectTransform, Anchor.BottomLeft), style: "InnerFrame") + var campaignContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), layout.RectTransform, Anchor.BottomLeft), style: "GUIFrameListBox") { CanBeFocused = false }; @@ -95,6 +96,7 @@ namespace Barotrauma loadCampaignButton.Selected = false; newCampaignContainer.Visible = true; loadCampaignContainer.Visible = false; + GameMain.NetLobbyScreen?.RefreshStartButtonVisibility(); return true; }; loadCampaignButton.OnClicked = (btn, obj) => @@ -103,6 +105,7 @@ namespace Barotrauma loadCampaignButton.Selected = true; newCampaignContainer.Visible = false; loadCampaignContainer.Visible = true; + GameMain.NetLobbyScreen?.RefreshStartButtonVisibility(); return true; }; loadCampaignContainer.Visible = false; @@ -297,7 +300,7 @@ namespace Barotrauma Level prevLevel = Level.Loaded; bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); - crewDead = false; + CrewDead = false; var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; if (continueButton != null) @@ -480,7 +483,6 @@ namespace Barotrauma GameMain.CampaignEndScreen.OnFinished = () => { GameMain.NetLobbyScreen.Select(); - if (GameMain.NetLobbyScreen.ContinueCampaignButton != null) { GameMain.NetLobbyScreen.ContinueCampaignButton.Enabled = false; } if (GameMain.NetLobbyScreen.QuitCampaignButton != null) { GameMain.NetLobbyScreen.QuitCampaignButton.Enabled = false; } }; } @@ -934,7 +936,7 @@ namespace Barotrauma { int renamedIdentifier = msg.ReadInt32(); string newName = msg.ReadString(); - CharacterInfo renamedCharacter = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); + CharacterInfo renamedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); if (renamedCharacter != null) { CrewManager.RenameCharacter(renamedCharacter, newName); } } @@ -942,7 +944,7 @@ namespace Barotrauma if (fireCharacter) { int firedIdentifier = msg.ReadInt32(); - CharacterInfo firedCharacter = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); + CharacterInfo firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); // this one might and is allowed to be null since the character is already fired on the original sender's game if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); } } @@ -952,7 +954,7 @@ namespace Barotrauma !NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID)) { CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires); - if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters); } + if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters, takeMoney: false); } CampaignUI.CrewManagement.SetPendingHires(pendingHires, map.CurrentLocation); if (renameCrewMember || fireCharacter) { CampaignUI.CrewManagement.UpdateCrew(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 803e3c275..f16ba4ad8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -243,7 +244,7 @@ namespace Barotrauma savedOnStart = true; } - crewDead = false; + CrewDead = false; endTimer = 5.0f; CrewManager.InitSinglePlayerRound(); LoadPets(); @@ -373,7 +374,7 @@ namespace Barotrauma SoundPlayer.OverrideMusicType = (success ? "endround" : "crewdead").ToIdentifier(); SoundPlayer.OverrideMusicDuration = 18.0f; GUI.SetSavingIndicatorState(success); - crewDead = false; + CrewDead = false; if (success) { @@ -582,9 +583,12 @@ namespace Barotrauma HintManager.OnAvailableTransition(transitionType); } - if (!crewDead) + if (!CrewDead) { - if (!CrewManager.GetCharacters().Any(c => !c.IsDead)) { crewDead = true; } + if (CrewManager.GetCharacters().None(c => !c.IsDead && !CrewManager.IsFired(c))) + { + CrewDead = true; + } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs index 725d24de9..c945b6103 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs @@ -42,6 +42,15 @@ namespace Barotrauma foreach (Submarine submarine in Submarine.Loaded) { submarine.NeutralizeBallast(); + //normally the body would be made static during level generation, + //but in the test mode we load the outpost/wreck/beacon as if it was a normal sub and need to do this manually + if (submarine.Info.Type == SubmarineType.Outpost || + submarine.Info.Type == SubmarineType.OutpostModule || + submarine.Info.Type == SubmarineType.Wreck || + submarine.Info.Type == SubmarineType.BeaconStation) + { + submarine.PhysicsBody.BodyType = FarseerPhysics.BodyType.Static; + } } if (SpawnOutpost) @@ -51,14 +60,14 @@ namespace Barotrauma if (TriggeredEvent != null) { - scriptedEvent = new List { TriggeredEvent.CreateInstance() }; + scriptedEvent = new List { TriggeredEvent.CreateInstance(GameMain.GameSession.EventManager.RandomSeed) }; GameMain.GameSession.EventManager.PinnedEvent = scriptedEvent.Last(); createEventButton = new GUIButton(new RectTransform(new Point(128, 64), GUI.Canvas, Anchor.TopCenter) { ScreenSpaceOffset = new Point(0, 32) }, TextManager.Get("create")) { OnClicked = delegate { - scriptedEvent.Add(TriggeredEvent.CreateInstance()); + scriptedEvent.Add(TriggeredEvent.CreateInstance(GameMain.GameSession.EventManager.RandomSeed)); GameMain.GameSession.EventManager.PinnedEvent = scriptedEvent.Last(); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 59031cb75..66efaee87 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -188,9 +188,9 @@ namespace Barotrauma.Tutorials var door = item.GetComponent(); if (door != null) { - if (door.requiredItems.Values.None(ris => ris.None(ri => ri.Identifiers.None(i => i == "locked")))) + if (door.RequiredItems.Values.None(ris => ris.None(ri => ri.Identifiers.None(i => i == "locked")))) { - door.requiredItems.Clear(); + door.RequiredItems.Clear(); } } } @@ -262,7 +262,7 @@ namespace Barotrauma.Tutorials yield return CoroutineStatus.Failure; } - if (eventPrefab.CreateInstance() is Event eventInstance) + if (eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed) is Event eventInstance) { GameMain.GameSession.EventManager.QueuedEvents.Enqueue(eventInstance); while (!eventInstance.IsFinished) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 7ce88ca26..ff5940862 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma @@ -110,7 +111,9 @@ namespace Barotrauma }; respawnTickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, respawnButtonContainer.RectTransform, Anchor.Center), TextManager.Get("respawnquestionpromptrespawn")) { - ToolTip = TextManager.Get("respawnquestionprompt"), + ToolTip = TextManager.GetWithVariable( + "respawnquestionprompt", "[percentage]", + (Math.Round(Networking.RespawnManager.SkillLossPercentageOnImmediateRespawn).ToString())), OnSelected = (tickbox) => { GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: !tickbox.Selected); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index 8d222c5a2..d859a5af5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -185,6 +185,19 @@ namespace Barotrauma } } + public static void OnItemMarkedForRelocation() + { + DisplayHint($"onitemmarkedforrelocation".ToIdentifier()); + } + + public static void OnItemMarkedForDeconstruction(Character character) + { + if (character == Character.Controlled) + { + DisplayHint($"onitemmarkedfordeconstruction".ToIdentifier()); + } + } + private static void CheckIsInteracting() { if (!CanDisplayHints()) { return; } @@ -582,6 +595,24 @@ namespace Barotrauma { DisplayHint("onballastflorainfected".ToIdentifier()); } + if (order.Identifier == "deconstructitems" && + Item.DeconstructItems.None()) + { + DisplayHint("ondeconstructorder".ToIdentifier()); + } + } + + public static void OnSetOrder(Character character, Order order) + { + if (!CanDisplayHints()) { return; } + if (character == null || order == null) { return; } + + if (order.OrderGiver == Character.Controlled && + order.Identifier == "deconstructitems" && + Item.DeconstructItems.None()) + { + DisplayHint("ondeconstructorder".ToIdentifier()); + } } private static void CheckIfDivingGearOutOfOxygen() @@ -719,7 +750,7 @@ namespace Barotrauma HintsIgnoredThisRound.Add(hintIdentifier); - ActiveHintMessageBox = new GUIMessageBox(hintIdentifier, text, icon); + ActiveHintMessageBox = new GUIMessageBox(hintIdentifier, TextManager.ParseInputTypes(text), icon); if (iconColor.HasValue) { ActiveHintMessageBox.IconColor = iconColor.Value; } OnUpdate = onUpdate; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 068477a41..8ab663787 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -822,7 +822,7 @@ namespace Barotrauma factionTextContent.Recalculate(); new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), - onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, reputation.NormalizedValue)); + onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, reputation.NormalizedValue, reputation.MinReputation, reputation.MaxReputation)); var reputationText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), string.Empty, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); @@ -871,7 +871,7 @@ namespace Barotrauma return factionFrame; } - public static void DrawReputationBar(SpriteBatch sb, Rectangle rect, float normalizedReputation) + public static void DrawReputationBar(SpriteBatch sb, Rectangle rect, float normalizedReputation, float minReputation, float maxReputation) { int segmentWidth = rect.Width / 5; rect.Width = segmentWidth * 5; @@ -885,9 +885,10 @@ namespace Barotrauma GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUIStyle.ColorInventoryBackground, scale: GUI.Scale, spriteEffect: SpriteEffects.FlipVertically); GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUIStyle.TextColorNormal, scale: GUI.Scale * 0.8f, spriteEffect: SpriteEffects.FlipVertically); - GUI.DrawString(sb, new Vector2(rect.X, rect.Bottom), "-100", GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); - Vector2 textSize = GUIStyle.SmallFont.MeasureString("100"); - GUI.DrawString(sb, new Vector2(rect.Right - textSize.X, rect.Bottom), "100", GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); + GUI.DrawString(sb, new Vector2(rect.X, rect.Bottom), ((int)minReputation).ToString(), GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); + string maxRepText = ((int)maxReputation).ToString(); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(maxRepText); + GUI.DrawString(sb, new Vector2(rect.Right - textSize.X, rect.Bottom), maxRepText, GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs index 073893bb2..da7d33546 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs @@ -1,7 +1,6 @@ #nullable enable -using System; -using System.Linq; using Barotrauma.Networking; +using System.Linq; namespace Barotrauma { @@ -13,7 +12,7 @@ namespace Barotrauma if (character != null) { - Speak(character); + character.Speak(text, ChatMessageType.Default); return; } @@ -21,23 +20,10 @@ namespace Barotrauma { if (npc.CampaignInteractionType == CampaignMode.InteractionType.Upgrade) { - Speak(npc); + npc.Speak(text, ChatMessageType.Default); break; } } - - void Speak(Character npc) - { - ChatMessage message = ChatMessage.Create(npc.Name, text, ChatMessageType.Default, npc); - if (!isSinglePlayer) - { - GameMain.Client?.AddChatMessage(message); - } - else - { - GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(message); - } - } } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 705b59f8f..2b9ec4112 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -771,7 +771,7 @@ namespace Barotrauma { if (Screen.Selected == GameMain.GameScreen) { - if (item.NonInteractable || item.NonPlayerTeamInteractable) + if (!item.IsInteractable(Character.Controlled)) { return QuickUseAction.None; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index f7062ac72..5ea8a5265 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -75,6 +75,7 @@ namespace Barotrauma.Items.Components private void UpdateConvexHulls() { if (item.Removed) { return; } + if (doorSprite == null) { return; } doorRect = new Rectangle( item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale), @@ -83,7 +84,7 @@ namespace Barotrauma.Items.Components (int)(doorSprite.size.Y * item.Scale)); Rectangle rect = doorRect; - if (IsHorizontal) + if (IsConvexHullHorizontal) { rect.Width = (int)(rect.Width * (1.0f - openState)); } @@ -94,7 +95,7 @@ namespace Barotrauma.Items.Components if (Window.Height > 0 && Window.Width > 0) { - if (IsHorizontal) + if (IsConvexHullHorizontal) { rect.Width = (int)(Window.X * item.Scale); rect.X -= (int)(doorRect.Width * openState); @@ -163,8 +164,8 @@ namespace Barotrauma.Items.Components var verts = GetConvexHullCorners(rect); Vector2 center = (verts[0] + verts[2]) / 2; convexHull.SetVertices( - verts, - IsHorizontal ? + verts, + IsConvexHullHorizontal ? new Vector2[] { new Vector2(verts[0].X, center.Y), new Vector2(verts[2].X, center.Y) } : new Vector2[] { new Vector2(center.X, verts[0].Y), new Vector2(center.X, verts[2].Y) }); convexHull.MaxMergeLosVerticesDist = 35.0f; @@ -304,6 +305,7 @@ namespace Barotrauma.Items.Components } else { + bool stateChanged = open != isOpen; isOpen = open; if (!isNetworkMessage || open != PredictedState) { @@ -314,6 +316,11 @@ namespace Barotrauma.Items.Components } if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } } + if (stateChanged) + { + ActionType actionType = open ? ActionType.OnOpen : ActionType.OnClose; + item.ApplyStatusEffects(actionType, deltaTime: 1.0f); + } } void PlayInteractionSound() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index d0f607e20..b24344c84 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -64,7 +64,8 @@ namespace Barotrauma.Items.Components spriteBatch, new Vector2(attachPos.X, -attachPos.Y), item.SpriteColor * 0.5f, - 0.0f, item.Scale, SpriteEffects.None, 0.0f); + item.RotationRad, + item.Scale, SpriteEffects.None, 0.0f); GUI.DrawRectangle(spriteBatch, new Vector2(attachPos.X - 2, -attachPos.Y - 2), Vector2.One * 5, GUIStyle.Red, thickness: 3); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 099f54b34..352ba2c30 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -18,11 +18,31 @@ namespace Barotrauma.Items.Components protected float currentCrossHairScale, currentCrossHairPointerScale; private RoundSound chargeSound; + private SoundChannel chargeSoundChannel; + + [Serialize(defaultValue: "0.5, 1.5", IsPropertySaveable.No, description: "Pitch slides from X to Y over the charge time")] + public Vector2 ChargeSoundWindupPitchSlide + { + get => _chargeSoundWindupPitchSlide; + set + { + _chargeSoundWindupPitchSlide = new Vector2( + Math.Max(value.X, SoundChannel.MinFrequencyMultiplier), + Math.Min(value.Y, SoundChannel.MaxFrequencyMultiplier)); + } + } + private Vector2 _chargeSoundWindupPitchSlide; private readonly List particleEmitters = new List(); private readonly List particleEmitterCharges = new List(); + /// + /// The orientation of the item is briefly wrong after the character holding it flips and before the holding logic forces it to the correct position. + /// We disable the crosshair briefly during that time to prevent it from momentarily jumping to an incorrect position. + /// + private float crossHairPosDirtyTimer; + [Serialize(1.0f, IsPropertySaveable.No, description: "The scale of the crosshair sprite (if there is one).")] public float CrossHairScale { @@ -30,9 +50,9 @@ namespace Barotrauma.Items.Components private set; } - partial void InitProjSpecific(ContentXElement element) + partial void InitProjSpecific(ContentXElement rangedWeaponElement) { - foreach (var subElement in element.Elements()) + foreach (var subElement in rangedWeaponElement.Elements()) { string textureDir = GetTextureDirectory(subElement); switch (subElement.Name.ToString().ToLowerInvariant()) @@ -62,6 +82,7 @@ namespace Barotrauma.Items.Components public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { + crossHairPosDirtyTimer -= deltaTime; currentCrossHairScale = currentCrossHairPointerScale = cam == null ? 1.0f : cam.Zoom; if (crosshairSprite != null) { @@ -92,6 +113,15 @@ namespace Barotrauma.Items.Components crosshairPointerPos = PlayerInput.MousePosition; } + public override void FlipX(bool relativeToSub) + { + crossHairPosDirtyTimer = 0.02f; + } + public override void FlipY(bool relativeToSub) + { + crossHairPosDirtyTimer = 0.02f; + } + partial void UpdateProjSpecific(float deltaTime) { float chargeRatio = currentChargeTime / MaxChargeTime; @@ -117,7 +147,7 @@ namespace Barotrauma.Items.Components } else if (chargeSoundChannel != null) { - chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); + chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(ChargeSoundWindupPitchSlide.X, ChargeSoundWindupPitchSlide.Y, chargeRatio); chargeSoundChannel.Position = new Vector3(item.WorldPosition, 0.0f); } break; @@ -143,15 +173,19 @@ namespace Barotrauma.Items.Components if (character == null || !character.IsKeyDown(InputType.Aim) || !character.CanAim) { return; } //camera focused on some other item/device, don't draw the crosshair - if (character.ViewTarget != null && (character.ViewTarget is Item viewTargetItem) && viewTargetItem.Prefab.FocusOnSelected) { return; } + if (character.ViewTarget is Item viewTargetItem && viewTargetItem.Prefab.FocusOnSelected) { return; } //don't draw the crosshair if the item is in some other type of equip slot than hands (e.g. assault rifle in the bag slot) if (!character.HeldItems.Contains(item)) { return; } GUI.HideCursor = (crosshairSprite != null || crosshairPointerSprite != null) && GUI.MouseOn == null && !Inventory.IsMouseOnInventory && !GameMain.Instance.Paused; - if (GUI.HideCursor) + + if (GUI.HideCursor && !character.AnimController.IsHoldingToRope) { - crosshairSprite?.Draw(spriteBatch, crosshairPos, ReloadTimer <= 0.0f ? Color.White : Color.White * 0.2f, 0, currentCrossHairScale); + if (crossHairPosDirtyTimer <= 0.0f) + { + crosshairSprite?.Draw(spriteBatch, crosshairPos, ReloadTimer <= 0.0f ? Color.White : Color.White * 0.2f, 0, currentCrossHairScale); + } crosshairPointerSprite?.Draw(spriteBatch, crosshairPointerPos, 0, currentCrossHairPointerScale); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 21b3c9c76..dfa27902a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -205,7 +205,7 @@ namespace Barotrauma.Items.Components int buttonSize = GUIStyle.ItemFrameTopBarHeight; Point margin = new Point(buttonSize / 4, buttonSize / 6); - GUILayoutGroup buttonArea = new GUILayoutGroup(new RectTransform(new Point(GuiFrame.Rect.Width - margin.X * 2, buttonSize - margin.Y * 2), GuiFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, margin.Y) }, + GUILayoutGroup buttonArea = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, buttonSize - margin.Y * 2), content.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(0, margin.Y) }, isHorizontal: true, childAnchor: Anchor.TopRight) { AbsoluteSpacing = margin.X / 2 @@ -334,7 +334,7 @@ namespace Barotrauma.Items.Components } else { - return item?.Name; + return item?.Prefab.Name; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 9e4c1b164..8bbdc47c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -40,6 +40,7 @@ namespace Barotrauma.Items.Components partial void SetLightSourceState(bool enabled, float brightness) { if (Light == null) { return; } + if (item.HiddenInGame) { enabled = false; } Light.Enabled = enabled; lightColorMultiplier = brightness; if (enabled) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index ba8b62d21..0fbbf4c00 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components RelativeSpacing = 0.08f }; - new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform) { MinSize = new Point(0, GUI.IntScale(25)) }, item.Name, font: GUIStyle.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform) { MinSize = new Point(0, GUI.IntScale(25)) }, item.Prefab.Name, font: GUIStyle.SubHeadingFont) { TextAlignment = Alignment.Center, AutoScaleHorizontal = true @@ -341,7 +341,7 @@ namespace Barotrauma.Items.Components GUIFrame itemFrame = new GUIFrame(new RectTransform(new Vector2(0.1f, 1f), parent.RectTransform), style: null) { UserData = identifier, - ToolTip = GetTooltip(prefab) + ToolTip = prefab.CreateTooltipText() }; Sprite icon = prefab.InventoryIcon ?? prefab.Sprite; @@ -372,21 +372,6 @@ namespace Barotrauma.Items.Components textBlock.Text = TextManager.GetWithVariable("campaignstore.quantity", "[amount]", count.ToString()); } - - static RichString GetTooltip(ItemPrefab prefab) - { - LocalizedString toolTip = $"‖color:{Color.White.ToStringHex()}‖{prefab.Name}‖color:end‖"; - - LocalizedString description = prefab.Description; - if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; } - - if (prefab.ContentPackage != GameMain.VanillaContent && prefab.ContentPackage != null) - { - toolTip += $"\n‖color:{Color.MediumPurple.ToStringHex()}‖{prefab.ContentPackage.Name}‖color:end‖"; - } - - return RichString.Rich(toolTip); - } } partial void OnItemLoadedProjSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 24fbcfb36..db034ffc6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -134,11 +134,11 @@ namespace Barotrauma.Items.Components spriteIndex += (force / 100.0f) * AnimSpeed * deltaTime; if (spriteIndex < 0) { - spriteIndex = propellerSprite.FrameCount; + spriteIndex = propellerSprite.FrameCount - Math.Abs(spriteIndex) % propellerSprite.FrameCount; } - if (spriteIndex >= propellerSprite.FrameCount) + else { - spriteIndex = 0.0f; + spriteIndex = spriteIndex % propellerSprite.FrameCount; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 96f708e8a..0596825f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), GuiFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter); // === LABEL === // - new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), paddedFrame.RectTransform), item.Name, font: GUIStyle.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), paddedFrame.RectTransform), item.Prefab.Name, font: GUIStyle.SubHeadingFont) { TextAlignment = Alignment.Center, AutoScaleVertical = true @@ -326,7 +326,7 @@ namespace Barotrauma.Items.Components UserData = fi, HoverColor = Color.Gold * 0.2f, SelectedColor = Color.Gold * 0.5f, - ToolTip = fi.TargetItem.Description + ToolTip = RichString.Rich(fi.TargetItem.Description) }; var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), @@ -339,7 +339,7 @@ namespace Barotrauma.Items.Components itemIcon, scaleToFit: true) { Color = fi.TargetItem.InventoryIconColor, - ToolTip = fi.TargetItem.Description + ToolTip = RichString.Rich(fi.TargetItem.Description) }; } @@ -347,7 +347,7 @@ namespace Barotrauma.Items.Components { Padding = Vector4.Zero, AutoScaleVertical = true, - ToolTip = fi.TargetItem.Description + ToolTip = RichString.Rich(fi.TargetItem.Description) }; new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), frame.RectTransform, Anchor.BottomRight), @@ -925,8 +925,6 @@ namespace Barotrauma.Items.Components nameBlock.Padding = new Vector4(0, nameBlock.Padding.Y, GUI.IntScale(5), nameBlock.Padding.W); if (nameBlock.TextScale < 0.7f) { - nameBlock.SetRichText(TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName) - .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName))); nameBlock.AutoScaleHorizontal = false; nameBlock.TextScale = 0.7f; nameBlock.Wrap = true; @@ -937,7 +935,7 @@ namespace Barotrauma.Items.Components if (!selectedItem.TargetItem.Description.IsNullOrEmpty()) { var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - selectedItem.TargetItem.Description, + RichString.Rich(selectedItem.TargetItem.Description), font: GUIStyle.SmallFont, wrap: true); description.Padding = new Vector4(0, description.Padding.Y, description.Padding.Z, description.Padding.W); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 48b20a156..dc0eeb6dd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -1090,7 +1090,12 @@ namespace Barotrauma.Items.Components float totalVolume = 0.0f; foreach (Hull linkedHull in hullData.LinkedHulls) { - waterVolume += linkedHull.WaterVolume; + //water detector ignores very small amounts of water, + //do it here too so the nav terminal doesn't display the water + if (WaterDetector.GetWaterPercentage(linkedHull) > 0.0f) + { + waterVolume += linkedHull.WaterVolume; + } totalVolume += linkedHull.Volume; } hullData.HullWaterAmount = diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs index 8c09ef322..67652e781 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components public override bool Select(Character character) { - if (GameMain.GameSession?.Campaign == null) + if (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 1e636ec15..4f3f08ffe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1446,7 +1446,7 @@ namespace Barotrauma.Items.Components //only relevant in the end levels or maybe custom subs with some kind of non-hulled parts Rectangle worldBorders = submarine.GetDockedBorders(); worldBorders.Location += submarine.WorldPosition.ToPoint(); - if (Submarine.RectContains(worldBorders, pingSource)) + if (Submarine.RectContains(worldBorders, pingSource) || submarine.Info.OutpostGenerationParams is { AlwaysShowStructuresOnSonar: true }) { CreateBlipsForSubmarineWalls(submarine, pingSource, transducerPos, pingRadius, prevPingRadius, range, passive); continue; @@ -1502,7 +1502,9 @@ namespace Barotrauma.Items.Components foreach (Voronoi2.GraphEdge edge in cell.Edges) { if (!edge.IsSolid) { continue; } - float cellDot = Vector2.Dot(cell.Center - pingSource, (edge.Center + cell.Translation) - cell.Center); + + //the normal of the edge must be pointing towards the ping source to be visible + float cellDot = Vector2.Dot((edge.Center + cell.Translation) - pingSource, edge.GetNormal(cell)); if (cellDot > 0) { continue; } float facingDot = Vector2.Dot( @@ -1543,6 +1545,7 @@ namespace Barotrauma.Items.Components { if (c.AnimController.CurrentHull != null || !c.Enabled) { continue; } if (!c.IsUnconscious && c.Params.HideInSonar) { continue; } + if (c.InDetectable) { continue; } if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; } if (c.AnimController.SimplePhysicsEnabled) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 90d3f85ed..4d9aabff1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -316,28 +316,28 @@ namespace Barotrauma.Items.Components { case 0: leftText = TextManager.Get("DescentVelocity"); - centerText = $"({TextManager.Get("KilometersPerHour")})"; + centerText = TextManager.Get("KilometersPerHour"); rightTextGetter = () => { Vector2 vel = controlledSub == null ? Vector2.Zero : controlledSub.Velocity; var realWorldVel = ConvertUnits.ToDisplayUnits(vel.Y * Physics.DisplayToRealWorldRatio) * 3.6f; - return ((int)(-realWorldVel)).ToString(); + return (-realWorldVel).ToString("0.0"); }; break; case 1: leftText = TextManager.Get("Velocity"); - centerText = $"({TextManager.Get("KilometersPerHour")})"; + centerText = TextManager.Get("KilometersPerHour"); rightTextGetter = () => { Vector2 vel = controlledSub == null ? Vector2.Zero : controlledSub.Velocity; var realWorldVel = ConvertUnits.ToDisplayUnits(vel.X * Physics.DisplayToRealWorldRatio) * 3.6f; if (controlledSub != null && controlledSub.FlippedX) { realWorldVel *= -1; } - return ((int)realWorldVel).ToString(); + return realWorldVel.ToString("0.0"); }; break; case 2: leftText = TextManager.Get("Depth"); - centerText = $"({TextManager.Get("Meter")})"; + centerText = TextManager.Get("Meter"); rightTextGetter = () => { if (Level.Loaded is { IsEndBiome: true }) @@ -789,9 +789,9 @@ namespace Barotrauma.Items.Components } pressureWarningText.Visible = item.Submarine != null && Timing.TotalTime % 1.0f < 0.8f; - float depthEffectThreshold = 500.0f; if (Level.Loaded != null && pressureWarningText.Visible && - item.Submarine.RealWorldDepth > Level.Loaded.RealWorldCrushDepth - depthEffectThreshold && item.Submarine.RealWorldDepth > item.Submarine.RealWorldCrushDepth - depthEffectThreshold) + item.Submarine.RealWorldDepth > Level.Loaded.RealWorldCrushDepth - PressureWarningThreshold && + item.Submarine.RealWorldDepth > item.Submarine.RealWorldCrushDepth - PressureWarningThreshold) { pressureWarningText.Visible = true; pressureWarningText.Text = diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index fd794ea11..0b551f6bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -148,11 +148,29 @@ namespace Barotrauma.Items.Components Vector2 particlePos = item.WorldPosition; float rotation = -item.body.Rotation; if (item.body.Dir < 0.0f) { rotation += MathHelper.Pi; } + + //if the position is in a sub's local coordinates, convert to world coordinates + particlePos = ConvertToWorldCoordinates(particlePos); + //if the start location is in a sub's local coordinates, convert to world coordinates + startLocation = ConvertToWorldCoordinates(startLocation); + //same for end location + endLocation = ConvertToWorldCoordinates(endLocation); + Tuple tracerPoints = new Tuple(startLocation, endLocation); foreach (ParticleEmitter emitter in particleEmitters) { emitter.Emit(1.0f, particlePos, hullGuess: null, angle: rotation, particleRotation: rotation, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier, tracerPoints: tracerPoints); } + + static Vector2 ConvertToWorldCoordinates(Vector2 position) + { + Submarine containing = Submarine.FindContainingInLocalCoordinates(position); + if (containing != null) + { + position += containing.Position; + } + return position; + } } partial void InitProjSpecific(ContentXElement element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 59010730c..a05f47a0f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -63,10 +63,7 @@ namespace Barotrauma.Items.Components if (item.HiddenInGame) { return false; } if (!HasRequiredItems(character, false) || character.SelectedItem != item) { return false; } if (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition) { return true; } - - float defaultMaxCondition = item.MaxCondition / item.MaxRepairConditionMultiplier; - - if (MathUtils.Percentage(item.Condition, defaultMaxCondition) < RepairThreshold) { return true; } + if (item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold) { return true; } if (CurrentFixer == character) { @@ -135,13 +132,13 @@ namespace Barotrauma.Items.Components new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("RequiredRepairSkills"), font: GUIStyle.SubHeadingFont); skillTextContainer = paddedFrame; - for (int i = 0; i < requiredSkills.Count; i++) + for (int i = 0; i < RequiredSkills.Count; i++) { var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillTextContainer.RectTransform), - " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + requiredSkills[i].Identifier), ((int) Math.Round(requiredSkills[i].Level * SkillRequirementMultiplier)).ToString()), + " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + RequiredSkills[i].Identifier), ((int) Math.Round(RequiredSkills[i].Level * SkillRequirementMultiplier)).ToString()), font: GUIStyle.SmallFont) { - UserData = requiredSkills[i] + UserData = RequiredSkills[i] }; } @@ -262,7 +259,7 @@ namespace Barotrauma.Items.Components } } - float conditionPercentage = item.Condition / (item.MaxCondition / item.MaxRepairConditionMultiplier) * 100f; + float conditionPercentage = item.ConditionPercentageRelativeToDefaultMaxCondition; for (int i = 0; i < particleEmitters.Count; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 79adeb549..c6b5417c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -2,13 +2,16 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Xml.Linq; +using Barotrauma.Sounds; namespace Barotrauma.Items.Components { partial class Rope : ItemComponent, IDrawableComponent { private Sprite sprite, startSprite, endSprite; + + private RoundSound snapSound, reelSound; + private SoundChannel reelSoundChannel; [Serialize(5, IsPropertySaveable.No)] public int SpriteWidth @@ -54,6 +57,19 @@ namespace Barotrauma.Items.Components Math.Abs(target.DrawPosition.Y - sourcePos.Y)) * 1.5f; } } + + [Serialize("1.0, 1.0", IsPropertySaveable.No, description: "When reeling in, the pitch slides from X to Y, depending on the length of the rope.")] + public Vector2 ReelSoundPitchSlide + { + get => _reelSoundPitchSlide; + set + { + _reelSoundPitchSlide = new Vector2( + Math.Max(value.X, SoundChannel.MinFrequencyMultiplier), + Math.Min(value.Y, SoundChannel.MaxFrequencyMultiplier)); + } + } + private Vector2 _reelSoundPitchSlide; partial void InitProjSpecific(ContentXElement element) { @@ -70,9 +86,28 @@ namespace Barotrauma.Items.Components case "endsprite": endSprite = new Sprite(subElement); break; + case "snapsound": + snapSound = RoundSound.Load(subElement); + break; + case "reelsound": + reelSound = RoundSound.Load(subElement); + break; } } } + + partial void UpdateProjSpecific() + { + if (isReelingIn && !Snapped) + { + PlaySound(reelSound, source.WorldPosition); + } + else + { + reelSoundChannel?.FadeOutAndDispose(); + reelSoundChannel = null; + } + } public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { @@ -184,6 +219,32 @@ namespace Barotrauma.Items.Components overrideColor ?? SpriteColor, depth: depth, width: width); } } + + private void PlaySound(RoundSound sound, Vector2 position) + { + if (sound == null) { return; } + if (sound == reelSound) + { + if (reelSoundChannel is not { IsPlaying: true }) + { + reelSoundChannel = SoundPlayer.PlaySound(sound.Sound, position, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier()); + if (reelSoundChannel != null) + { + reelSoundChannel.Looping = true; + } + } + else + { + reelSoundChannel.Position = new Vector3(position, 0); + reelSoundChannel.Gain = MathHelper.Lerp(0, 1.0f, MathUtils.InverseLerp(MinPullDistance, MaxLength, MathUtils.Pow(currentRopeLength, 1.5f))); + reelSoundChannel.FrequencyMultiplier = MathHelper.Lerp(ReelSoundPitchSlide.X, ReelSoundPitchSlide.Y, MathUtils.InverseLerp(MinPullDistance, MaxLength, currentRopeLength)); + } + } + else + { + SoundPlayer.PlaySound(sound.Sound, position, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier()); + } + } public void ClientEventRead(IReadMessage msg, float sendingTime) { @@ -191,30 +252,38 @@ namespace Barotrauma.Items.Components if (!snapped) { - UInt16 targetId = msg.ReadUInt16(); - UInt16 sourceId = msg.ReadUInt16(); + ushort targetId = msg.ReadUInt16(); + ushort sourceId = msg.ReadUInt16(); byte limbIndex = msg.ReadByte(); - Item target = Entity.FindEntityByID(targetId) as Item; - if (target == null) { return; } + if (Entity.FindEntityByID(targetId) is not Item target) { return; } var source = Entity.FindEntityByID(sourceId); - if (source is Character sourceCharacter && limbIndex >= 0 && limbIndex < sourceCharacter.AnimController.Limbs.Length) + switch (source) { - Limb sourceLimb = sourceCharacter.AnimController.Limbs[limbIndex]; - Attach(sourceLimb, target); - } - else if (source is ISpatialEntity spatialEntity) - { - Attach(spatialEntity, target); + case Character sourceCharacter when limbIndex >= 0 && limbIndex < sourceCharacter.AnimController.Limbs.Length: + { + Limb sourceLimb = sourceCharacter.AnimController.Limbs[limbIndex]; + Attach(sourceLimb, target); + sourceCharacter.AnimController.DragWithRope(); + break; + } + case ISpatialEntity spatialEntity: + Attach(spatialEntity, target); + break; } } } protected override void RemoveComponentSpecific() { - sprite?.Remove(); sprite = null; - startSprite?.Remove(); startSprite = null; - endSprite?.Remove(); endSprite = null; + sprite?.Remove(); + sprite = null; + startSprite?.Remove(); + startSprite = null; + endSprite?.Remove(); + endSprite = null; + reelSoundChannel?.FadeOutAndDispose(); + reelSoundChannel = null; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs index 3f466be5c..6eb381503 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs @@ -129,6 +129,7 @@ namespace Barotrauma.Items.Components public void RemoveComponents(IReadOnlyCollection node) { + if (Locked) { return; } var ids = node.Select(static n => n.ID).ToImmutableArray(); if (GameMain.NetworkMember is null) @@ -145,6 +146,7 @@ namespace Barotrauma.Items.Components public void AddWire(CircuitBoxConnection one, CircuitBoxConnection two) { + if (Locked) { return; } if (GameMain.NetworkMember is null) { Connect(one, two, static delegate { }, CircuitBoxWire.SelectedWirePrefab); @@ -158,6 +160,7 @@ namespace Barotrauma.Items.Components public void RemoveWires(IReadOnlyCollection wires) { + if (Locked) { return; } var ids = wires.Select(static w => w.ID).ToImmutableArray(); if (GameMain.NetworkMember is null) { @@ -175,6 +178,7 @@ namespace Barotrauma.Items.Components var ids = ImmutableArray.CreateBuilder(); var ios = ImmutableArray.CreateBuilder(); + var labelIds = ImmutableArray.CreateBuilder(); foreach (var moveable in moveables) { @@ -188,6 +192,9 @@ namespace Barotrauma.Items.Components case CircuitBoxInputOutputNode io: ios.Add(io.NodeType); break; + case CircuitBoxLabelNode label: + labelIds.Add(label.ID); + break; } } @@ -195,12 +202,13 @@ namespace Barotrauma.Items.Components { SelectComponentsInternal(ids, controlledId, overwrite); SelectInputOutputInternal(ios, controlledId, overwrite); + SelectLabelsInternal(labelIds, controlledId, overwrite); return; } - if ((!ids.Any() && !ios.Any()) && !overwrite) { return; } + if (!ids.Any() && !ios.Any() && !labelIds.Any() && !overwrite) { return; } - CreateClientEvent(new CircuitBoxSelectNodesEvent(ids.ToImmutable(), ios.ToImmutable(), overwrite, controlledId)); + CreateClientEvent(new CircuitBoxSelectNodesEvent(ids.ToImmutable(), ios.ToImmutable(), labelIds.ToImmutable(), overwrite, controlledId)); } public void SelectWires(IReadOnlyCollection wires, bool overwrite) @@ -222,8 +230,10 @@ namespace Barotrauma.Items.Components public void MoveComponent(Vector2 moveAmount, IReadOnlyCollection moveables) { + if (Locked) { return; } var ids = ImmutableArray.CreateBuilder(); var ios = ImmutableArray.CreateBuilder(); + var labelIds = ImmutableArray.CreateBuilder(); foreach (CircuitBoxNode move in moveables) { @@ -235,23 +245,27 @@ namespace Barotrauma.Items.Components case CircuitBoxInputOutputNode io: ios.Add(io.NodeType); break; + case CircuitBoxLabelNode label: + labelIds.Add(label.ID); + break; } } if (GameMain.NetworkMember is null) { - MoveNodesInternal(ids, ios, moveAmount); + MoveNodesInternal(ids, ios, labelIds, moveAmount); return; } - if (!ids.Any() && !ios.Any()) { return; } + if (!ids.Any() && !ios.Any() && !labelIds.Any()) { return; } - CreateClientEvent(new CircuitBoxMoveComponentEvent(ids.ToImmutable(), ios.ToImmutable(), moveAmount)); + CreateClientEvent(new CircuitBoxMoveComponentEvent(ids.ToImmutable(), ios.ToImmutable(), labelIds.ToImmutable(), moveAmount)); } public void AddComponent(ItemPrefab prefab, Vector2 pos) { + if (Locked) { return; } if (GameMain.NetworkMember is null) { ItemPrefab resource; @@ -276,6 +290,72 @@ namespace Barotrauma.Items.Components CreateClientEvent(new CircuitBoxAddComponentEvent(prefab.UintIdentifier, pos)); } + public void RenameLabel(CircuitBoxLabelNode label, Color color, NetLimitedString header, NetLimitedString body) + { + if (Locked) { return; } + if (GameMain.NetworkMember is null) + { + label.EditText(header, body); + label.Color = color; + return; + } + + CreateClientEvent(new CircuitBoxRenameLabelEvent(label.ID, color, header, body)); + } + + public void ResizeNode(CircuitBoxNode node, CircuitBoxResizeDirection dir, Vector2 amount) + { + if (Locked) { return; } + var resize = node.ResizeBy(dir, amount); + if (GameMain.NetworkMember is null) + { + node.ApplyResize(resize.Size, resize.Pos); + return; + } + + // TODO this needs to be refactored at some point, probably not now + // the problem here is that the circuit box supports resizing all nodes + // but we limit the resizing to only labels on the client + // and on the server we only have a network message that targets labels + // so if we ever want the ability to resize other nodes (could be useful) the network message + // needs to know what type of ID it's targeting + if (node is not ICircuitBoxIdentifiable identifiable) + { + DebugConsole.ThrowError("Tried to resize a node that doesn't have an ID."); + return; + } + + CreateClientEvent(new CircuitBoxResizeLabelEvent(identifiable.ID, resize.Pos, resize.Size)); + } + + public void AddLabel(Vector2 pos) + { + if (Locked) { return; } + if (GameMain.NetworkMember is null) + { + AddLabelInternal(ICircuitBoxIdentifiable.FindFreeID(Labels), GUIStyle.Blue, pos, CircuitBoxLabelNode.DefaultHeaderText, NetLimitedString.Empty); + return; + } + + CreateClientEvent(new CircuitBoxAddLabelEvent(pos, GUIStyle.Blue, CircuitBoxLabelNode.DefaultHeaderText, NetLimitedString.Empty)); + } + + public void RemoveLabel(IReadOnlyCollection labels) + { + if (Locked) { return; } + if (!labels.Any()) { return; } + + var ids = labels.Select(static n => n.ID).ToImmutableArray(); + + if (GameMain.NetworkMember is null) + { + RemoveLabelInternal(ids); + return; + } + + CreateClientEvent(new CircuitBoxRemoveLabelEvent(ids)); + } + public partial void OnViewUpdateProjSpecific() { UI?.MouseSnapshotHandler.UpdateConnections(); @@ -396,7 +476,7 @@ namespace Barotrauma.Items.Components case CircuitBoxOpcode.MoveComponent: { var data = INetSerializableStruct.Read(msg); - MoveNodesInternal(data.TargetIDs, data.IOs, data.MoveAmount); + MoveNodesInternal(data.TargetIDs, data.IOs, data.LabelIDs, data.MoveAmount); break; } case CircuitBoxOpcode.UpdateSelection: @@ -406,8 +486,9 @@ namespace Barotrauma.Items.Components var nodeDict = data.ComponentIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy); var wireDict = data.WireIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy); var ioDict = data.InputOutputs.ToImmutableDictionary(static s => s.Type, static s => s.SelectedBy); + var labelDict = data.LabelIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy); - UpdateSelections(nodeDict, wireDict, ioDict); + UpdateSelections(nodeDict, wireDict, ioDict, labelDict); break; } case CircuitBoxOpcode.AddWire: @@ -426,11 +507,18 @@ namespace Barotrauma.Items.Components { Components.Clear(); Wires.Clear(); + Labels.Clear(); var data = INetSerializableStruct.Read(msg); foreach (var compData in data.Components) { AddComponentFromData(compData); } foreach (var wireData in data.Wires) { AddWireFromData(wireData); } + foreach (var labelData in data.Labels) + { + AddLabelInternal(labelData.ID, labelData.Color, labelData.Position, labelData.Header, labelData.Body); + ResizeLabelInternal(labelData.ID, labelData.Position, labelData.Size); + } + foreach (var node in InputOutputNodes) { node.Position = node.NodeType switch @@ -443,6 +531,31 @@ namespace Barotrauma.Items.Components wasInitializedByServer = true; break; } + case CircuitBoxOpcode.RenameLabel: + { + var data = INetSerializableStruct.Read(msg); + RenameLabelInternal(data.LabelId, data.Color, data.NewHeader, data.NewBody); + break; + } + case CircuitBoxOpcode.AddLabel: + { + var data = INetSerializableStruct.Read(msg); + AddLabelInternal(data.ID, data.Color, data.Position, data.Header, data.Body); + ResizeLabelInternal(data.ID, data.Position, data.Size); + break; + } + case CircuitBoxOpcode.RemoveLabel: + { + var data = INetSerializableStruct.Read(msg); + RemoveLabelInternal(data.TargetIDs); + break; + } + case CircuitBoxOpcode.ResizeLabel: + { + var data = INetSerializableStruct.Read(msg); + ResizeLabelInternal(data.ID, data.Position, data.Size); + break; + } default: throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs index 88dde557b..a6ca45e36 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs @@ -12,11 +12,13 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { - if (!editing || !MapEntity.SelectedList.Contains(item)) { return; } - - Vector2 pos = item.WorldPosition + TransformedDetectOffset; - pos.Y = -pos.Y; - GUI.DrawRectangle(spriteBatch, pos - new Vector2(rangeX, rangeY), new Vector2(rangeX, rangeY) * 2.0f, Color.Cyan * 0.5f, isFilled: false, thickness: 2); + if ((editing && MapEntity.SelectedList.Contains(item)) || + (ConnectionPanel.ShouldDebugDrawWiring && Character.Controlled?.SelectedItem == item)) + { + Vector2 pos = item.WorldPosition + TransformedDetectOffset; + pos.Y = -pos.Y; + GUI.DrawRectangle(spriteBatch, pos - new Vector2(rangeX, rangeY), new Vector2(rangeX, rangeY) * 2.0f, Color.Cyan * 0.5f, isFilled: false, thickness: 2); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index a2f10cce8..5021d59a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -137,6 +137,7 @@ namespace Barotrauma.Items.Components { if (c == equipper || !c.Enabled || c.Removed) { continue; } if (!ShowDeadCharacters && c.IsDead) { continue; } + if (c.InDetectable) { continue; } float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); if (dist < Range * Range) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index c43909cc7..febcfc55b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -118,6 +118,19 @@ namespace Barotrauma.Items.Components get; private set; } + + [Serialize(defaultValue: "0.5, 1.5", IsPropertySaveable.No, description: "Pitch slides from X to Y over the charge time")] + public Vector2 ChargeSoundWindupPitchSlide + { + get => _chargeSoundWindupPitchSlide; + set + { + _chargeSoundWindupPitchSlide = new Vector2( + Math.Max(value.X, SoundChannel.MinFrequencyMultiplier), + Math.Min(value.Y, SoundChannel.MaxFrequencyMultiplier)); + } + } + private Vector2 _chargeSoundWindupPitchSlide; partial void InitProjSpecific(ContentXElement element) { @@ -220,9 +233,9 @@ namespace Barotrauma.Items.Components { if (moveSound != null) { - moveSoundChannel.FadeOutAndDispose(); + moveSoundChannel?.FadeOutAndDispose(); moveSoundChannel = SoundPlayer.PlaySound(moveSound.Sound, item.WorldPosition, moveSound.Volume, moveSound.Range, ignoreMuffling: moveSound.IgnoreMuffling, freqMult: moveSound.GetRandomFrequencyMultiplier()); - if (moveSoundChannel != null) moveSoundChannel.Looping = true; + if (moveSoundChannel != null) { moveSoundChannel.Looping = true;} } } } @@ -268,7 +281,7 @@ namespace Barotrauma.Items.Components } else if (chargeSoundChannel != null) { - chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); + chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(ChargeSoundWindupPitchSlide.X, ChargeSoundWindupPitchSlide.Y, chargeRatio); chargeSoundChannel.Position = new Vector3(item.WorldPosition, 0.0f); } break; @@ -482,12 +495,12 @@ namespace Barotrauma.Items.Components }; widget.MouseDown += () => { - widget.color = GUIStyle.Green; + widget.Color = GUIStyle.Green; prevAngle = minRotation; }; widget.Deselected += () => { - widget.color = Color.Yellow; + widget.Color = Color.Yellow; item.CreateEditingHUD(); RotationLimits = RotationLimits; if (SubEditorScreen.IsSubEditor()) @@ -513,7 +526,7 @@ namespace Barotrauma.Items.Components }; widget.PreDraw += (sprtBtch, deltaTime) => { - widget.tooltip = "Min: " + (int)MathHelper.ToDegrees(minRotation); + widget.Tooltip = "Min: " + (int)MathHelper.ToDegrees(minRotation); widget.DrawPos = GetDrawPos() + new Vector2((float)Math.Cos(minRotation), (float)Math.Sin(minRotation)) * coneRadius / Screen.Selected.Cam.Zoom * GUI.Scale; }; }); @@ -526,12 +539,12 @@ namespace Barotrauma.Items.Components }; widget.MouseDown += () => { - widget.color = GUIStyle.Green; + widget.Color = GUIStyle.Green; prevAngle = maxRotation; }; widget.Deselected += () => { - widget.color = Color.Yellow; + widget.Color = Color.Yellow; item.CreateEditingHUD(); RotationLimits = RotationLimits; if (SubEditorScreen.IsSubEditor()) @@ -557,7 +570,7 @@ namespace Barotrauma.Items.Components }; widget.PreDraw += (sprtBtch, deltaTime) => { - widget.tooltip = "Max: " + (int)MathHelper.ToDegrees(maxRotation); + widget.Tooltip = "Max: " + (int)MathHelper.ToDegrees(maxRotation); widget.DrawPos = GetDrawPos() + new Vector2((float)Math.Cos(maxRotation), (float)Math.Sin(maxRotation)) * coneRadius / Screen.Selected.Cam.Zoom * GUI.Scale; widget.Update(deltaTime); }; @@ -584,20 +597,20 @@ namespace Barotrauma.Items.Components Vector2 offset = new Vector2(size / 2 + 5, -10); if (!widgets.TryGetValue(id, out Widget widget)) { - widget = new Widget(id, size, Widget.Shape.Rectangle) + widget = new Widget(id, size, WidgetShape.Rectangle) { - color = Color.Yellow, - tooltipOffset = offset, - inputAreaMargin = 20, + Color = Color.Yellow, + TooltipOffset = offset, + InputAreaMargin = 20, RequireMouseOn = false }; widgets.Add(id, widget); initMethod?.Invoke(widget); } - widget.size = size; - widget.tooltipOffset = offset; - widget.thickness = thickness; + widget.Size = size; + widget.TooltipOffset = offset; + widget.Thickness = thickness; return widget; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 1846a30b6..176d5c98b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -168,11 +168,14 @@ namespace Barotrauma //TODO: define this in xml slotSpriteSmall = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(10, 6, 119, 120), null, 0); // Adjustment to match the old size of 75,71 - SlotSpriteSmall.size = new Vector2(SlotSpriteSmall.SourceRect.Width * 0.575f, SlotSpriteSmall.SourceRect.Height * 0.575f); + SlotSpriteSmall.size = new Vector2(SlotSpriteSmall.SourceRect.Width * SlotSpriteSmallScale, SlotSpriteSmall.SourceRect.Height * SlotSpriteSmallScale); } return slotSpriteSmall; } } + + public const float SlotSpriteSmallScale = 0.575f; + public static Sprite DraggableIndicator; public static Sprite UnequippedIndicator, UnequippedHoverIndicator, UnequippedClickedIndicator, EquippedIndicator, EquippedHoverIndicator, EquippedClickedIndicator; @@ -211,6 +214,7 @@ namespace Barotrauma public RichString Tooltip { get; private set; } public int tooltipDisplayedCondition; + public bool tooltipShowedContextualOptions; public bool ForceTooltipRefresh; @@ -230,6 +234,7 @@ namespace Barotrauma { if (ForceTooltipRefresh) { return true; } if (Item == null) { return false; } + if (PlayerInput.KeyDown(InputType.ContextualCommand) != tooltipShowedContextualOptions) { return true; } return (int)Item.ConditionPercentage != tooltipDisplayedCondition; } @@ -244,6 +249,7 @@ namespace Barotrauma } Tooltip = GetTooltip(Item, itemsInSlot, Character.Controlled); tooltipDisplayedCondition = (int)Item.ConditionPercentage; + tooltipShowedContextualOptions = PlayerInput.KeyDown(InputType.ContextualCommand); } private static RichString GetTooltip(Item item, IEnumerable itemsInSlot, Character character) @@ -323,19 +329,19 @@ namespace Barotrauma .TrimStart(); } - if (itemsInSlot.All(it => it.NonInteractable || it.NonPlayerTeamInteractable)) + if (itemsInSlot.All(it => !it.IsInteractable(Character.Controlled))) { toolTip += " " + TextManager.Get("connectionlocked"); } if (!item.IsFullCondition && !item.Prefab.HideConditionInTooltip) { - string conditionColorStr = XMLExtensions.ColorToString(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull)); + string conditionColorStr = XMLExtensions.ToStringHex(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull)); toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖"; } if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; } if (item.Prefab.ContentPackage != GameMain.VanillaContent && item.Prefab.ContentPackage != null) { - colorStr = XMLExtensions.ColorToString(Color.MediumPurple); + colorStr = XMLExtensions.ToStringHex(Color.MediumPurple); toolTip += $"\n‖color:{colorStr}‖{item.Prefab.ContentPackage.Name}‖color:end‖"; } } @@ -350,7 +356,17 @@ namespace Barotrauma } #if DEBUG toolTip += $" ({item.Prefab.Identifier})"; -#endif +#endif + if (PlayerInput.KeyDown(InputType.ContextualCommand)) + { + toolTip += $"\n‖color:gui.blue‖{TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders"))}‖color:end‖"; + } + else + { + var colorStr = XMLExtensions.ToStringHex(Color.LightGray * 0.7f); + toolTip += $"\n‖color:{colorStr}‖{TextManager.Get("itemmsg.morreoptionsavailable")}‖color:end‖"; + } + return RichString.Rich(toolTip); } } @@ -613,10 +629,7 @@ namespace Barotrauma slot.State = GUIComponent.ComponentState.None; if (mouseOn && (DraggingItems.Any() || selectedSlot == null || selectedSlot.Slot == slot) && DraggingInventory == null) - // && - //(highlightedSubInventories.Count == 0 || highlightedSubInventories.Contains(this) || highlightedSubInventorySlot?.Slot == slot || highlightedSubInventory.Owner == item)) - { - + { slot.State = GUIComponent.ComponentState.Hover; if (selectedSlot == null || (!selectedSlot.IsSubSlot && isSubSlot)) @@ -631,27 +644,47 @@ namespace Barotrauma if (!DraggingItems.Any()) { - var interactableItems = Screen.Selected == GameMain.GameScreen ? slots[slotIndex].Items.Where(it => !it.NonInteractable && !it.NonPlayerTeamInteractable) : slots[slotIndex].Items; - if (PlayerInput.PrimaryMouseButtonDown() && interactableItems.Any()) - { - if (PlayerInput.KeyDown(InputType.TakeHalfFromInventorySlot)) + var interactableItems = Screen.Selected == GameMain.GameScreen ? slots[slotIndex].Items.Where(it => it.IsInteractable(Character.Controlled)) : slots[slotIndex].Items; + if (interactableItems.Any()) + { + if (availableContextualOrder.target != null) { - DraggingItems.AddRange(interactableItems.Skip(interactableItems.Count() / 2)); + if (PlayerInput.PrimaryMouseButtonClicked()) + { + GameMain.GameSession.CrewManager.SetCharacterOrder(character: null, + new Order(OrderPrefab.Prefabs[availableContextualOrder.orderIdentifier], availableContextualOrder.target, targetItem: null, orderGiver: Character.Controlled)); + } + availableContextualOrder = default; } - else if (PlayerInput.KeyDown(InputType.TakeOneFromInventorySlot)) + else if (PlayerInput.KeyDown(InputType.Command) && + PlayerInput.KeyDown(InputType.ContextualCommand) && + GameMain.GameSession?.CrewManager != null) { - DraggingItems.Add(interactableItems.First()); + GameMain.GameSession.CrewManager.OpenCommandUI(interactableItems.FirstOrDefault(), forceContextual: true); } - else + else if (PlayerInput.PrimaryMouseButtonDown()) { - DraggingItems.AddRange(interactableItems); + if (PlayerInput.KeyDown(InputType.TakeHalfFromInventorySlot)) + { + DraggingItems.AddRange(interactableItems.Skip(interactableItems.Count() / 2)); + } + else if (PlayerInput.KeyDown(InputType.TakeOneFromInventorySlot)) + { + DraggingItems.Add(interactableItems.First()); + } + else + { + DraggingItems.AddRange(interactableItems); + } + DraggingSlot = slot; } - DraggingSlot = slot; } } else if (PlayerInput.PrimaryMouseButtonReleased()) { - var interactableItems = Screen.Selected == GameMain.GameScreen ? slots[slotIndex].Items.Where(it => !it.NonInteractable && !it.NonPlayerTeamInteractable) : slots[slotIndex].Items; + var interactableItems = Screen.Selected == GameMain.GameScreen ? + slots[slotIndex].Items.Where(it => it.IsInteractable(Character.Controlled)) : + slots[slotIndex].Items; if (PlayerInput.DoubleClicked() && interactableItems.Any()) { doubleClickedItems.Clear(); @@ -1286,7 +1319,7 @@ namespace Barotrauma if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container && container.Inventory.CanBePut(item)) { - if (!container.AllowDragAndDrop || !container.DrawInventory) + if (!container.AllowDragAndDrop || !container.AllowAccess) { allowCombine = false; } @@ -1562,10 +1595,22 @@ namespace Barotrauma { selectedSlot.RefreshTooltip(); } - DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect); + + if (!slotIconTooltip.IsNullOrEmpty()) + { + DrawToolTip(spriteBatch, slotIconTooltip, slotRect); + } + else + { + DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect); + } + slotIconTooltip = string.Empty; } } + private static (Item target, Identifier orderIdentifier) availableContextualOrder; + private static LocalizedString slotIconTooltip; + public static void DrawSlot(SpriteBatch spriteBatch, Inventory inventory, VisualSlot slot, Item item, int slotIndex, bool drawItem = true, InvSlotType type = InvSlotType.Any) { Rectangle rect = slot.Rect; @@ -1730,7 +1775,7 @@ namespace Barotrauma } Color spriteColor = sprite == item.Sprite ? item.GetSpriteColor() : item.GetInventoryIconColor(); - if (inventory != null && (inventory.Locked || inventory.slots[slotIndex].Items.All(it => it.NonInteractable || it.NonPlayerTeamInteractable))) { spriteColor *= 0.5f; } + if (inventory != null && (inventory.Locked || inventory.slots[slotIndex].Items.All(it => !it.IsInteractable(Character.Controlled)))) { spriteColor *= 0.5f; } if (CharacterHealth.OpenHealthWindow != null && !item.UseInHealthInterface && !item.AllowedSlots.Contains(InvSlotType.HealthInterface) && item.GetComponent() == null) { spriteColor = Color.Lerp(spriteColor, Color.TransparentBlack, 0.5f); @@ -1741,15 +1786,24 @@ namespace Barotrauma } sprite.Draw(spriteBatch, itemPos, spriteColor, rotation, scale); - if (((item.SpawnedInCurrentOutpost && !item.AllowStealing) || (inventory != null && inventory.slots[slotIndex].Items.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) + if (item.OrderedToBeIgnored) { - var stealIcon = CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand]; - Vector2 iconSize = new Vector2(25 * GUI.Scale); - stealIcon.Draw( - spriteBatch, - new Vector2(rect.X + iconSize.X * 0.2f, rect.Bottom - iconSize.Y * 1.2f), - color: GUIStyle.Red, - scale: iconSize.X / stealIcon.size.X); + if (OrderPrefab.Prefabs.TryGet(Tags.IgnoreThis, out OrderPrefab ignoreOrder)) + { + DrawSideIcon(ignoreOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.ignored"), ignoreOrder.Color, out bool mouseOn); + if (mouseOn) { availableContextualOrder = (item, Tags.UnignoreThis); } + + } + } + else if (Item.DeconstructItems.Contains(item) && + OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder)) + { + DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn); + if (mouseOn) { availableContextualOrder = (item, Tags.DontDeconstructThis); } + } + else if (((item.SpawnedInCurrentOutpost && !item.AllowStealing) || (inventory != null && inventory.slots[slotIndex].Items.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) + { + DrawSideIcon(CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand], Direction.Left, TextManager.Get("tooltip.stolenitem"), GUIStyle.Red, out _); } int maxStackSize = item.Prefab.GetMaxStackSize(inventory); if (inventory is ItemInventory itemInventory) @@ -1798,6 +1852,22 @@ namespace Barotrauma SpriteEffects.None, layerDepth: 0.0f); } + + void DrawSideIcon(Sprite icon, Direction side, LocalizedString tooltip, Color color, out bool mouseOn) + { + Vector2 iconSize = new Vector2(25 * GUI.Scale); + float margin = 0.2f; + Vector2 pos = new Vector2( + side == Direction.Left ? rect.X + iconSize.X * margin : rect.Right - iconSize.X * margin, + rect.Bottom - iconSize.Y * 1.2f); + mouseOn = Vector2.Distance(PlayerInput.MousePosition, pos) < iconSize.X / 2; + if (mouseOn) + { + slotIconTooltip = tooltip; + color = Color.Lerp(color, Color.White, 0.5f); + } + icon.Draw(spriteBatch, pos, color: color, scale: iconSize.X / icon.size.X); + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 6552ba373..5ff674fb4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -9,7 +9,10 @@ using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; +using System.Text; namespace Barotrauma { @@ -216,7 +219,7 @@ namespace Barotrauma } } - float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; + float displayCondition = FakeBroken ? 0.0f : ConditionPercentageRelativeToDefaultMaxCondition; for (int i = 0; i < Prefab.BrokenSprites.Length;i++) { if (Prefab.BrokenSprites[i].FadeIn) { continue; } @@ -340,9 +343,28 @@ namespace Barotrauma bool renderTransparent = isWiringMode && GetComponent() == null; if (renderTransparent) { color *= 0.15f; } + if (Character.Controlled != null && Character.DebugDrawInteract) + { + color = Color.Red; + foreach (var ic in components) + { + var interactionType = GetComponentInteractionVisibility(Character.Controlled, ic); + if (interactionType == InteractionVisibility.MissingRequirement) + { + color = Color.Orange; + } + else if (interactionType == InteractionVisibility.Visible) + { + color = Color.LightGreen; + break; + } + } + } + BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; - float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; + + float displayCondition = FakeBroken ? 0.0f : ConditionPercentageRelativeToDefaultMaxCondition; Vector2 drawOffset = GetCollapseEffectOffset(); drawOffset.Y = -drawOffset.Y; @@ -849,27 +871,6 @@ namespace Barotrauma itemEditor.Children.First().Color = Color.Black * 0.7f; if (!inGame) { - //create a tag picker for item containers to make it easier to pick relevant tags for PreferredContainers - var itemContainer = GetComponent(); - if (itemContainer != null) - { - var tagsField = itemEditor.Fields["Tags".ToIdentifier()].First().Parent; - - //find all the items that can be put inside the container and add their PreferredContainer identifiers/tags to the available tags - ImmutableHashSet availableTags = ItemPrefab.Prefabs - .Where(ip => itemContainer.CanBeContained(ip)) - .SelectMany(ip => ip.PreferredContainers.SelectMany(pc => pc.Primary.Union(pc.Secondary))) - //remove identifiers from the available container tags - //(otherwise the list will include many irrelevant options, - //e.g. "weldingtool" because a welding fuel tank can be placed inside the container, etc) - .Where(t => !ItemPrefab.Prefabs.ContainsKey(t)) - .ToImmutableHashSet(); - new GUIButton(new RectTransform(new Vector2(0.1f, 1), tagsField.RectTransform, Anchor.TopRight), "...") - { - OnClicked = (bt, userData) => { CreateTagPicker(tagsField.GetChild(), availableTags); return true; } - }; - } - if (Linkable) { var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("HoldToLink"), font: GUIStyle.SmallFont); @@ -881,6 +882,33 @@ namespace Barotrauma linkText.TextColor = GUIStyle.Orange; itemsText.TextColor = GUIStyle.Orange; } + + //create a tag picker for item containers to make it easier to pick relevant tags for PreferredContainers + var itemContainer = GetComponent(); + if (itemContainer != null) + { + var tagBox = itemEditor.Fields["Tags".ToIdentifier()].First() as GUITextBox; + var tagsField = tagBox?.Parent; + + var containerTagLayout = new GUILayoutGroup(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), isHorizontal: true); + var containerTagButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1), containerTagLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); + new GUIButton(new RectTransform(new Vector2(0.95f, 1), containerTagButtonLayout.RectTransform), text: TextManager.Get("containertaguibutton"), style: "GUIButtonSmall") + { + OnClicked = (_, _) => { CreateContainerTagPicker(tagBox); return true; }, + TextBlock = { AutoScaleHorizontal = true } + }; + var containerTagText = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1), containerTagLayout.RectTransform), TextManager.Get("containertaguibuttondescription"), font: GUIStyle.SmallFont) + { + TextColor = GUIStyle.Orange + }; + var limitedString = ToolBox.LimitString(containerTagText.Text, containerTagText.Font, itemEditor.Rect.Width - containerTagButtonLayout.Rect.Width); + if (limitedString != containerTagText.Text) + { + containerTagText.ToolTip = containerTagText.Text; + containerTagText.Text = limitedString; + } + itemEditor.AddCustomContent(containerTagLayout, 3); + } var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), isHorizontal: true) { @@ -972,6 +1000,12 @@ namespace Barotrauma }; itemEditor.AddCustomContent(tickBox, 1); } + + if (!Layer.IsNullOrEmpty()) + { + var layerText = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)) { MinSize = new Point(0, heightScaled) }, TextManager.AddPunctuation(':', TextManager.Get("editor.layer"), Layer)); + itemEditor.AddCustomContent(layerText, 1); + } } foreach (ItemComponent ic in components) @@ -987,7 +1021,7 @@ namespace Barotrauma } else { - if (ic.requiredItems.Count == 0 && ic.DisabledRequiredItems.Count == 0 && SerializableProperty.GetProperties(ic).Count == 0) { continue; } + if (ic.RequiredItems.Count == 0 && ic.DisabledRequiredItems.Count == 0 && SerializableProperty.GetProperties(ic).Count == 0) { continue; } } new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); @@ -1004,7 +1038,7 @@ namespace Barotrauma } List requiredItems = new List(); - foreach (var kvp in ic.requiredItems) + foreach (var kvp in ic.RequiredItems) { foreach (RelatedItem relatedItem in kvp.Value) { @@ -1089,34 +1123,389 @@ namespace Barotrauma return result; } - private void CreateTagPicker(GUITextBox textBox, IEnumerable availableTags) + public void CreateContainerTagPicker([MaybeNull] GUITextBox tagTextBox) { - var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); + var msgBox = new GUIMessageBox(string.Empty, string.Empty, new[] { TextManager.Get("Ok") }, new Vector2(0.35f, 0.6f), new Point(400, 400)); msgBox.Buttons[0].OnClicked = msgBox.Close; - var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) + var infoIcon = new GUIImage(new RectTransform(new Vector2(0.066f), msgBox.InnerFrame.RectTransform) { - PlaySoundOnSelect = true, - OnSelected = (component, userData) => + RelativeOffset = new Vector2(0.015f) + }, style: "GUIButtonInfo") + { + ToolTip = TextManager.Get("containertagui.tutorial"), + IgnoreLayoutGroups = true + }; + + var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.85f), msgBox.Content.RectTransform)); + + var list = new GUIListBox(new RectTransform(new Vector2(1f, 1f), layout.RectTransform)); + + const float NameSize = 0.4f; + const float ItemSize = 0.5f; + const float CountSize = 0.1f; + + var headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), list.Content.RectTransform), isHorizontal: true); + new GUIButton(new RectTransform(new Vector2(NameSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.tag"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; + new GUIButton(new RectTransform(new Vector2(ItemSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.items"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; + new GUIButton(new RectTransform(new Vector2(CountSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.count"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; + + var itemsByTag = + ContainerTagPrefab.Prefabs + .ToImmutableDictionary( + ct => ct, + ct => ct.GetItemsAndSpawnProbabilities()); + + // Group the prefabs by category and turn them into a dictionary where the key is the category and value is the list of identifiers of the prefabs. + // LINQ GroupBy returns GroupedEnumerable where the enumerable is the list of prefabs and key is what we grouped by. + var tagCategories = ContainerTagPrefab.Prefabs + .GroupBy(ct => ct.Category) + .ToImmutableDictionary( + g => g.Key, + g => g.Select(ct => ct.Identifier).ToImmutableArray()); + + foreach (var (category, categoryTags) in tagCategories) + { + var categoryButton = new GUIButton(new RectTransform(new Vector2(1f, 0.075f), list.Content.RectTransform), style: "GUIButtonSmallFreeScale"); + categoryButton.Color *= 0.66f; + var categoryLayout = new GUILayoutGroup(new RectTransform(Vector2.One, categoryButton.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var categoryText = new GUITextBlock(new RectTransform(Vector2.One, categoryLayout.RectTransform), TextManager.Get($"tagcategory.{category}"), font: GUIStyle.SubHeadingFont); + var arrowImage = new GUIImage(new RectTransform(new Vector2(1f, 0.5f), categoryLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrowFreeScale"); + var arrowPadding = new GUIFrame(new RectTransform(new Vector2(0.025f, 1f), categoryLayout.RectTransform), style: null); + + bool hasHiddenCategories = false; + foreach (var categoryTag in categoryTags.OrderBy(t => t.Value)) { - if (!(userData is Identifier)) { return true; } - AddTag((Identifier)userData); - textBox.Text = Tags; - msgBox.Close(); + var found = itemsByTag.FirstOrNull(kvp => kvp.Key.Identifier == categoryTag); + if (found is null) + { + DebugConsole.ThrowError($"Failed to find tag with identifier {categoryTag} in itemsByTag"); + continue; + } + + var (tag, prefabsAndProbabilities) = found.Value; + + bool isCorrectSubType = tag.IsRecommendedForSub(Submarine); + + var tagLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), list.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + UserData = category, + Visible = isCorrectSubType + }; + + if (!isCorrectSubType) + { + hasHiddenCategories = true; + } + + var checkBoxLayout = new GUILayoutGroup(new RectTransform(new Vector2(NameSize, 1f), tagLayout.RectTransform), childAnchor: Anchor.Center); + var enabledCheckBox = new GUITickBox(new RectTransform(Vector2.One, checkBoxLayout.RectTransform, Anchor.Center), tag.Name, font: GUIStyle.SmallFont) + { + Selected = tags.Contains(tag.Identifier), + ToolTip = tag.Description + }; + + var tickBoxText = enabledCheckBox.TextBlock; + tickBoxText.Text = ToolBox.LimitString(tickBoxText.Text, tickBoxText.Font, tickBoxText.Rect.Width); + + var itemLayout = new GUILayoutGroup(new RectTransform(new Vector2(ItemSize, 1f), tagLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var itemLayoutScissor = new GUIScissorComponent(new RectTransform(new Vector2(0.8f, 1f), itemLayout.RectTransform)) { CanBeFocused = false }; + var itemLayoutButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1), itemLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center); + var itemLayoutButton = new GUIButton(new RectTransform(new Vector2(0.8f), itemLayoutButtonLayout.RectTransform), text: "...", style: "GUICharacterInfoButton") + { + UserData = tag, + ToolTip = TextManager.Get("containertagui.viewprobabilities") + }; + + itemLayoutButtonLayout.Recalculate(); + + float scroll = 0f; + float localScroll = 0f; + int lastSkippedItems = 0; + int skippedItems = 0; + var itemLayoutDraw = new GUICustomComponent(new RectTransform(new Vector2(1f, 0.9f), itemLayoutScissor.Content.RectTransform, Anchor.CenterLeft), onDraw: (spriteBatch, component) => + { + component.ToolTip = string.Empty; + + const float padding = 8f; + float offset = 0f; + float size = component.Rect.Height; + int start = (int)Math.Floor(scroll); + int amountToDraw = (int)Math.Ceiling(component.Rect.Width / size) + 1; // +1 just to be on the safe side + bool shouldIncrementOnSkip = true; + float toDrawWidth = prefabsAndProbabilities.Length * (size + padding); + + // if the width is less than the component width we need to limit how many items we draw or it looks weird + if (toDrawWidth < component.Rect.Width) + { + shouldIncrementOnSkip = false; + amountToDraw = prefabsAndProbabilities.Length; + } + + for (int i = start; i < start + amountToDraw; i++) + { + var (ip, probability, _) = prefabsAndProbabilities[i % prefabsAndProbabilities.Length]; + var sprite = ip.InventoryIcon ?? ip.Sprite; + + if (sprite is null) + { + // I don't think this should happen but just in case + if (shouldIncrementOnSkip) + { + amountToDraw++; + skippedItems++; + } + continue; + } + + if (ShouldHideItemPrefab(ip, probability)) + { + if (shouldIncrementOnSkip) + { + skippedItems++; + amountToDraw++; + } + continue; + } + + float partialScroll = localScroll * (size + padding); + var drawRect = new RectangleF(itemLayoutScissor.Rect.X + offset - partialScroll, component.Rect.Y, size, size); + + var isMouseOver = drawRect.Contains(PlayerInput.MousePosition); + if (isMouseOver) + { + component.ToolTip = ip.CreateTooltipText(); + } + + var slotSprite = Inventory.SlotSpriteSmall; + slotSprite?.Draw(spriteBatch, drawRect.Location, Color.White, origin: Vector2.Zero, rotate: 0f, scale: size / slotSprite.size.X * Inventory.SlotSpriteSmallScale); + + float iconScale = Math.Min(drawRect.Width / sprite.size.X, drawRect.Height / sprite.size.Y) * 0.9f; + + Color drawColor = ip.InventoryIconColor; + + sprite.Draw(spriteBatch, drawRect.Center, drawColor, origin: sprite.Origin, scale: iconScale); + offset += size + padding; + } + + // we need to compensate for the skipped items so that the scroll doesn't jump around + if (skippedItems < lastSkippedItems) + { + scroll += lastSkippedItems - skippedItems; + } + + lastSkippedItems = skippedItems; + skippedItems = 0; + }, onUpdate: (deltaTime, component) => + { + if (GUI.MouseOn != component && MathUtils.NearlyEqual(localScroll, 0, deltaTime * 2)) + { + localScroll = 0f; + return; + } + + float totalWidth = prefabsAndProbabilities.Length * (component.Rect.Height + 8f); + if (totalWidth < component.Rect.Width) { return; } + scroll += deltaTime; + localScroll = scroll % 1f; + }) + { + HoverCursor = CursorState.Default, + AlwaysOverrideCursor = true + }; + + var tooltip = TextManager.Get(tag.WarnIfLess ? "ContainerTagUI.RecommendedAmount" : "ContainerTagUI.SuggestedAmount"); + + var countBlock = new GUITextBlock(new RectTransform(new Vector2(CountSize, 1f), tagLayout.RectTransform), string.Empty, textAlignment: Alignment.Center) + { + ToolTip = tooltip + }; + UpdateCountBlock(countBlock, tag); + + enabledCheckBox.OnSelected += tickBox => + { + if (tickBox.Selected) + { + AddTag(tag.Identifier); + } + else + { + RemoveTag(tag.Identifier); + } + + if (tagTextBox is not null) + { + tagTextBox.Text = string.Join(',', tags.Where(t => !Prefab.Tags.Contains(t))); + } + UpdateCountBlock(countBlock, tag); + return true; + }; + + itemLayoutButton.OnClicked = (button, _) => + { + CreateContainerTagItemListPopup(tag, button.Rect.Center, layout, prefabsAndProbabilities); + return true; + }; + + void UpdateCountBlock(GUITextBlock textBlock, ContainerTagPrefab containerTag) + { + if (textBlock is null) { return; } + + var tagCount = Submarine.GetItems(alsoFromConnectedSubs: true).Count(i => i.HasTag(containerTag.Identifier)); + textBlock.Text = $"{tagCount} ({containerTag.RecommendedAmount})"; + + if (!isCorrectSubType || !containerTag.WarnIfLess || containerTag.RecommendedAmount <= 0) { return; } + + if (tagCount < containerTag.RecommendedAmount) + { + textBlock.TextColor = GUIStyle.Red; + textBlock.Text += "*"; + textBlock.ToolTip = RichString.Rich($"{tooltip}\n\n‖color:gui.red‖{TextManager.Get("ContainerTagUI.RecommendedAmountWarning")}‖color:end‖"); + } + else if (tagCount >= containerTag.RecommendedAmount) + { + textBlock.TextColor = GUIStyle.Green; + textBlock.ToolTip = tooltip; + } + } + } + + arrowImage.SpriteEffects = hasHiddenCategories ? SpriteEffects.None : SpriteEffects.FlipVertically; + categoryButton.OnClicked = (_, _) => + { + arrowImage.SpriteEffects ^= SpriteEffects.FlipVertically; + + foreach (var child in list.Content.Children) + { + if (child.UserData is Identifier id && id == category) + { + child.Visible = !child.Visible; + } + } + return true; + }; + } + } + + private static void CreateContainerTagItemListPopup(ContainerTagPrefab tag, Point location, GUIComponent popupParent, ImmutableArray prefabAndProbabilities) + { + const string TooltipUserData = "tooltip"; + const string ProbabilityUserData = "probability"; + + if (popupParent.GetChildByUserData(TooltipUserData) is { } existingTooltip) + { + popupParent.RemoveChild(existingTooltip); + } + + var tooltip = new GUIFrame(new RectTransform(new Point(popupParent.Rect.Height), popupParent.RectTransform) + { + AbsoluteOffset = location - popupParent.Rect.Location + }) + { + UserData = TooltipUserData, + IgnoreLayoutGroups = true + }; + + if (tooltip.Rect.Bottom > GameMain.GraphicsHeight) + { + int diffY = tooltip.Rect.Bottom - GameMain.GraphicsHeight; + tooltip.RectTransform.AbsoluteOffset -= new Point(0, diffY); + } + + if (tooltip.Rect.Right > GameMain.GraphicsWidth) + { + int diffX = tooltip.Rect.Right - GameMain.GraphicsWidth; + tooltip.RectTransform.AbsoluteOffset -= new Point(diffX, 0); + } + + var tooltipLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(tooltip.RectTransform, 0.9f), tooltip.RectTransform, Anchor.Center)); + + var tooltipHeader = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), tag.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); + var tooltipList = new GUIListBox(new RectTransform(new Vector2(1f, 0.7f), tooltipLayout.RectTransform)); + + var tooltipHeaderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), tooltipList.Content.RectTransform), isHorizontal: true); + new GUIButton(new RectTransform(new Vector2(0.66f, 1f), tooltipHeaderLayout.RectTransform), TextManager.Get("tagheader.item"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; + new GUIButton(new RectTransform(new Vector2(0.33f, 1f), tooltipHeaderLayout.RectTransform), TextManager.Get("tagheader.probability"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; + + foreach (var itemAndProbability in prefabAndProbabilities.OrderByDescending(p => p.Probability)) + { + var (ip, probability, campaignOnlyProbability) = itemAndProbability; + if (ShouldHideItemPrefab(ip, probability)) { continue; } + + var itemLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), tooltipList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + UserData = itemAndProbability + }; + + var itemNameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.66f, 1f), itemLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + Stretch = true + }; + + var itemIcon = new GUIImage(new RectTransform(Vector2.One, itemNameLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), ip.InventoryIcon ?? ip.Sprite, scaleToFit: true) + { + Color = ip.InventoryIconColor + }; + + var itemName = new GUITextBlock(new RectTransform(Vector2.One, itemNameLayout.RectTransform), ip.Name); + itemName.Text = ToolBox.LimitString(ip.Name, itemName.Font, itemName.Rect.Width); + + var toolTipContainer = new GUIFrame(new RectTransform(Vector2.One, itemNameLayout.RectTransform), style: null) + { + IgnoreLayoutGroups = true, + ToolTip = ip.CreateTooltipText() + }; + + var probabilityText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1f), itemLayout.RectTransform), ProbabilityToPercentage(campaignOnlyProbability), textAlignment: Alignment.Right) + { + UserData = ProbabilityUserData + }; + if (MathUtils.NearlyEqual(campaignOnlyProbability, 0f)) { probabilityText.TextColor = GUIStyle.Red; } + } + + var campaignCheckbox = new GUITickBox(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), label: TextManager.Get("containertagui.campaignonly")) + { + ToolTip = TextManager.Get("containertagui.campaignonlytooltip"), + Selected = true, + OnSelected = box => + { + foreach (var child in tooltipList.Content.Children) + { + if (child.UserData is not ContainerTagPrefab.ItemAndProbability data) { continue; } + + if (child.GetChildByUserData(ProbabilityUserData) is not GUITextBlock text) { continue; } + + float probability = box.Selected + ? data.CampaignProbability + : data.Probability; + text.Text = ProbabilityToPercentage(probability); + + text.TextColor = MathUtils.NearlyEqual(probability, 0f) + ? GUIStyle.Red + : GUIStyle.TextColorNormal; + } + return true; } }; - foreach (var availableTag in availableTags.ToList().OrderBy(t => t)) + var tooltipClose = new GUIButton(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), TextManager.Get("Close")) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textList.Content.RectTransform) { MinSize = new Point(0, 20) }, - ToolBox.LimitString(availableTag.Value, GUIStyle.Font, textList.Content.Rect.Width)) + OnClicked = (_, _) => { - UserData = availableTag - }; - } + popupParent.RemoveChild(tooltip); + return true; + } + }; + + static LocalizedString ProbabilityToPercentage(float probability) + => TextManager.GetWithVariable("percentageformat", "[value]", MathF.Round((probability * 100f), 1).ToString(CultureInfo.InvariantCulture)); + } + private static bool ShouldHideItemPrefab(ItemPrefab ip, float probability) + => ip.HideInMenus && MathUtils.NearlyEqual(probability, 0f); + /// /// Reposition currently active item interfaces to make sure they don't overlap with each other /// @@ -1213,10 +1602,21 @@ namespace Barotrauma } activeHUDs.Clear(); + maxPriorityHUDs.Clear(); + bool DrawHud(ItemComponent ic) + { + if (!ic.ShouldDrawHUD(character)) { return false; } + if (character.HasEquippedItem(this)) + { + return ic.DrawHudWhenEquipped; + } + else + { + return ic.CanBeSelected && ic.HasRequiredItems(character, addMessage: false); + } + } //the HUD of the component with the highest priority will be drawn //if all components have a priority of 0, all of them are drawn - maxPriorityHUDs.Clear(); - bool DrawHud(ItemComponent ic) => ic.ShouldDrawHUD(character) && (ic.CanBeSelected && ic.HasRequiredItems(character, addMessage: false) || (character.HasEquippedItem(this) && ic.DrawHudWhenEquipped)); foreach (ItemComponent ic in activeComponents) { if (ic.HudPriority > 0 && DrawHud(ic) && (maxPriorityHUDs.Count == 0 || ic.HudPriority >= maxPriorityHUDs[0].HudPriority)) @@ -1317,7 +1717,7 @@ namespace Barotrauma } } - readonly List texts = new List(); + readonly List texts = new(); public List GetHUDTexts(Character character, bool recreateHudTexts = true) { // Always create the texts if they have not yet been created @@ -1348,42 +1748,91 @@ namespace Barotrauma nameText += $" x{DroppedStack.Count()}"; } - texts.Add(new ColoredText(nameText, GUIStyle.TextColorNormal, false, false)); + texts.Add(new ColoredText(nameText, GUIStyle.TextColorNormal, isCommand: false, isError: false)); if (CampaignMode.BlocksInteraction(CampaignInteractionType)) { - texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)).Value, Color.Cyan, false, false)); + texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)).Value, Color.Cyan, isCommand: false, isError: false)); } else { - foreach (ItemComponent ic in components) + foreach (ItemComponent itemComponent in components) { - if (!ic.CanBePicked && !ic.CanBeSelected) { continue; } - if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; } - if (ic is ConnectionPanel connectionPanel && !connectionPanel.CanRewire()) { continue; } - Color color = Color.Gray; - if (ic.HasRequiredItems(character, false)) - { - if (ic is Repairable r) - { - if (r.IsBelowRepairThreshold) { color = Color.Cyan; } - } - else - { - color = Color.Cyan; - } - } - if (ic.DisplayMsg.IsNullOrEmpty()) { continue; } - texts.Add(new ColoredText(ic.DisplayMsg.Value, color, false, false)); + var interactionVisibility = GetComponentInteractionVisibility(character, itemComponent); + if (interactionVisibility == InteractionVisibility.None) { continue; } + if (itemComponent.DisplayMsg.IsNullOrEmpty()) { continue; } + + Color color = interactionVisibility == InteractionVisibility.MissingRequirement ? Color.Gray : Color.Cyan; + texts.Add(new ColoredText(itemComponent.DisplayMsg.Value, color, isCommand: false, isError: false)); } } - if (PlayerInput.IsShiftDown() && CrewManager.DoesItemHaveContextualOrders(this)) + if (PlayerInput.KeyDown(InputType.ContextualCommand)) { - texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")).Value, Color.Cyan, false, false)); + texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")).Value, Color.Cyan, isCommand: false, isError: false)); } + else + { + texts.Add(new ColoredText(TextManager.Get("itemmsg.morreoptionsavailable").Value, Color.LightGray * 0.7f, isCommand: false, isError: false)); + } return texts; } + private enum InteractionVisibility + { + None, + MissingRequirement, + Visible + } + + /// + /// Determine, for UI display purposes, the type of interaction visibility for an item component. + /// + /// Example: + /// Visible -> Display cyan "click to interact" type text on item hover. + /// MissingRequirement -> Display gray "need tool" type text on item hover. + /// None -> Hide from item hover texts. + /// + /// Character, for tool requirement purposes. + /// The item component to inspect. + /// The interaction visibility state for this component. + private static InteractionVisibility GetComponentInteractionVisibility(Character character, ItemComponent itemComponent) + { + if (!itemComponent.CanBePicked && !itemComponent.CanBeSelected) { return InteractionVisibility.None; } + if (itemComponent is Holdable holdable && !holdable.CanBeDeattached()) { return InteractionVisibility.None; } + if (itemComponent is ConnectionPanel connectionPanel && !connectionPanel.CanRewire()) { return InteractionVisibility.None; } + + InteractionVisibility interactionVisibility = InteractionVisibility.MissingRequirement; + if (itemComponent.HasRequiredItems(character, addMessage: false)) + { + if (itemComponent is Repairable repairable) + { + if (repairable.IsBelowRepairThreshold) + { + interactionVisibility = InteractionVisibility.Visible; + } + } + else + { + interactionVisibility = InteractionVisibility.Visible; + } + } + + return interactionVisibility; + } + + public bool HasVisibleInteraction(Character character) + { + foreach (var component in components) + { + if (GetComponentInteractionVisibility(character, component) == InteractionVisibility.Visible) + { + return true; + } + } + + return false; + } + public void ForceHUDLayoutUpdate(bool ignoreLocking = false) { foreach (ItemComponent ic in activeHUDs) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 362dafa31..e12c9b41a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -331,7 +331,6 @@ namespace Barotrauma } private GUIComponent CreateEditingHUD(bool inGame = false) { - editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.15f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index a3ab80804..d2deb27cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -170,16 +170,18 @@ namespace Barotrauma { if (!pendingSectionUpdates.Any() && !pendingDecalUpdates.Any()) { - GameMain.NetworkMember?.CreateEntityEvent(this, new StatusEventData()); + //these are used to modify the amount water/fire in the hull with console commands + //they should be usable even when not controlling a character + GameMain.Client?.CreateEntityEvent(this, new StatusEventData(), requireControlledCharacter: false); } foreach (Decal decal in pendingDecalUpdates) { - GameMain.NetworkMember?.CreateEntityEvent(this, new DecalEventData(decal)); + GameMain.Client?.CreateEntityEvent(this, new DecalEventData(decal)); } pendingDecalUpdates.Clear(); foreach (int pendingSectionUpdate in pendingSectionUpdates) { - GameMain.NetworkMember?.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); + GameMain.Client?.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); } pendingSectionUpdates.Clear(); networkUpdatePending = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index d2b5d9cef..5375c89eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -16,27 +16,34 @@ namespace Barotrauma float scale = Math.Min(drawArea.Width / (float)Bounds.Width, drawArea.Height / (float)Bounds.Height) * 0.9f; - foreach ((Identifier identifier, Rectangle rect) in DisplayEntities) + foreach (var displayEntity in DisplayEntities) { - var entityPrefab = FindByIdentifier(identifier); + var entityPrefab = FindByIdentifier(displayEntity.Identifier); if (entityPrefab is CoreEntityPrefab || entityPrefab == null) { continue; } var drawRect = new Rectangle( - (int)(rect.X * scale) + drawArea.Center.X, (int)((rect.Y) * scale) - drawArea.Center.Y, - (int)(rect.Width * scale), (int)(rect.Height * scale)); - entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale * scale); + (int)(displayEntity.Rect.X * scale) + drawArea.Center.X, (int)((displayEntity.Rect.Y) * scale) - drawArea.Center.Y, + (int)(displayEntity.Rect.Width * scale), (int)(displayEntity.Rect.Height * scale)); + entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale * scale, rotation: displayEntity.RotationRad); } } public override void DrawPlacing(SpriteBatch spriteBatch, Camera cam) { base.DrawPlacing(spriteBatch, cam); - foreach ((Identifier identifier, Rectangle rect) in DisplayEntities) + Draw( + spriteBatch, + placePosition != Vector2.Zero ? placePosition : Submarine.MouseToWorldGrid(cam, Submarine.MainSub)); + } + + public void Draw(SpriteBatch spriteBatch, Vector2 pos) + { + foreach (var displayEntity in DisplayEntities) { - var entityPrefab = FindByIdentifier(identifier); + var entityPrefab = FindByIdentifier(displayEntity.Identifier); if (entityPrefab == null) { continue; } - Rectangle drawRect = rect; - drawRect.Location += placePosition != Vector2.Zero ? placePosition.ToPoint() : Submarine.MouseToWorldGrid(cam, Submarine.MainSub).ToPoint(); - entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale); + Rectangle drawRect = displayEntity.Rect; + drawRect.Location += pos.ToPoint(); + entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale, rotation: displayEntity.RotationRad); } } @@ -47,9 +54,12 @@ namespace Barotrauma new XAttribute("description", description), new XAttribute("hideinmenus", hideInMenus)); - //move the entities so that their "center of mass" is at {0,0} var assemblyEntities = MapEntity.CopyEntities(entities); + for (int i = 0; i < assemblyEntities.Count && i < entities.Count; i++) + { + assemblyEntities[i].Layer = entities[i].Layer; + } //find wires and items that are contained inside another item //place them at {0,0} to prevent them from messing up the origin of the prefab and to hide them in preview @@ -86,7 +96,7 @@ namespace Barotrauma MathUtils.RoundTowardsClosest(center.Y, Submarine.GridSize.Y) - center.Y - Submarine.GridSize.Y / 2); MapEntity.SelectedList.Clear(); - entities.ForEach(e => MapEntity.AddSelection(e)); + assemblyEntities.ForEach(e => MapEntity.AddSelection(e)); foreach (MapEntity mapEntity in assemblyEntities) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs index a137242ce..75991e4b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs @@ -20,6 +20,8 @@ namespace Barotrauma { if (damage <= 0.0f) { return; } Vector2 particlePos = worldPosition; + Vector2 particleDir = particlePos - WorldPosition; + if (particleDir.LengthSquared() > 0.0001f) { particleDir = Vector2.Normalize(particleDir); } if (!Cells.Any(c => c.IsPointInside(particlePos))) { bool intersectionFound = false; @@ -31,6 +33,7 @@ namespace Barotrauma { intersectionFound = true; particlePos = intersection; + particleDir = edge.GetNormal(cell); break; } } @@ -38,14 +41,15 @@ namespace Barotrauma } } - Vector2 particleDir = particlePos - WorldPosition; - if (particleDir.LengthSquared() > 0.0001f) { particleDir = Vector2.Normalize(particleDir); } int particleAmount = MathHelper.Clamp((int)damage, 1, 10); for (int i = 0; i < particleAmount; i++) { - var particle = GameMain.ParticleManager.CreateParticle("iceshards", + var particle = GameMain.ParticleManager.CreateParticle("iceexplosionsmall", particlePos + Rand.Vector(5.0f), - particleDir * Rand.Range(200.0f, 500.0f) + Rand.Vector(100.0f)); + particleDir * Rand.Range(30.0f, 500.0f) + Rand.Vector(20.0f)); + GameMain.ParticleManager.CreateParticle("iceshards", + particlePos + Rand.Vector(5.0f), + particleDir * Rand.Range(100.0f, 500.0f) + Rand.Vector(100.0f)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 5fe2aeca2..34caf829f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -95,6 +95,7 @@ namespace Barotrauma } } } + foreach (var rects in blockedRects.Values) { foreach (var rect in rects) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 80027c367..1d9f4ecd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -18,7 +18,7 @@ namespace Barotrauma //Maximum number of visible objects drawn at once. Should be large enough to not have an effect during normal gameplay, //but small enough to prevent wrecking performance when zooming out very far - const int MaxVisibleObjects = 500; + const int MaxVisibleObjects = 600; private Rectangle currentGridIndices; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 9366776a9..8b6a6b6fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -362,6 +362,13 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(edge.Point1.X + cell.Translation.X, -(edge.Point1.Y + cell.Translation.Y)), new Vector2(edge.Point2.X + cell.Translation.X, -(edge.Point2.Y + cell.Translation.Y)), edge.NextToCave ? Color.Red : (cell.Body == null ? Color.Cyan * 0.5f : (edge.IsSolid ? Color.White : Color.Gray)), width: edge.NextToCave ? 8 : 1); + + Vector2 normal = edge.GetNormal(cell); + GUI.DrawLine(spriteBatch, + (edge.Center + cell.Translation).FlipY(), + (edge.Center + cell.Translation + normal * 32).FlipY(), + Color.Red * 0.5f, + width: 3); } foreach (Vector2 point in cell.BodyVertices) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index bb2024258..31c08ce71 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -156,17 +156,7 @@ namespace Barotrauma.Lights BoundingBox = rect; this.isHorizontal = isHorizontal; - if (ParentEntity is Structure structure) - { - Debug.Assert(!structure.Removed); - isHorizontal = structure.IsHorizontal; - } - else if (ParentEntity is Item item) - { - Debug.Assert(!item.Removed); - var door = item.GetComponent(); - if (door != null) { isHorizontal = door.IsHorizontal; } - } + Debug.Assert(!ParentEntity.Removed); Vector2[] verts = new Vector2[] { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 9e163b8f1..2a60446e1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -268,8 +268,7 @@ namespace Barotrauma.Lights { if (!light.Enabled) { continue; } if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } - //above the top boundary of the level (in an inactive respawn shuttle?) - if (Level.Loaded != null && light.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (light.ParentBody != null) { light.ParentBody.UpdateDrawPosition(); @@ -696,12 +695,24 @@ namespace Barotrauma.Lights if (diff.LengthSquared() > 20.0f * 20.0f) { losOffset = diff; } float rotation = MathUtils.VectorToAngle(losOffset); + //the visible area stretches to the maximum when the cursor is this far from the character + const float MaxOffset = 256.0f; + const float MinHorizontalScale = 2.2f; + const float MaxHorizontalScale = 2.8f; + const float VerticalScale = 2.5f; + + //Starting point and scale-based modifier that moves the point of origin closer to the edge of the texture if the player moves their mouse further away, or vice versa. + float relativeOriginStartPosition = 0.22f; //Increasing this value moves the origin further behind the character + float originStartPosition = visionCircle.Width * relativeOriginStartPosition; + float relativeOriginLookAtPosModifier = -0.055f; //Increase this value increases how much the vision changes by moving the mouse + float originLookAtPosModifier = visionCircle.Width * relativeOriginLookAtPosModifier; + Vector2 scale = new Vector2( - MathHelper.Clamp(losOffset.Length() / 256.0f, 4.0f, 5.0f), 3.0f); + MathHelper.Clamp(losOffset.Length() / MaxOffset, MinHorizontalScale, MaxHorizontalScale), VerticalScale); spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform * Matrix.CreateScale(new Vector3(GameSettings.CurrentConfig.Graphics.LightMapScale, GameSettings.CurrentConfig.Graphics.LightMapScale, 1.0f))); spriteBatch.Draw(visionCircle, new Vector2(ViewTarget.WorldPosition.X, -ViewTarget.WorldPosition.Y), null, Color.White, rotation, - new Vector2(visionCircle.Width * 0.2f, visionCircle.Height / 2), scale, SpriteEffects.None, 0.0f); + new Vector2(originStartPosition + (scale.X * originLookAtPosModifier), visionCircle.Height / 2), scale, SpriteEffects.None, 0.0f); spriteBatch.End(); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 92beaecdb..74eba1d6d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -461,7 +461,9 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.6f, 1.0f), repBarHolder.RectTransform), onDraw: (sb, component) => { if (location.Reputation == null) { return; } - RoundSummary.DrawReputationBar(sb, component.Rect, location.Reputation.NormalizedValue); + RoundSummary.DrawReputationBar(sb, component.Rect, + location.Reputation.NormalizedValue, + location.Reputation.MinReputation, location.Reputation.MaxReputation); }); new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), repBarHolder.RectTransform), @@ -1128,8 +1130,11 @@ namespace Barotrauma if (connection.LevelData.HasBeaconStation) { - var beaconStationIconStyle = connection.LevelData.IsBeaconActive ? "BeaconStationActive" : "BeaconStationInactive"; - DrawIcon(beaconStationIconStyle, (int)(28 * zoom), connection.LevelData.IsBeaconActive ? beaconStationActiveText : beaconStationInactiveText); + bool beaconActive = + connection.LevelData.IsBeaconActive || + (Level.Loaded?.LevelData == connection.LevelData && Level.Loaded.CheckBeaconActive()); + var beaconStationIconStyle = beaconActive ? "BeaconStationActive" : "BeaconStationInactive"; + DrawIcon(beaconStationIconStyle, (int)(28 * zoom), beaconActive ? beaconStationActiveText : beaconStationInactiveText); } if (connection.Locked) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index bfb858f90..6560a4ea1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -808,8 +808,16 @@ namespace Barotrauma { if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; } if (structure.FlippedY && structure.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } - spriteRotation = MathHelper.ToRadians(structure.Rotation); - rectangleRotation = spriteRotation; + rectangleRotation = MathHelper.ToRadians(structure.Rotation); + + spriteRotation = rectangleRotation; + bool spriteIsFlippedHorizontally = structure.Sprite.effects.HasFlag(SpriteEffects.FlipHorizontally); + bool spriteIsFlippedVertically = structure.Sprite.effects.HasFlag(SpriteEffects.FlipVertically); + if (spriteIsFlippedHorizontally != spriteIsFlippedVertically) + { + spriteRotation = -spriteRotation; + } + if (structure.FlippedX != structure.FlippedY) { rectangleRotation = -rectangleRotation; } break; } @@ -978,6 +986,11 @@ namespace Barotrauma } } + public static void ResetEditingHUD() + { + editingHUD = null; + } + public static void DrawEditor(SpriteBatch spriteBatch, Camera cam) { if (SelectedList.Count == 1) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs index 5007fbecc..dfb0f0e1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs @@ -1,12 +1,32 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections.Generic; namespace Barotrauma { abstract partial class MapEntityPrefab : PrefabWithUintIdentifier { + public RichString CreateTooltipText() + { + LocalizedString name = Category.HasFlag(MapEntityCategory.Legacy) ? TextManager.GetWithVariable("legacyitemformat", "[name]", Name) : Name; + LocalizedString tooltip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{name}‖color:end‖"; + + if (!Description.IsNullOrEmpty()) + { + tooltip += '\n' + Description; + } + + if (IsModded) + { + tooltip = $"{tooltip}\n‖color:{Color.MediumPurple.ToStringHex()}‖{ContentPackage?.Name}‖color:end‖"; + } + + return RichString.Rich(tooltip); + } + + public bool IsModded + => ContentPackage != GameMain.VanillaContent && ContentPackage != null; + public virtual void UpdatePlacing(Camera cam) { if (PlayerInput.SecondaryMouseButtonClicked()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs index 1b3c99d28..ca438606f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -25,8 +25,10 @@ namespace Barotrauma Stream = sound.Stream; Range = element.GetAttributeFloat("range", 1000.0f); Volume = element.GetAttributeFloat("volume", 1.0f); + IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); + FrequencyMultiplierRange = new Vector2(1.0f); - string freqMultAttr = element.GetAttributeString("frequencymultiplier", element.GetAttributeString("frequency", "1.0"))!; + string freqMultAttr = element.GetAttributeString("frequencymultiplier", element.GetAttributeString("frequency", "1.0")); if (!freqMultAttr.Contains(',')) { if (float.TryParse(freqMultAttr, NumberStyles.Any, CultureInfo.InvariantCulture, out float freqMult)) @@ -47,7 +49,6 @@ namespace Barotrauma DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")", contentPackage: element.ContentPackage); } - IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); } public float GetRandomFrequencyMultiplier() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 846499d7e..1db65ab0d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -221,6 +221,12 @@ namespace Barotrauma editor.AddCustomContent(tickBox, 1); } + if (!Layer.IsNullOrEmpty()) + { + var layerText = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)) { MinSize = new Point(0, heightScaled) }, TextManager.AddPunctuation(':', TextManager.Get("editor.layer"), Layer)); + editor.AddCustomContent(layerText, 1); + } + var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), isHorizontal: true) { Stretch = true, @@ -435,13 +441,12 @@ namespace Barotrauma dropShadowOffset.Y = -dropShadowOffset.Y; } - SpriteEffects oldEffects = Prefab.BackgroundSprite.effects; - Prefab.BackgroundSprite.effects ^= SpriteEffects; - Vector2 backGroundOffset = new Vector2( MathUtils.PositiveModulo(-textureOffset.X, Prefab.BackgroundSprite.SourceRect.Width * TextureScale.X * Scale), MathUtils.PositiveModulo(-textureOffset.Y, Prefab.BackgroundSprite.SourceRect.Height * TextureScale.Y * Scale)); + float rotationRad = rotationForSprite(this.rotationRad, Prefab.BackgroundSprite); + Prefab.BackgroundSprite.DrawTiled( spriteBatch, new Vector2(rect.X + rect.Width / 2 + drawOffset.X, -(rect.Y - rect.Height / 2 + drawOffset.Y)), @@ -451,7 +456,8 @@ namespace Barotrauma color: Prefab.BackgroundSpriteColor, textureScale: TextureScale * Scale, startOffset: backGroundOffset, - depth: Math.Max(GetDrawDepth(Prefab.BackgroundSprite.Depth, Prefab.BackgroundSprite), depth + 0.000001f)); + depth: Math.Max(GetDrawDepth(Prefab.BackgroundSprite.Depth, Prefab.BackgroundSprite), depth + 0.000001f), + spriteEffects: Prefab.BackgroundSprite.effects ^ SpriteEffects); if (UseDropShadow) { @@ -464,18 +470,14 @@ namespace Barotrauma color: Color.Black * 0.5f, textureScale: TextureScale * Scale, startOffset: backGroundOffset, - depth: (depth + Prefab.BackgroundSprite.Depth) / 2.0f); + depth: (depth + Prefab.BackgroundSprite.Depth) / 2.0f, + spriteEffects: Prefab.BackgroundSprite.effects ^ SpriteEffects); } - - Prefab.BackgroundSprite.effects = oldEffects; } } if (back == GetRealDepth() > 0.5f) { - SpriteEffects oldEffects = Prefab.Sprite.effects; - Prefab.Sprite.effects ^= SpriteEffects; - Vector2 advanceX = MathUtils.RotatedUnitXRadians(this.rotationRad).FlipY(); Vector2 advanceY = advanceX.YX().FlipX(); if (FlippedX != FlippedY) @@ -483,6 +485,9 @@ namespace Barotrauma advanceX = advanceX.FlipY(); advanceY = advanceY.FlipX(); } + + float sectionSpriteRotationRad = rotationForSprite(this.rotationRad, Prefab.Sprite); + for (int i = 0; i < Sections.Length; i++) { Rectangle drawSection = Sections[i].rect; @@ -528,22 +533,17 @@ namespace Barotrauma pos += rect.Location.ToVector2(); pos = new Vector2(pos.X + rect.Width / 2 + drawOffset.X, -(pos.Y - rect.Height / 2 + drawOffset.Y)); - 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, pos, new Vector2(drawSection.Width, drawSection.Height), - rotation: rotationRad, + rotation: sectionSpriteRotationRad, origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: color, startOffset: sectionOffset, depth: depth, - textureScale: TextureScale * Scale); + textureScale: TextureScale * Scale, + spriteEffects: Prefab.Sprite.effects ^ SpriteEffects); } foreach (var decorativeSprite in Prefab.DecorativeSprites) @@ -551,27 +551,42 @@ namespace Barotrauma if (!spriteAnimState[decorativeSprite].IsActive) { continue; } 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, + Vector2 drawPos = DrawPosition + MathUtils.RotatePoint(offset, -this.rotationRad); + decorativeSprite.Sprite.Draw( + spriteBatch: spriteBatch, + pos: drawPos.FlipY(), + color: color, + rotate: rotation, + scale: decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, + spriteEffect: Prefab.Sprite.effects ^ SpriteEffects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - Prefab.Sprite.Depth), 0.999f)); } - Prefab.Sprite.effects = oldEffects; + } + + static float rotationForSprite(float rotationRad, Sprite sprite) + { + if (sprite.effects.HasFlag(SpriteEffects.FlipHorizontally) != sprite.effects.HasFlag(SpriteEffects.FlipVertically)) + { + rotationRad = -rotationRad; + } + return rotationRad; } if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.5f) { if (Bodies != null) { - for (int i = 0; i < Bodies.Count; i++) + foreach (var body in Bodies) { - Vector2 pos = FarseerPhysics.ConvertUnits.ToDisplayUnits(Bodies[i].Position); + Vector2 pos = ConvertUnits.ToDisplayUnits(body.Position); if (Submarine != null) { pos += Submarine.DrawPosition; } pos.Y = -pos.Y; + var dimensions = bodyDimensions[body]; GUI.DrawRectangle(spriteBatch, pos, - FarseerPhysics.ConvertUnits.ToDisplayUnits(bodyDebugDimensions[i].X), - FarseerPhysics.ConvertUnits.ToDisplayUnits(bodyDebugDimensions[i].Y), - -Bodies[i].Rotation, Color.White); + ConvertUnits.ToDisplayUnits(dimensions.X), + ConvertUnits.ToDisplayUnits(dimensions.Y), + -body.Rotation, Color.White); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 9cf47521f..0b4494a0d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text; namespace Barotrauma { @@ -506,6 +507,47 @@ namespace Barotrauma Hull.ShowHulls = true; } + if (!IsWarningSuppressed(SubEditorScreen.WarningType.NotEnoughContainers)) + { + HashSet missingContainerTags = new(); + foreach (var prefab in ContainerTagPrefab.Prefabs) + { + if (!prefab.IsRecommendedForSub(this) || !prefab.WarnIfLess) { continue; } + + int count = Item.ItemList.Count(i => i.HasTag(prefab.Identifier)); + if (count < prefab.RecommendedAmount) + { + missingContainerTags.Add(prefab); + } + } + + if (missingContainerTags.Any()) + { + StringBuilder sb = new(); + int count = 0; + foreach (var tag in missingContainerTags) + { + sb.AppendLine($"- {tag.Name}"); + count++; + if (missingContainerTags.Count > count && count >= 3) + { + var moreIndicator = TextManager.GetWithVariable( + "upgradeuitooltip.moreindicator", + "[amount]", + (missingContainerTags.Count - count).ToString()).Value; + sb.AppendLine(moreIndicator); + break; + } + } + + errorMsgs.Add(TextManager.GetWithVariable( + "ContainerTagUI.CountWarning", + "[tags]", + sb.ToString()).Value); + warnings.Add(SubEditorScreen.WarningType.NotEnoughContainers); + } + } + if (Info.Type == SubmarineType.Player) { foreach (Item item in Item.ItemList) @@ -521,7 +563,40 @@ namespace Barotrauma break; } } + foreach (Item item in Item.ItemList) + { + if (item.GetComponent() is not OxygenGenerator oxygenGenerator) { continue; } + Dictionary hullOxygenFlow = new Dictionary(); + + foreach (var linkedTo in item.linkedTo) + { + if (linkedTo is not Item linkedItem || linkedItem.GetComponent() is not Vent vent) { continue; } + if (vent.Item.CurrentHull == null) + { + vent.Item.FindHull(); + if (vent.Item.CurrentHull == null) { continue; } + } + float oxygenFlow = oxygenGenerator.GetVentOxygenFlow(vent); + if (!hullOxygenFlow.ContainsKey(vent.Item.CurrentHull)) + { + hullOxygenFlow[vent.Item.CurrentHull] = oxygenFlow; + } + else + { + hullOxygenFlow[vent.Item.CurrentHull] += oxygenFlow; + } + } + foreach ((Hull hull, float oxygenFlow) in hullOxygenFlow) + { + if (oxygenFlow < Hull.OxygenConsumptionSpeed) + { + errorMsgs.Add(TextManager.GetWithVariable("LowOxygenOutputWarning", "[roomname]", + hull.DisplayName).Value); + warnings.Add(SubEditorScreen.WarningType.LowOxygenOutputWarning); + } + } + } if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Human)) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoHumanSpawnpoints)) @@ -554,6 +629,15 @@ namespace Barotrauma warnings.Add(SubEditorScreen.WarningType.NoHiddenContainers); } } + if (Info.Dimensions.X * Physics.DisplayToRealWorldRatio > 80 || + Info.Dimensions.Y * Physics.DisplayToRealWorldRatio > 32) + { + if (!IsWarningSuppressed(SubEditorScreen.WarningType.TooLargeForEndGame)) + { + errorMsgs.Add(TextManager.Get("TooLargeForEndGameWarning").Value); + warnings.Add(SubEditorScreen.WarningType.TooLargeForEndGame); + } + } } else if (Info.Type == SubmarineType.OutpostModule) { @@ -749,7 +833,9 @@ namespace Barotrauma public void ClientEventRead(IReadMessage msg, float sendingTime) { - throw new Exception($"Error while reading a network event for the submarine \"{Info.Name} ({ID})\". Submarines are not even supposed to receive events!"); + Identifier layerIdentifier = msg.ReadIdentifier(); + bool enabled = msg.ReadBoolean(); + SetLayerEnabled(layerIdentifier, enabled); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs index 27242fb85..145f55d0e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs @@ -1,12 +1,52 @@ using FarseerPhysics; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; +using Voronoi2; namespace Barotrauma { partial class SubmarineBody { + + partial void HandleLevelCollisionProjSpecific(Impact impact) + { + float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal); + int particleAmount = (int)Math.Min(wallImpact, 10); + + const float BurstParticleThreshold = 5.0f; + + float velocityFactor = MathHelper.Clamp(wallImpact / 10.0f, 0.0f, 1.0f); + for (int i = 0; i < particleAmount * 5; i++) + { + GameMain.ParticleManager.CreateParticle("iceshards", + ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)), + (Rand.Vector(0.9f) + impact.Normal) * Rand.Range(100.0f, 10000) * velocityFactor); + } + for (int i = 0; i < particleAmount; i++) + { + float particleVelocityMultiplier = Rand.Range(0.0f, 1); + var p = GameMain.ParticleManager.CreateParticle("iceexplosion", + ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)), + (Rand.Vector(0.5f) + impact.Normal) * particleVelocityMultiplier * 500 * velocityFactor); + if (p != null) + { + p.VelocityChangeMultiplier = particleVelocityMultiplier * Rand.Range(0.0f, 1.0f); + p.Size *= Math.Max(particleVelocityMultiplier, 0); + } + } + if (wallImpact > BurstParticleThreshold) + { + for (int i = 0; i < particleAmount; i++) + { + GameMain.ParticleManager.CreateParticle("iceburst", + ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)), + angle: MathUtils.VectorToAngle(impact.Normal.FlipY() + Rand.Vector(0.25f)), speed: 0.0f); + } + } + } + partial void ClientUpdatePosition(float deltaTime) { if (GameMain.Client == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index d48f4c959..264d0aebc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -168,7 +168,7 @@ namespace Barotrauma private bool IsHidden() { - if (!SubEditorScreen.IsLayerVisible(this)) { return false; } + if (!SubEditorScreen.IsLayerVisible(this)) { return true; } if (spawnType == SpawnType.Path) { return (!GameMain.DebugDraw && !ShowWayPoints); @@ -316,6 +316,11 @@ namespace Barotrauma { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Spawnpoint"), font: GUIStyle.LargeFont); + if (!Layer.IsNullOrEmpty()) + { + var layerText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("editor.layer"), Layer)); + } + var spawnTypeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), isHorizontal: true) { Stretch = true, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index ad412a92b..82bb5ee62 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -46,7 +46,7 @@ namespace Barotrauma.Networking { if (localRemovedBans.Contains(bannedPlayer.UniqueIdentifier)) { continue; } - var playerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), ((GUIListBox)BanFrame).Content.RectTransform) { MinSize = new Point(0, 70) }) + var playerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), ((GUIListBox)BanFrame).Content.RectTransform) { MinSize = new Point(0, 70) }, style: "InnerFrame") { UserData = BanFrame }; @@ -80,18 +80,22 @@ namespace Barotrauma.Networking { CanBeFocused = true }; - textBlock.RectTransform.MinSize = new Point( - (int)textBlock.Font.MeasureString(textBlock.Text.SanitizedValue).X, 0); + textBlock.RectTransform.MinSize = new Point(0, (int)textBlock.Font.MeasureString(textBlock.Text.SanitizedValue).Y); - var removeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.4f), topArea.RectTransform), + var removeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.4f), topArea.RectTransform, Anchor.CenterRight), TextManager.Get("BanListRemove"), style: "GUIButtonSmall") { + IgnoreLayoutGroups = true, UserData = bannedPlayer, - OnClicked = RemoveBan + OnClicked = RemoveBan, + Enabled = false }; - topArea.RectTransform.MinSize = new Point(0, (int)(removeButton.Rect.Height * 1.25f)); - - topArea.ForceLayoutRecalculation(); + removeButton.OnAddedToGUIUpdateList += (component) => + { + component.Enabled = GameMain.Client?.HasPermission(ClientPermissions.Unban) ?? false; + }; + topArea.RectTransform.MinSize = new Point(0, Math.Max(textBlock.RectTransform.MinSize.Y, removeButton.RectTransform.MinSize.Y)); + topArea.RectTransform.IsFixedSize = true; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform), bannedPlayer.ExpirationTime.TryUnwrap(out var expirationTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 318176714..4d31e3c57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -14,6 +14,17 @@ namespace Barotrauma.Networking set; } + // Players can boost per-user volume by 200% + public const float MaxVoiceChatBoost = 2.0f; + + private float voiceVolume = 1f; + + public float VoiceVolume + { + get => voiceVolume; + set => voiceVolume = Math.Clamp(value, 0f, MaxVoiceChatBoost); + } + private SoundChannel radioNoiseChannel; private float radioNoise; @@ -23,7 +34,6 @@ namespace Barotrauma.Networking set { radioNoise = MathHelper.Clamp(value, 0.0f, 1.0f); } } - private bool mutedLocally; public bool MutedLocally { @@ -86,6 +96,17 @@ namespace Barotrauma.Networking float dist = Vector3.Distance(new Vector3(character.WorldPosition, 0.0f), GameMain.SoundManager.ListenerPosition); gain = 1.0f - MathUtils.InverseLerp(VoipSound.Near, VoipSound.Far, dist); } + if (!VoipSound.UsingRadio) + { + //emulate the "garbling" of the text chat + //this in a sense means the volume diminishes exponentially when close to the maximum range of the sound + //(diminished by both the garbling and the distance attenuation) + + //which is good, because we want the voice chat to become unintelligible close to the max range, + //and we need to heavily reduce the volume to do that (otherwise it's just quiet, but still intelligible) + float garbleAmount = ChatMessage.GetGarbleAmount(Character.Controlled, character, ChatMessage.SpeakRangeVOIP); + gain *= 1.0f - garbleAmount; + } if (RadioNoise > 0.0f) { noiseGain = gain * RadioNoise; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs index 8ce5ecdea..6ba6e36a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs @@ -21,7 +21,12 @@ namespace Barotrauma DebugConsole.Log($"Received entity removal message for \"{entity}\"."); if (entity is Item item && item.Container?.GetComponent() != null) { - GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + item.Prefab.Identifier); + if (item.Prefab.ContentPackage == ContentPackageManager.VanillaCorePackage && + /* we don't need info of every deconstructed item, we can get a good sample size just by logging 5% */ + Rand.Range(0.0f, 1.0f) < 0.05f) + { + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + item.Prefab.Identifier); + } } entity.Remove(); } @@ -45,7 +50,12 @@ namespace Barotrauma { if (newItem.Container?.GetComponent() != null) { - GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + newItem.Prefab.Identifier); + if (newItem.Prefab.ContentPackage == ContentPackageManager.VanillaCorePackage && + /* we don't need info of every fabricated item, we can get a good sample size just by logging 5% */ + Rand.Range(0.0f, 1.0f) < 0.05f) + { + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + newItem.Prefab.Identifier); + } } receivedEvents.Add((newItem, false)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 705f8d1b0..1f115ec0f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -89,7 +89,7 @@ namespace Barotrauma.Networking public readonly List ServerSubmarines = new List(); - public string ServerName { get; private set; } + public string ServerName => ServerSettings.ServerName; private bool canStart; @@ -270,11 +270,11 @@ namespace Barotrauma.Networking otherClients = new List(); - ServerSettings = new ServerSettings(this, "Server", 0, 0, 0, false, false); + ServerSettings = new ServerSettings(this, serverName, 0, 0, 0, false, false); Voting = new Voting(); serverEndpoints = endpoints; - InitiateServerJoin(serverName); + InitiateServerJoin(); //ServerLog = new ServerLog(""); @@ -289,7 +289,7 @@ namespace Barotrauma.Networking return serverInfo; } - private void InitiateServerJoin(string hostName) + private void InitiateServerJoin() { LastClientListUpdateID = 0; @@ -306,8 +306,6 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.ChatInput.Enabled = false; } - ServerName = hostName; - myCharacter = Character.Controlled; ChatMessage.LastID = 0; @@ -318,9 +316,8 @@ namespace Barotrauma.Networking CoroutineManager.StartCoroutine(WaitForStartingInfo(), "WaitForStartingInfo"); } - public void SetLobbyPublic(bool isPublic) + public static void SetLobbyPublic(bool isPublic) { - GameMain.NetLobbyScreen.SetPublic(isPublic); SteamManager.SetLobbyPublic(isPublic); } @@ -723,7 +720,7 @@ namespace Barotrauma.Networking if (readyToStart && !CoroutineManager.IsCoroutineRunning("WaitForStartRound")) { - CoroutineManager.StartCoroutine(GameMain.NetLobbyScreen.WaitForStartRound(startButton: null), "WaitForStartRound"); + CoroutineManager.StartCoroutine(NetLobbyScreen.WaitForStartRound(startButton: null), "WaitForStartRound"); } break; case ServerPacketHeader.STARTGAME: @@ -1096,7 +1093,7 @@ namespace Barotrauma.Networking var prevContentPackages = ClientPeer.ServerContentPackages; //decrement lobby update ID to make sure we update the lobby when we reconnect GameMain.NetLobbyScreen.LastUpdateID--; - InitiateServerJoin(ServerName); + InitiateServerJoin(); if (ClientPeer != null) { //restore the previous list of content packages so we can reconnect immediately without having to recheck that the packages match @@ -1189,7 +1186,7 @@ namespace Barotrauma.Networking { if (!CoroutineManager.IsCoroutineRunning("WaitForStartingInfo")) { - InitiateServerJoin(ServerName); + InitiateServerJoin(); yield return new WaitForSeconds(5.0f); } yield return new WaitForSeconds(0.5f); @@ -1253,7 +1250,7 @@ namespace Barotrauma.Networking if (!(this.permittedConsoleCommands.Any(c => !permittedConsoleCommands.Contains(c)) || permittedConsoleCommands.Any(c => !this.permittedConsoleCommands.Contains(c)))) { - if (newPermissions == permissions) return; + if (newPermissions == permissions) { return; } } bool refreshCampaignUI = permissions.HasFlag(ClientPermissions.ManageCampaign) != newPermissions.HasFlag(ClientPermissions.ManageCampaign) || @@ -1335,6 +1332,8 @@ namespace Barotrauma.Networking } GameMain.NetLobbyScreen.RefreshEnabledElements(); + //close settings menu in case it was open + ServerSettings.Close(); OnPermissionChanged.Invoke(new PermissionChangedEvent(permissions, this.permittedConsoleCommands)); } @@ -2025,18 +2024,15 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.LastUpdateID = updateID; ServerSettings.ServerLog.ServerName = ServerSettings.ServerName; - - if (!GameMain.NetLobbyScreen.ServerName.Selected) { GameMain.NetLobbyScreen.ServerName.Text = ServerSettings.ServerName; } - if (!GameMain.NetLobbyScreen.ServerMessage.Selected) { GameMain.NetLobbyScreen.ServerMessage.Text = ServerSettings.ServerMessageText; } GameMain.NetLobbyScreen.UsingShuttle = usingShuttle; - if (!allowSubVoting) { GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, GameMain.NetLobbyScreen.SubList); } + if (!allowSubVoting || GameMain.NetLobbyScreen.SelectedSub == null) { GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, GameMain.NetLobbyScreen.SubList); } GameMain.NetLobbyScreen.TrySelectSub(selectShuttleName, selectShuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox); GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability); GameMain.NetLobbyScreen.SetTraitorDangerLevel(traitorDangerLevel); - GameMain.NetLobbyScreen.SetMissionType(missionType); + GameMain.NetLobbyScreen.LevelSeed = levelSeed; GameMain.NetLobbyScreen.SelectMode(modeIndex); if (isInitialUpdate && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) @@ -2053,7 +2049,6 @@ namespace Barotrauma.Networking } GameMain.NetLobbyScreen.SetAllowSpectating(allowSpectating); - GameMain.NetLobbyScreen.LevelSeed = levelSeed; GameMain.NetLobbyScreen.SetLevelDifficulty(levelDifficulty); GameMain.NetLobbyScreen.SetBotSpawnMode(botSpawnMode); GameMain.NetLobbyScreen.SetBotCount(botCount); @@ -2574,12 +2569,17 @@ namespace Barotrauma.Networking } public override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null) + { + CreateEntityEvent(entity, extraData, requireControlledCharacter: true); + } + + public void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData, bool requireControlledCharacter) { if (entity is not IClientSerializable clientSerializable) { throw new InvalidCastException($"Entity is not {nameof(IClientSerializable)}"); } - EntityEventManager.CreateEvent(clientSerializable, extraData); + EntityEventManager.CreateEvent(clientSerializable, extraData, requireControlledCharacter); } public bool HasPermission(ClientPermissions permission) @@ -2703,9 +2703,19 @@ namespace Barotrauma.Networking public override void AddChatMessage(ChatMessage message) { - base.AddChatMessage(message); - if (string.IsNullOrEmpty(message.Text)) { return; } + if (message.Sender != null && !message.Sender.IsDead) + { + if (message.Text.IsNullOrEmpty()) + { + message.Sender.ShowTextlessSpeechBubble(2.0f, message.Color); + + } + else + { + message.Sender.ShowSpeechBubble(message.Color, message.Text); + } + } GameMain.NetLobbyScreen.NewChatMessage(message); chatBox.AddMessage(message); } @@ -2894,7 +2904,7 @@ namespace Barotrauma.Networking ClientPeer.Send(msg, DeliveryMethod.Reliable); } - public bool SpectateClicked(GUIButton button, object _) + public bool JoinOnGoingClicked(GUIButton button, object _) { MultiPlayerCampaign campaign = GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? @@ -3283,17 +3293,36 @@ namespace Barotrauma.Networking private void CreateSelectionRelatedButtons(Client client, GUIComponent frame) { - var content = new GUIFrame(new RectTransform(new Vector2(1f, 1.0f - frame.RectTransform.RelativeSize.Y), frame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), - style: null); + var content = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1.0f - frame.RectTransform.RelativeSize.Y), frame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), childAnchor: Anchor.TopCenter); - var mute = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform, Anchor.TopCenter), + var mute = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.2f), content.RectTransform, Anchor.TopCenter), TextManager.Get("Mute")) { Selected = client.MutedLocally, OnSelected = (tickBox) => { client.MutedLocally = tickBox.Selected; return true; } }; + + var volumeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.45f), content.RectTransform, Anchor.TopCenter), isHorizontal: false); - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), content.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomLeft) + var volumeTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var label = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), volumeTextLayout.RectTransform), TextManager.Get("VoiceChatVolume")); + var percentageText = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), volumeTextLayout.RectTransform), ToolBox.GetFormattedPercentage(client.VoiceVolume), textAlignment: Alignment.Right); + + var volumeSlider = new GUIScrollBar(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), barSize: 0.1f, style: "GUISlider") + { + Range = new Vector2(0f, 1f), + BarScroll = client.VoiceVolume / Client.MaxVoiceChatBoost, + OnMoved = (_, barScroll) => + { + float newVolume = barScroll * Client.MaxVoiceChatBoost; + + client.VoiceVolume = newVolume; + percentageText.Text = ToolBox.GetFormattedPercentage(newVolume); + return true; + } + }; + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft) { RelativeSpacing = 0.05f, Stretch = true @@ -3327,7 +3356,7 @@ namespace Barotrauma.Networking TextManager.Get("Ban"), style: "GUIButtonSmall") { UserData = client, - OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.BanPlayer(client); return false; } + OnClicked = (btn, userdata) => { NetLobbyScreen.BanPlayer(client); return false; } }; } if (HasPermission(ClientPermissions.Kick) && client.AllowKicking) @@ -3336,7 +3365,7 @@ namespace Barotrauma.Networking TextManager.Get("Kick"), style: "GUIButtonSmall") { UserData = client, - OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.KickPlayer(client); return false; } + OnClicked = (btn, userdata) => { NetLobbyScreen.KickPlayer(client); return false; } }; } else if (ServerSettings.AllowVoteKick && client.AllowKicking) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 3db7e69c3..c50dc2d86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -42,16 +42,17 @@ namespace Barotrauma.Networking thisClient = client; } - public void CreateEvent(IClientSerializable entity, NetEntityEvent.IData extraData = null) + public void CreateEvent(IClientSerializable entity, NetEntityEvent.IData extraData = null, bool requireControlledCharacter = true) { - if (GameMain.Client?.Character == null) { return; } + if (GameMain.Client == null) { return; } + if (requireControlledCharacter && GameMain.Client.Character == null) { return; } if (!ValidateEntity(entity)) { return; } var newEvent = new ClientEntityEvent( entity, eventId: (UInt16)(ID + 1), - characterStateId: GameMain.Client.Character.LastNetworkUpdateID); + characterStateId: GameMain.Client.Character?.LastNetworkUpdateID ?? Entity.NullEntityID); if (extraData != null) { newEvent.SetData(extraData); } for (int i = events.Count - 1; i >= 0; i--) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index f357f1a2b..5ecc23923 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -18,6 +18,8 @@ namespace Barotrauma.Networking public ImmutableArray ServerContentPackages { get; set; } = ImmutableArray.Empty; + public bool AllowModDownloads { get; private set; } = true; + public readonly record struct Callbacks( Callbacks.MessageCallback OnMessageReceived, Callbacks.DisconnectCallback OnDisconnect, @@ -151,6 +153,7 @@ namespace Barotrauma.Networking if (!ContentPackageOrderReceived) { ServerContentPackages = orderPacket.ContentPackages; + AllowModDownloads = orderPacket.AllowModDownloads; if (ServerContentPackages.Length == 0) { string errorMsg = "Error in ContentPackageOrder message: list of content packages enabled on the server was empty."; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index 5441869bd..bd219b5f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -51,20 +51,38 @@ namespace Barotrauma.Networking { if (Character.Controlled != null || (GameMain.GameSession is not { IsRunning: true })) { return; } - LocalizedString text = TextManager.Get("respawnquestionprompt"); + LocalizedString text; + GUIMessageBox respawnPrompt; + if (SkillLossPercentageOnImmediateRespawn > 0) + { + // Respawn asap with extra skill loss? + text = TextManager.GetWithVariable("respawnquestionprompt", "[percentage]", ((int)Math.Round(SkillLossPercentageOnImmediateRespawn)).ToString()); + respawnPrompt = new GUIMessageBox( + TextManager.Get("tutorial.tryagainheader"), text, + new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }) + { + UserData = "respawnquestionprompt" + }; + } + else + { + // Respawn asap? + text = TextManager.Get("respawnquestionpromptnoloss"); + respawnPrompt = new GUIMessageBox( + TextManager.Get("tutorial.tryagainheader"), text, + new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawnnoloss"), TextManager.Get("respawnquestionpromptwait") }) + { + UserData = "respawnquestionprompt" + }; + } if (SkillLossPercentageOnDeath > 0) { + // You have died... etc added BEFORE the above text text = TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", ((int)SkillLossPercentageOnDeath).ToString()) + "\n\n" + text; }; - var respawnPrompt = new GUIMessageBox( - TextManager.Get("tutorial.tryagainheader"), text, - new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }) - { - UserData = "respawnquestionprompt" - }; respawnPrompt.Buttons[0].OnClicked += (btn, userdata) => { GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index fa077332e..b7252310c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using Barotrauma.Steam; namespace Barotrauma.Networking { @@ -28,9 +27,9 @@ namespace Barotrauma.Networking { get { - if (GUIComponent == null) return null; - else if (GUIComponent is GUITickBox tickBox) return tickBox.Selected; - else if (GUIComponent is GUITextBox textBox) return textBox.Text; + if (GUIComponent == null) { return null; } + else if (GUIComponent is GUITickBox tickBox) { return tickBox.Selected; } + else if (GUIComponent is GUITextBox textBox) { return textBox.Text; } else if (GUIComponent is GUIScrollBar scrollBar) { if (property.PropertyType == typeof(int)) @@ -40,19 +39,23 @@ namespace Barotrauma.Networking return scrollBar.BarScrollValue; } - else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) return radioButtonGroup.Selected; - else if (GUIComponent is GUIDropDown dropdown) return dropdown.SelectedData; + else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) { return radioButtonGroup.Selected; } + else if (GUIComponent is GUIDropDown dropdown) { return dropdown.SelectedData; } else if (GUIComponent is GUINumberInput numInput) { if (numInput.InputType == NumberType.Int) { return numInput.IntValue; } else { return numInput.FloatValue; } } + else if (GUIComponent is IGUISelectionCarouselAccessor selectionCarousel) + { + return selectionCarousel.GetSelectedElement(); + } return null; } set { - if (GUIComponent == null) return; - else if (GUIComponent is GUITickBox tickBox) tickBox.Selected = (bool)value; - else if (GUIComponent is GUITextBox textBox) textBox.Text = (string)value; + if (GUIComponent == null) { return; } + else if (GUIComponent is GUITickBox tickBox) { tickBox.Selected = (bool)value; } + else if (GUIComponent is GUITextBox textBox) { textBox.Text = (string)value; } else if (GUIComponent is GUIScrollBar scrollBar) { if (value is int i) @@ -63,9 +66,10 @@ namespace Barotrauma.Networking { scrollBar.BarScrollValue = (float)value; } + scrollBar.OnMoved?.Invoke(scrollBar, scrollBar.BarScroll); } - else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) radioButtonGroup.Selected = (int)value; - else if (GUIComponent is GUIDropDown dropdown) dropdown.SelectItem(value); + else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) { radioButtonGroup.Selected = (int)value; } + else if (GUIComponent is GUIDropDown dropdown) { dropdown.SelectItem(value); } else if (GUIComponent is GUINumberInput numInput) { if (numInput.InputType == NumberType.Int) @@ -77,6 +81,10 @@ namespace Barotrauma.Networking numInput.FloatValue = (float)value; } } + else if (GUIComponent is IGUISelectionCarouselAccessor selectionCarousel) + { + selectionCarousel.SelectElement(value); + } } } @@ -84,7 +92,7 @@ namespace Barotrauma.Networking { get { - if (GUIComponent == null) return false; + if (GUIComponent == null) { return false; } return !PropEquals(TempValue, GUIComponentValue); } } @@ -131,35 +139,45 @@ namespace Barotrauma.Networking } } - ReadMonsterEnabled(incMsg); + if (ReadMonsterEnabled(incMsg)) + { + if (monstersEnabledPanel is { Visible: true }) + { + //refresh panel if someone else changes it + monstersEnabledPanel.Parent?.RemoveChild(monstersEnabledPanel); + monstersEnabledPanel = CreateMonstersEnabledPanel(); + monstersEnabledPanel.Visible = true; + } + } BanList.ClientAdminRead(incMsg); + GameMain.NetLobbyScreen?.RefreshPlaystyleIcons(); } public void ClientRead(IReadMessage incMsg) { NetFlags requiredFlags = (NetFlags)incMsg.ReadByte(); - if (requiredFlags.HasFlag(NetFlags.Name)) - { - ServerName = incMsg.ReadString(); - } - - if (requiredFlags.HasFlag(NetFlags.Message)) - { - ServerMessageText = incMsg.ReadString(); - } PlayStyle = (PlayStyle)incMsg.ReadByte(); MaxPlayers = incMsg.ReadByte(); HasPassword = incMsg.ReadBoolean(); IsPublic = incMsg.ReadBoolean(); - GameMain.Client?.SetLobbyPublic(IsPublic); + GameClient.SetLobbyPublic(IsPublic); AllowFileTransfers = incMsg.ReadBoolean(); incMsg.ReadPadBits(); TickRate = incMsg.ReadRangedInteger(1, 60); if (requiredFlags.HasFlag(NetFlags.Properties)) { - ReadExtraCargo(incMsg); + if (ReadExtraCargo(incMsg)) + { + if (extraCargoPanel is { Visible: true }) + { + //refresh panel if someone else changes it + extraCargoPanel.Parent?.RemoveChild(extraCargoPanel); + extraCargoPanel = CreateExtraCargoPanel(); + extraCargoPanel.Visible = true; + } + } } if (requiredFlags.HasFlag(NetFlags.HiddenSubs)) @@ -180,13 +198,7 @@ namespace Barotrauma.Networking NetFlags dataToSend, int? missionTypeOr = null, int? missionTypeAnd = null, - float? levelDifficulty = null, - bool? autoRestart = null, - float? traitorProbability = null, - int traitorDangerLevel = 0, - int botCount = 0, - int botSpawnMode = 0, - bool? useRespawnShuttle = null) + int traitorDangerLevel = 0) { if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return; } @@ -196,24 +208,6 @@ namespace Barotrauma.Networking outMsg.WriteByte((byte)dataToSend); - if (dataToSend.HasFlag(NetFlags.Name)) - { - if (GameMain.NetLobbyScreen.ServerName.Text != ServerName) - { - ServerName = GameMain.NetLobbyScreen.ServerName.Text; - } - outMsg.WriteString(ServerName); - } - - if (dataToSend.HasFlag(NetFlags.Message)) - { - if (GameMain.NetLobbyScreen.ServerMessage.Text != ServerMessageText) - { - ServerMessageText = GameMain.NetLobbyScreen.ServerMessage.Text; - } - outMsg.WriteString(ServerMessageText); - } - if (dataToSend.HasFlag(NetFlags.Properties)) { //TODO: split this up? @@ -245,966 +239,29 @@ namespace Barotrauma.Networking { outMsg.WriteRangedInteger(missionTypeOr ?? (int)Barotrauma.MissionType.None, 0, (int)Barotrauma.MissionType.All); outMsg.WriteRangedInteger(missionTypeAnd ?? (int)Barotrauma.MissionType.All, 0, (int)Barotrauma.MissionType.All); - - outMsg.WriteBoolean(traitorProbability != null); - outMsg.WriteSingle(traitorProbability ?? 0.0f); outMsg.WriteByte((byte)(traitorDangerLevel + 1)); - - outMsg.WriteByte((byte)(botCount + 1)); - outMsg.WriteByte((byte)(botSpawnMode + 1)); - - outMsg.WriteSingle(levelDifficulty ?? -1000.0f); - - outMsg.WriteBoolean(useRespawnShuttle != null); - outMsg.WriteBoolean(useRespawnShuttle ?? false); - - outMsg.WriteBoolean(autoRestart != null); - outMsg.WriteBoolean(autoRestart ?? false); - outMsg.WritePadBits(); } if (dataToSend.HasFlag(NetFlags.LevelSeed)) { - outMsg.WriteString(GameMain.NetLobbyScreen.SeedBox.Text); + outMsg.WriteString(GameMain.NetLobbyScreen.LevelSeedBox.Text); } GameMain.Client.ClientPeer.Send(outMsg, DeliveryMethod.Reliable); } - //GUI stuff - private GUIFrame settingsFrame; - private GUIFrame[] settingsTabs; - private GUIButton[] tabButtons; - private int settingsTabIndex; - - private GUIDropDown karmaPresetDD; - private GUIComponent karmaSettingsBlocker; - - enum SettingsTab - { - General, - Rounds, - Antigriefing, - Banlist - } - private NetPropertyData GetPropertyData(string name) { - return netProperties.First(p => p.Value.Name == name).Value; - } - - public void AssignGUIComponent(string propertyName, GUIComponent component) - { - GetPropertyData(propertyName).AssignGUIComponent(component); - } - - public void AddToGUIUpdateList() - { - if (GUI.DisableHUD) return; - - settingsFrame?.AddToGUIUpdateList(); - } - - private void CreateSettingsFrame() - { - foreach (NetPropertyData prop in netProperties.Values) + var matchingProperty = netProperties.FirstOrDefault(p => p.Value.Name == name); + if (matchingProperty.Equals(default(KeyValuePair))) { - prop.TempValue = prop.Value; - } - - //background frame - settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, settingsFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - - new GUIButton(new RectTransform(Vector2.One, settingsFrame.RectTransform), "", style: null).OnClicked += (btn, userData) => - { - if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) { ToggleSettingsFrame(btn, userData); } - return true; - }; - - new GUIButton(new RectTransform(Vector2.One, settingsFrame.RectTransform), "", style: null) - { - OnClicked = ToggleSettingsFrame - }; - - //center frames - GUIFrame innerFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.85f), settingsFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 430) }); - GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), innerFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("Settings"), font: GUIStyle.LargeFont); - - var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), paddedFrame.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - var tabContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), paddedFrame.RectTransform), style: "InnerFrame"); - - //tabs - LocalizedString[] tabNames = - Enum.GetValues(typeof(SettingsTab)).Cast() - .Select(tv => TextManager.Get("ServerSettings" + tv + "Tab")).ToArray(); - settingsTabs = new GUIFrame[tabNames.Length]; - tabButtons = new GUIButton[tabNames.Length]; - for (int i = 0; i < tabNames.Length; i++) - { - settingsTabs[i] = new GUIFrame(new RectTransform(Vector2.One, tabContent.RectTransform, Anchor.Center), style: null); - tabButtons[i] = new GUIButton(new RectTransform(new Vector2(0.2f, 1.2f), buttonArea.RectTransform), tabNames[i], style: "GUITabButton") - { - UserData = i, - OnClicked = SelectSettingsTab - }; - } - GUITextBlock.AutoScaleAndNormalize(tabButtons.Select(b => b.TextBlock)); - SelectSettingsTab(tabButtons[0], 0); - - //"Close" - var buttonContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.05f), paddedFrame.RectTransform), style: null); - var closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Close")) - { - OnClicked = ToggleSettingsFrame - }; - - - //-------------------------------------------------------------------------------- - // server settings - //-------------------------------------------------------------------------------- - - var serverTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.General].RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - //*********************************************** - - // Language - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("Language"), font: GUIStyle.SubHeadingFont); - var languageDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform)); - foreach (var language in ServerLanguageOptions.Options) - { - languageDD.AddItem(language.Label, language.Identifier); - } - GetPropertyData(nameof(Language)).AssignGUIComponent(languageDD); - - //changing server visibility on the fly is not supported in dedicated servers - if (GameMain.Client?.ClientPeer is not LidgrenClientPeer) - { - var isPublic = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), - TextManager.Get("publicserver")) - { - ToolTip = TextManager.Get("publicservertooltip") - }; - GetPropertyData(nameof(IsPublic)).AssignGUIComponent(isPublic); - } - - // Sub Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsSubSelection"), font: GUIStyle.SubHeadingFont); - var selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - GUIRadioButtonGroup selectionMode = new GUIRadioButtonGroup(); - for (int i = 0; i < 3; i++) - { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUIStyle.SmallFont, style: "GUIRadioButton"); - selectionMode.AddRadioButton(i, selectionTick); - } - selectionFrame.RectTransform.NonScaledSize = new Point(selectionFrame.Rect.Width, selectionFrame.Children.First().Rect.Height); - selectionFrame.RectTransform.IsFixedSize = true; - - GetPropertyData(nameof(SubSelectionMode)).AssignGUIComponent(selectionMode); - - // Mode Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsModeSelection"), font: GUIStyle.SubHeadingFont); - selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - selectionMode = new GUIRadioButtonGroup(); - for (int i = 0; i < 3; i++) - { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUIStyle.SmallFont, style: "GUIRadioButton"); - selectionMode.AddRadioButton(i, selectionTick); - } - selectionFrame.RectTransform.NonScaledSize = new Point(selectionFrame.Rect.Width, selectionFrame.Children.First().Rect.Height); - selectionFrame.RectTransform.IsFixedSize = true; - GetPropertyData(nameof(ModeSelectionMode)).AssignGUIComponent(selectionMode); - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), style: "HorizontalLine"); - - //*********************************************** - - var voiceChatEnabled = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), - TextManager.Get("ServerSettingsVoiceChatEnabled")); - GetPropertyData(nameof(VoiceChatEnabled)).AssignGUIComponent(voiceChatEnabled); - - //*********************************************** - - LocalizedString autoRestartDelayLabel = TextManager.Get("ServerSettingsAutoRestartDelay") + " "; - var startIntervalText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), autoRestartDelayLabel); - var startIntervalSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), barSize: 0.1f, style: "GUISlider") - { - UserData = startIntervalText, - Step = 0.05f, - OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - GUITextBlock text = scrollBar.UserData as GUITextBlock; - text.Text = autoRestartDelayLabel + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); - return true; - } - }; - startIntervalSlider.Range = new Vector2(10.0f, 300.0f); - GetPropertyData(nameof(AutoRestartInterval)).AssignGUIComponent(startIntervalSlider); - startIntervalSlider.OnMoved(startIntervalSlider, startIntervalSlider.BarScroll); - - //*********************************************** - - var startWhenClientsReady = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), - TextManager.Get("ServerSettingsStartWhenClientsReady")); - GetPropertyData(nameof(StartWhenClientsReady)).AssignGUIComponent(startWhenClientsReady); - - CreateLabeledSlider(serverTab, "ServerSettingsStartWhenClientsReadyRatio", out GUIScrollBar slider, out GUITextBlock sliderLabel); - LocalizedString clientsReadyRequiredLabel = sliderLabel.Text; - slider.Step = 0.2f; - slider.Range = new Vector2(0.5f, 1.0f); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - ((GUITextBlock)scrollBar.UserData).Text = clientsReadyRequiredLabel.Replace("[percentage]", ((int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f)).ToString()); - return true; - }; - GetPropertyData(nameof(StartWhenClientsReadyRatio)).AssignGUIComponent(slider); - slider.OnMoved(slider, slider.BarScroll); - - //*********************************************** - - var allowSpecBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsAllowSpectating")); - GetPropertyData(nameof(AllowSpectating)).AssignGUIComponent(allowSpecBox); - - var shareSubsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsShareSubFiles")); - GetPropertyData(nameof(AllowFileTransfers)).AssignGUIComponent(shareSubsBox); - - var randomizeLevelBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsRandomizeSeed")); - GetPropertyData(nameof(RandomizeSeed)).AssignGUIComponent(randomizeLevelBox); - - var saveLogsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsSaveLogs")) - { - OnSelected = (GUITickBox) => - { - //TODO: fix? - //showLogButton.Visible = SaveServerLogs; - return true; - } - }; - GetPropertyData(nameof(SaveServerLogs)).AssignGUIComponent(saveLogsBox); - - //-------------------------------------------------------------------------------- - // game settings - //-------------------------------------------------------------------------------- - - var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)); - var roundsContent = new GUIListBox(new RectTransform(Vector2.One, roundsTab.RectTransform, Anchor.Center), style: "GUIListBoxNoBorder").Content; - - GUILayoutGroup playStyleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), roundsContent.RectTransform)); - // Play Style Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), playStyleLayout.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont); - var playstyleList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), playStyleLayout.RectTransform)) - { - AutoHideScrollBar = true, - UseGridLayout = true - }; - playstyleList.Padding *= 2.0f; - - List playStyleTickBoxes = new List(); - GUIRadioButtonGroup selectionPlayStyle = new GUIRadioButtonGroup(); - foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) - { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.32f, 0.49f), playstyleList.Content.RectTransform), TextManager.Get("servertag." + playStyle), font: GUIStyle.SmallFont, style: "GUIRadioButton") - { - ToolTip = TextManager.Get("servertagdescription." + playStyle) - }; - selectionPlayStyle.AddRadioButton((int)playStyle, selectionTick); - playStyleTickBoxes.Add(selectionTick); - } - GetPropertyData(nameof(PlayStyle)).AssignGUIComponent(selectionPlayStyle); - GUITextBlock.AutoScaleAndNormalize(playStyleTickBoxes.Select(t => t.TextBlock)); - playstyleList.RectTransform.MinSize = new Point(0, (int)(playstyleList.Content.Children.First().Rect.Height * 2.0f + playstyleList.Padding.Y + playstyleList.Padding.W)); - - GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), roundsContent.RectTransform)) - { - Stretch = true - }; - - var endVoteBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), sliderLayout.RectTransform), - TextManager.Get("ServerSettingsEndRoundVoting")); - GetPropertyData(nameof(AllowEndVoting)).AssignGUIComponent(endVoteBox); - - CreateLabeledSlider(sliderLayout, "ServerSettingsEndRoundVotesRequired", out slider, out sliderLabel); - - LocalizedString endRoundLabel = sliderLabel.Text; - slider.Step = 0.2f; - slider.Range = new Vector2(0.5f, 1.0f); - GetPropertyData(nameof(EndVoteRequiredRatio)).AssignGUIComponent(slider); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - ((GUITextBlock)scrollBar.UserData).Text = endRoundLabel + " " + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; - return true; - }; - slider.OnMoved(slider, slider.BarScroll); - - LocalizedString skillLossLabel = TextManager.Get("ServerSettingsSkillLossPercentageOnDeath"); - var skillLossText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), roundsContent.RectTransform), skillLossLabel); - var skillLossSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), roundsContent.RectTransform), barSize: 0.1f, style: "GUISlider") - { - UserData = skillLossText, - Range = new Vector2(0, 100), - StepValue = 1, - OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - GUITextBlock text = scrollBar.UserData as GUITextBlock; - text.Text = TextManager.AddPunctuation( - ':', - skillLossLabel, - TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString())); - return true; - } - }; - GetPropertyData(nameof(SkillLossPercentageOnDeath)).AssignGUIComponent(skillLossSlider); - skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll); - - var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), sliderLayout.RectTransform), - TextManager.Get("ServerSettingsAllowRespawning")); - GetPropertyData(nameof(AllowRespawn)).AssignGUIComponent(respawnBox); - - CreateLabeledSlider(sliderLayout, "ServerSettingsRespawnInterval", out slider, out sliderLabel); - LocalizedString intervalLabel = sliderLabel.Text; - slider.Range = new Vector2(10.0f, 600.0f); - slider.StepValue = 10.0f; - GetPropertyData(nameof(RespawnInterval)).AssignGUIComponent(slider); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - GUITextBlock text = scrollBar.UserData as GUITextBlock; - text.Text = intervalLabel + " " + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); - return true; - }; - slider.OnMoved(slider, slider.BarScroll); - - var respawnLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), sliderLayout.RectTransform), - isHorizontal: true); - - var minRespawnLayout - = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), respawnLayout.RectTransform)); - - var minRespawnText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), minRespawnLayout.RectTransform), "") - { - ToolTip = TextManager.Get("ServerSettingsMinRespawnToolTip") - }; - - LocalizedString minRespawnLabel = TextManager.Get("ServerSettingsMinRespawn") + " "; - CreateLabeledSlider(minRespawnLayout, "", out slider, out sliderLabel); - sliderLabel.RectTransform.RelativeSize = Vector2.Zero; - slider.RectTransform.RelativeSize = new Vector2(1.0f, 0.5f); - slider.ToolTip = minRespawnText.ToolTip; - slider.UserData = minRespawnText; - slider.Step = 0.1f; - slider.Range = new Vector2(0.0f, 1.0f); - GetPropertyData(nameof(MinRespawnRatio)).AssignGUIComponent(slider); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - ((GUITextBlock)scrollBar.UserData).Text = minRespawnLabel + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; - return true; - }; - slider.OnMoved(slider, MinRespawnRatio); - - var respawnDurationLayout - = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), respawnLayout.RectTransform)); - - var respawnDurationText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), respawnDurationLayout.RectTransform), "") - { - ToolTip = TextManager.Get("ServerSettingsRespawnDurationToolTip") - }; - - LocalizedString respawnDurationLabel = TextManager.Get("ServerSettingsRespawnDuration") + " "; - CreateLabeledSlider(respawnDurationLayout, "", out slider, out sliderLabel); - sliderLabel.RectTransform.RelativeSize = Vector2.Zero; - slider.RectTransform.RelativeSize = new Vector2(1.0f, 0.5f); - slider.ToolTip = respawnDurationText.ToolTip; - slider.UserData = respawnDurationText; - slider.Step = 0.1f; - slider.Range = new Vector2(60.0f, 660.0f); - slider.ScrollToValue = (GUIScrollBar scrollBar, float barScroll) => - { - return barScroll >= 1.0f ? 0.0f : barScroll * (scrollBar.Range.Y - scrollBar.Range.X) + scrollBar.Range.X; - }; - slider.ValueToScroll = (GUIScrollBar scrollBar, float value) => - { - return value <= 0.0f ? 1.0f : (value - scrollBar.Range.X) / (scrollBar.Range.Y - scrollBar.Range.X); - }; - GetPropertyData(nameof(MaxTransportTime)).AssignGUIComponent(slider); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - if (barScroll == 1.0f) - { - ((GUITextBlock)scrollBar.UserData).Text = respawnDurationLabel + TextManager.Get("Unlimited"); - } - else - { - ((GUITextBlock)scrollBar.UserData).Text = respawnDurationLabel + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); - } - - return true; - }; - slider.OnMoved(slider, slider.BarScroll); - - GUILayoutGroup losModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsContent.RectTransform)); - - var losModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), losModeLayout.RectTransform), - TextManager.Get("LosEffect")); - - var losModeRadioButtonLayout - = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), losModeLayout.RectTransform), - isHorizontal: true) - { - Stretch = true - }; - - var losModeRadioButtonGroup = new GUIRadioButtonGroup(); - LosMode[] losModes = (LosMode[])Enum.GetValues(typeof(LosMode)); - for (int i = 0; i < losModes.Length; i++) - { - var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), losModeRadioButtonLayout.RectTransform), TextManager.Get($"LosMode{losModes[i]}"), font: GUIStyle.SmallFont, style: "GUIRadioButton"); - losModeRadioButtonGroup.AddRadioButton(i, losTick); - } - GetPropertyData(nameof(LosMode)).AssignGUIComponent(losModeRadioButtonGroup); - - GUILayoutGroup healthBarModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsContent.RectTransform)); - - var healthBarModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), healthBarModeLayout.RectTransform), - TextManager.Get("ShowEnemyHealthBars")); - - var healthBarModeRadioButtonLayout - = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), healthBarModeLayout.RectTransform), - isHorizontal: true) - { - Stretch = true - }; - - var healthBarModeRadioButtonGroup = new GUIRadioButtonGroup(); - EnemyHealthBarMode[] healthBarModeModes = Enum.GetValues(); - for (int i = 0; i < healthBarModeModes.Length; i++) - { - var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), healthBarModeRadioButtonLayout.RectTransform), - TextManager.Get($"ShowEnemyHealthBars.{healthBarModeModes[i]}"), - font: GUIStyle.SmallFont, style: "GUIRadioButton"); - healthBarModeRadioButtonGroup.AddRadioButton(i, losTick); - } - GetPropertyData(nameof(ShowEnemyHealthBars)).AssignGUIComponent(healthBarModeRadioButtonGroup); - - GUILayoutGroup numberLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), roundsContent.RectTransform)) - { - Stretch = true - }; - - var traitorsMinPlayerCount = CreateLabeledNumberInput(numberLayout, "ServerSettingsTraitorsMinPlayerCount", 2, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); - GetPropertyData(nameof(TraitorsMinPlayerCount)).AssignGUIComponent(traitorsMinPlayerCount); - - var maximumTransferAmount = CreateLabeledNumberInput(numberLayout, "serversettingsmaximumtransferrequest", 0, CampaignMode.MaxMoney, "serversettingsmaximumtransferrequesttooltip"); - GetPropertyData(nameof(MaximumMoneyTransferRequest)).AssignGUIComponent(maximumTransferAmount); - - var lootedMoneyDestination = CreateLabeledDropdown(numberLayout, "serversettingslootedmoneydestination", numElements: 2, "serversettingslootedmoneydestinationtooltip"); - lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.bank"), LootedMoneyDestination.Bank); - lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.wallet"), LootedMoneyDestination.Wallet); - GetPropertyData(nameof(LootedMoneyDestination)).AssignGUIComponent(lootedMoneyDestination); - - var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); - GetPropertyData(nameof(DisableBotConversations)).AssignGUIComponent(disableBotConversationsBox); - - GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsContent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - var monsterButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonHolder.RectTransform), - TextManager.Get("ServerSettingsMonsterSpawns"), style: "GUIButtonSmall") - { - Enabled = !GameMain.NetworkMember.GameStarted - }; - var monsterFrame = new GUIListBox(new RectTransform(new Vector2(0.6f, 0.7f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.BottomLeft, Pivot.BottomRight)) - { - Visible = false - }; - monsterButton.UserData = monsterFrame; - monsterButton.OnClicked = (button, obj) => - { - if (GameMain.NetworkMember.GameStarted) - { - ((GUIComponent)obj).Visible = false; - button.Enabled = false; - return true; - } - ((GUIComponent)obj).Visible = !((GUIComponent)obj).Visible; - return true; - }; - - InitMonstersEnabled(); - List monsterNames = MonsterEnabled.Keys.ToList(); - tempMonsterEnabled = new Dictionary(MonsterEnabled); - foreach (Identifier s in monsterNames) - { - LocalizedString translatedLabel = TextManager.Get($"Character.{s}").Fallback(s.Value); - var monsterEnabledBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), monsterFrame.Content.RectTransform) { MinSize = new Point(0, 25) }, - label: translatedLabel) - { - Selected = tempMonsterEnabled[s], - OnSelected = (GUITickBox tb) => - { - tempMonsterEnabled[s] = tb.Selected; - return true; - } - }; - } - - var cargoButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonHolder.RectTransform), - TextManager.Get("ServerSettingsAdditionalCargo"), style: "GUIButtonSmall") - { - Enabled = !GameMain.NetworkMember.GameStarted - }; - - var cargoFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.7f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.BottomRight, Pivot.BottomLeft)) - { - Visible = false - }; - var cargoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), cargoFrame.RectTransform, Anchor.Center)) - { - Stretch = true - }; - - var filterText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), cargoContent.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.SubHeadingFont); - var entityFilterBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), filterText.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); - filterText.RectTransform.MinSize = new Point(0, entityFilterBox.RectTransform.MinSize.Y); - var cargoList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), cargoContent.RectTransform)); - entityFilterBox.OnTextChanged += (textBox, text) => - { - foreach (var child in cargoList.Content.Children) - { - if (child.UserData is not ItemPrefab itemPrefab) { continue; } - child.Visible = string.IsNullOrEmpty(text) || itemPrefab.Name.Contains(text, StringComparison.OrdinalIgnoreCase); - } - return true; - }; - - cargoButton.UserData = cargoFrame; - cargoButton.OnClicked = (button, obj) => - { - if (GameMain.NetworkMember.GameStarted) - { - ((GUIComponent)obj).Visible = false; - button.Enabled = false; - return true; - } - ((GUIComponent)obj).Visible = !((GUIComponent)obj).Visible; - return true; - }; - - GUITextBlock.AutoScaleAndNormalize(buttonHolder.Children.Select(c => ((GUIButton)c).TextBlock)); - - foreach (ItemPrefab ip in ItemPrefab.Prefabs.OrderBy(ip => ip.Name)) - { - if (ip.AllowAsExtraCargo.HasValue) - { - if (!ip.AllowAsExtraCargo.Value) { continue; } - } - else - { - if (!ip.CanBeBought) { continue; } - } - - var itemFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), cargoList.Content.RectTransform) { MinSize = new Point(0, 30) }, isHorizontal: true) - { - Stretch = true, - UserData = ip, - RelativeSpacing = 0.05f - }; - - if (ip.InventoryIcon != null || ip.Sprite != null) - { - GUIImage img = new GUIImage(new RectTransform(new Point(itemFrame.Rect.Height), itemFrame.RectTransform), - ip.InventoryIcon ?? ip.Sprite, scaleToFit: true) - { - CanBeFocused = false - }; - img.Color = img.Sprite == ip.InventoryIcon ? ip.InventoryIconColor : ip.SpriteColor; - } - - new GUITextBlock(new RectTransform(new Vector2(0.75f, 1.0f), itemFrame.RectTransform), - ip.Name, font: GUIStyle.SmallFont) - { - Wrap = true, - CanBeFocused = false - }; - - ExtraCargo.TryGetValue(ip, out int cargoVal); - var amountInput = new GUINumberInput(new RectTransform(new Vector2(0.35f, 1.0f), itemFrame.RectTransform), - NumberType.Int, textAlignment: Alignment.CenterLeft) - { - MinValueInt = 0, - MaxValueInt = MaxExtraCargoItemsOfType, - IntValue = cargoVal - }; - amountInput.OnValueChanged += (numberInput) => - { - if (ExtraCargo.ContainsKey(ip)) - { - ExtraCargo[ip] = numberInput.IntValue; - if (numberInput.IntValue <= 0) { ExtraCargo.Remove(ip); } - } - else if (ExtraCargo.Keys.Count() < MaxExtraCargoItemTypes) - { - ExtraCargo.Add(ip, numberInput.IntValue); - } - numberInput.IntValue = ExtraCargo.ContainsKey(ip) ? ExtraCargo[ip] : 0; - CoroutineManager.Invoke(() => - { - foreach (var child in cargoList.Content.GetAllChildren()) - { - if (child.GetChild() is GUINumberInput otherNumberInput) - { - otherNumberInput.PlusButton.Enabled = ExtraCargo.Keys.Count() < MaxExtraCargoItemTypes && otherNumberInput.IntValue < otherNumberInput.MaxValueInt; - } - } - }, 0.0f); - }; - } - - //-------------------------------------------------------------------------------- - // antigriefing - //-------------------------------------------------------------------------------- - - var antigriefingTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Antigriefing].RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var tickBoxContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.16f), antigriefingTab.RectTransform)) - { - AutoHideScrollBar = true, - UseGridLayout = true - }; - tickBoxContainer.Padding *= 2.0f; - - var allowFriendlyFire = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsAllowFriendlyFire")); - GetPropertyData(nameof(AllowFriendlyFire)).AssignGUIComponent(allowFriendlyFire); - - var killableNPCs = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsKillableNPCs")); - GetPropertyData(nameof(KillableNPCs)).AssignGUIComponent(killableNPCs); - - var destructibleOutposts = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsDestructibleOutposts")); - GetPropertyData(nameof(DestructibleOutposts)).AssignGUIComponent(destructibleOutposts); - - var lockAllDefaultWires = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsLockAllDefaultWires")); - GetPropertyData(nameof(LockAllDefaultWires)).AssignGUIComponent(lockAllDefaultWires); - - var allowRewiring = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsAllowRewiring")); - GetPropertyData(nameof(AllowRewiring)).AssignGUIComponent(allowRewiring); - - var allowWifiChatter = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsAllowWifiChat")); - GetPropertyData(nameof(AllowLinkingWifiToChat)).AssignGUIComponent(allowWifiChatter); - - var allowDisguises = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsAllowDisguises")); - GetPropertyData(nameof(AllowDisguises)).AssignGUIComponent(allowDisguises); - - var voteKickBox = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - 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)); - - CreateLabeledSlider(antigriefingTab, "ServerSettingsKickVotesRequired", out slider, out sliderLabel); - LocalizedString votesRequiredLabel = sliderLabel.Text + " "; - slider.Step = 0.2f; - slider.Range = new Vector2(0.5f, 1.0f); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - ((GUITextBlock)scrollBar.UserData).Text = votesRequiredLabel + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; - return true; - }; - GetPropertyData(nameof(KickVoteRequiredRatio)).AssignGUIComponent(slider); - slider.OnMoved(slider, slider.BarScroll); - - CreateLabeledSlider(antigriefingTab, "ServerSettingsAutobanTime", out slider, out sliderLabel); - LocalizedString autobanLabel = sliderLabel.Text + " "; - slider.Step = 0.01f; - slider.Range = new Vector2(0.0f, MaxAutoBanTime); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - ((GUITextBlock)scrollBar.UserData).Text = autobanLabel + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); - return true; - }; - GetPropertyData(nameof(AutoBanTime)).AssignGUIComponent(slider); - slider.OnMoved(slider, slider.BarScroll); - - var wrongPasswordBanBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), TextManager.Get("ServerSettingsBanAfterWrongPassword")); - GetPropertyData(nameof(BanAfterWrongPassword)).AssignGUIComponent(wrongPasswordBanBox); - var allowedPasswordRetries = CreateLabeledNumberInput(antigriefingTab, "ServerSettingsPasswordRetriesBeforeBan", 0, 10); - GetPropertyData(nameof(MaxPasswordRetriesBeforeBan)).AssignGUIComponent(allowedPasswordRetries); - wrongPasswordBanBox.OnSelected += (tb) => - { - allowedPasswordRetries.Enabled = tb.Selected; - return true; - }; - - - GUILayoutGroup karmaAndDosLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), antigriefingTab.RectTransform), isHorizontal: false); - GUILayoutGroup lowerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true); - GUILayoutGroup upperLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true); - - // karma -------------------------------------------------------------------------- - - var karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsUseKarma")); - GetPropertyData(nameof(KarmaEnabled)).AssignGUIComponent(karmaBox); - - var enableDosProtection = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsEnableDoSProtection")) - { - ToolTip = TextManager.Get("ServerSettingsEnableDoSProtectionTooltip") - }; - GetPropertyData(nameof(EnableDoSProtection)).AssignGUIComponent(enableDosProtection); - - CreateLabeledSlider(lowerLayout, "ServerSettingsMaxPacketAmount", out GUIScrollBar maxPacketSlider, out GUITextBlock maxPacketSliderLabel); - LocalizedString maxPacketCountLabel = maxPacketSliderLabel.Text; - maxPacketSlider.Step = 0.001f; - maxPacketSlider.Range = new Vector2(PacketLimitMin, PacketLimitMax); - maxPacketSlider.ToolTip = packetAmountTooltip; - maxPacketSlider.OnMoved = (scrollBar, _) => - { - GUITextBlock textBlock = (GUITextBlock)scrollBar.UserData; - int value = (int)MathF.Floor(scrollBar.BarScrollValue); - - LocalizedString valueText = value > PacketLimitMin - ? value.ToString() - : TextManager.Get("ServerSettingsNoLimit"); - - switch (value) - { - case <= PacketLimitMin: - textBlock.TextColor = GUIStyle.Green; - scrollBar.ToolTip = packetAmountTooltip; - break; - case < PacketLimitWarning: - textBlock.TextColor = GUIStyle.Red; - scrollBar.ToolTip = packetAmountTooltipWarning; - break; - default: - textBlock.TextColor = GUIStyle.TextColorNormal; - scrollBar.ToolTip = packetAmountTooltip; - break; - } - - textBlock.Text = $"{maxPacketCountLabel} {valueText}"; - return true; - }; - GetPropertyData(nameof(MaxPacketAmount)).AssignGUIComponent(maxPacketSlider); - maxPacketSlider.OnMoved(maxPacketSlider, maxPacketSlider.BarScroll); - - karmaPresetDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform)); - foreach (string karmaPreset in GameMain.NetworkMember.KarmaManager.Presets.Keys) - { - karmaPresetDD.AddItem(TextManager.Get("KarmaPreset." + karmaPreset), karmaPreset); - } - - var karmaSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), antigriefingTab.RectTransform), style: null); - var karmaSettingsList = new GUIListBox(new RectTransform(Vector2.One, karmaSettingsContainer.RectTransform)) - { - Spacing = (int)(8 * GUI.Scale) - }; - karmaSettingsList.Padding *= 2.0f; - - karmaSettingsBlocker = new GUIFrame(new RectTransform(Vector2.One, karmaSettingsContainer.RectTransform, Anchor.CenterLeft) - { MaxSize = new Point(karmaSettingsList.ContentBackground.Rect.Width, int.MaxValue) }, style: null) - { - UserData = "karmasettingsblocker", - Color = Color.Black * 0.95f - }; - karmaSettingsBlocker.Color *= 0.5f; - karmaPresetDD.SelectItem(KarmaPreset); - karmaSettingsBlocker.Visible = !karmaBox.Selected || KarmaPreset != "custom"; - GameMain.NetworkMember.KarmaManager.CreateSettingsFrame(karmaSettingsList.Content); - karmaPresetDD.OnSelected = (selected, obj) => - { - string newKarmaPreset = obj as string; - if (newKarmaPreset == KarmaPreset) { return true; } - - List properties = netProperties.Values.ToList(); - List prevValues = new List(); - foreach (NetPropertyData prop in netProperties.Values) - { - prevValues.Add(prop.TempValue); - if (prop.GUIComponent != null) { prop.Value = prop.GUIComponentValue; } - } - if (KarmaPreset == "custom") - { - GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); - GameMain.NetworkMember?.KarmaManager?.Save(); - } - KarmaPreset = newKarmaPreset; - GameMain.NetworkMember.KarmaManager.SelectPreset(KarmaPreset); - karmaSettingsList.Content.ClearChildren(); - karmaSettingsBlocker.Visible = !karmaBox.Selected || KarmaPreset != "custom"; - GameMain.NetworkMember.KarmaManager.CreateSettingsFrame(karmaSettingsList.Content); - for (int i = 0; i < netProperties.Count; i++) - { - properties[i].TempValue = prevValues[i]; - } - return true; - }; - AssignGUIComponent("KarmaPreset", karmaPresetDD); - karmaBox.OnSelected = (tb) => - { - karmaSettingsBlocker.Visible = !tb.Selected || KarmaPreset != "custom"; - return true; - }; - - //-------------------------------------------------------------------------------- - // banlist - //-------------------------------------------------------------------------------- - - BanList.CreateBanFrame(settingsTabs[(int)SettingsTab.Banlist]); - } - - private void CreateLabeledSlider(GUIComponent parent, string labelTag, out GUIScrollBar slider, out GUITextBlock label) - { - var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - slider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), barSize: 0.1f, style: "GUISlider"); - label = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), - string.IsNullOrEmpty(labelTag) ? "" : TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); - - container.RectTransform.MinSize = new Point(0, slider.RectTransform.MinSize.Y); - container.RectTransform.MaxSize = new Point(int.MaxValue, slider.RectTransform.MaxSize.Y); - - //slider has a reference to the label to change the text when it's used - slider.UserData = label; - } - - private GUINumberInput CreateLabeledNumberInput(GUIComponent parent, string labelTag, int min, int max, string toolTipTag = null) - { - var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f, - ToolTip = TextManager.Get(labelTag) - }; - - var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), - TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) - { - AutoScaleHorizontal = true - }; - if (!string.IsNullOrEmpty(toolTipTag)) - { - label.ToolTip = TextManager.Get(toolTipTag); - } - var input = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), NumberType.Int) - { - MinValueInt = min, - MaxValueInt = max - }; - - container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y); - container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y); - - return input; - } - - private GUIDropDown CreateLabeledDropdown(GUIComponent parent, string labelTag, int numElements, string toolTipTag = null) - { - var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f, - ToolTip = TextManager.Get(labelTag) - }; - - var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), - TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) - { - AutoScaleHorizontal = true - }; - if (!string.IsNullOrEmpty(toolTipTag)) - { - label.ToolTip = TextManager.Get(toolTipTag); - } - var input = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), elementCount: numElements); - - container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y); - container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y); - - return input; - } - - private bool SelectSettingsTab(GUIButton button, object obj) - { - settingsTabIndex = (int)obj; - - for (int i = 0; i < settingsTabs.Length; i++) - { - settingsTabs[i].Visible = i == settingsTabIndex; - tabButtons[i].Selected = i == settingsTabIndex; - } - - return true; - } - - public bool ToggleSettingsFrame(GUIButton button, object obj) - { - if (GameMain.NetworkMember == null) { return false; } - if (settingsFrame == null) - { - CreateSettingsFrame(); + throw new ArgumentException($"Could not find a {nameof(ServerSettings)} property with the name \"{name}\"."); } else { - if (KarmaPreset == "custom") - { - GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); - GameMain.NetworkMember?.KarmaManager?.Save(); - } - ClientAdminWrite(NetFlags.Properties); - foreach (NetPropertyData prop in netProperties.Values) - { - prop.GUIComponent = null; - } - settingsFrame = null; + return matchingProperty.Value; } - return false; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs new file mode 100644 index 000000000..0aad3ebfa --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs @@ -0,0 +1,924 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma.Networking +{ + partial class ServerSettings : ISerializableEntity + { + //GUI stuff + private GUIFrame settingsFrame; + private readonly Dictionary settingsTabs = new Dictionary(); + private readonly Dictionary tabButtons = new Dictionary(); + private SettingsTab selectedTab; + + //UI elements relating to karma, hidden when karma is disabled + private readonly List karmaElements = new List(); + private GUIDropDown karmaPresetDD; + private GUIListBox karmaSettingsList; + + private GUIComponent extraCargoPanel, monstersEnabledPanel; + private GUIButton extraCargoButton, monstersEnabledButton; + + enum SettingsTab + { + ServerIdentity, + General, + Antigriefing, + Banlist + } + + public void AssignGUIComponent(string propertyName, GUIComponent component) + { + GetPropertyData(propertyName).AssignGUIComponent(component); + } + + public void AddToGUIUpdateList() + { + if (GUI.DisableHUD) { return; } + + settingsFrame?.AddToGUIUpdateList(); + } + + private void CreateSettingsFrame() + { + foreach (NetPropertyData prop in netProperties.Values) + { + prop.TempValue = prop.Value; + } + + //background frame + settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, settingsFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + + new GUIButton(new RectTransform(Vector2.One, settingsFrame.RectTransform), "", style: null).OnClicked += (btn, userData) => + { + if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) { ToggleSettingsFrame(btn, userData); } + return true; + }; + + new GUIButton(new RectTransform(Vector2.One, settingsFrame.RectTransform), "", style: null) + { + OnClicked = ToggleSettingsFrame + }; + + //center frames + GUIFrame innerFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.85f), settingsFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 430) }); + GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(innerFrame.Rect.Size - new Point(GUI.IntScale(20)), innerFrame.RectTransform, Anchor.Center), + childAnchor: Anchor.TopCenter) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("serversettingsbutton"), font: GUIStyle.LargeFont); + + var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), paddedFrame.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + var tabContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), paddedFrame.RectTransform), style: "InnerFrame"); + + //tabs + var settingsTabTypes = Enum.GetValues(typeof(SettingsTab)).Cast(); + foreach (var settingsTab in settingsTabTypes) + { + settingsTabs[settingsTab] = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), tabContent.RectTransform, Anchor.Center)); + tabButtons[settingsTab] = new GUIButton(new RectTransform(new Vector2(0.2f, 1.2f), buttonArea.RectTransform), TextManager.Get($"ServerSettings{settingsTab}Tab"), style: "GUITabButton") + { + UserData = settingsTab, + OnClicked = SelectSettingsTab + }; + } + GUITextBlock.AutoScaleAndNormalize(tabButtons.Values.Select(b => b.TextBlock)); + SelectSettingsTab(tabButtons[0], 0); + tabButtons[SettingsTab.Banlist].Enabled = + GameMain.Client.HasPermission(Networking.ClientPermissions.Ban) || + GameMain.Client.HasPermission(Networking.ClientPermissions.Unban); + + //"Close" + var buttonContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.05f), paddedFrame.RectTransform), style: null); + var closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Close")) + { + OnClicked = ToggleSettingsFrame + }; + + CreateServerIdentityTab(settingsTabs[SettingsTab.ServerIdentity]); + CreateGeneralTab(settingsTabs[SettingsTab.General]); + CreateAntigriefingTab(settingsTabs[SettingsTab.Antigriefing]); + CreateBanlistTab(settingsTabs[SettingsTab.Banlist]); + + if (GameMain.Client == null || + !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) + { + //block all settings if the client doesn't have permission to edit them + foreach (var settingsTab in settingsTabs) + { + SetElementInteractability(settingsTab.Value, false); + } + } + //keep these enabled, so clients can open the panels and see what's enabled even if they can't edit them + extraCargoButton.Enabled = monstersEnabledButton.Enabled = true; + } + + private void SetElementInteractability(GUIComponent parent, bool interactable) + { + foreach (var child in parent.GetAllChildren()) + { + child.Enabled = interactable; + //make the disabled color slightly less dim (these should be readable, despite being non-interactable) + child.DisabledColor = new Color(child.Color, child.Color.A / 255.0f * 0.8f); + if (child is GUITextBlock textBlock) + { + textBlock.DisabledTextColor = new Color(textBlock.TextColor, textBlock.TextColor.A / 255.0f * 0.8f); + } + } + } + + private void CreateServerIdentityTab(GUIComponent parent) + { + //changing server visibility on the fly is not supported in dedicated servers + if (GameMain.Client?.ClientPeer is not LidgrenClientPeer) + { + var isPublic = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), + TextManager.Get("publicserver")) + { + ToolTip = TextManager.Get("publicservertooltip") + }; + AssignGUIComponent(nameof(IsPublic), isPublic); + } + + var serverNameLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), TextManager.Get("ServerName")); + var serverNameBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), serverNameLabel.RectTransform, Anchor.CenterRight), + GameMain.Client.ServerSettings.ServerName) + { + OverflowClip = true, + MaxTextLength = NetConfig.ServerNameMaxLength + }; + serverNameBox.OnDeselected += (textBox, key) => + { + if (textBox.Text.IsNullOrWhiteSpace()) + { + textBox.Flash(GUIStyle.Red); + if (GameMain.Client != null) + { + textBox.Text = GameMain.Client.ServerSettings.ServerName; + } + } + GameMain.Client?.ServerSettings.ClientAdminWrite(NetFlags.Properties); + }; + AssignGUIComponent(nameof(ServerName), serverNameBox); + + // server message ************************************************************************* + + var motdHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), TextManager.Get("ServerMOTD")); + var motdCharacterCount = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), motdHeader.RectTransform, Anchor.CenterRight), string.Empty, textAlignment: Alignment.CenterRight); + var serverMessageContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), parent.RectTransform)) + { + Visible = true + }; + var serverMessageBox = new GUITextBox(new RectTransform(Vector2.One, serverMessageContainer.Content.RectTransform), + style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) + { + MaxTextLength = NetConfig.ServerMessageMaxLength + }; + var serverMessageHint = new GUITextBlock(new RectTransform(Vector2.One, serverMessageBox.RectTransform), + textColor: Color.DarkGray * 0.6f, textAlignment: Alignment.TopLeft, font: GUIStyle.Font, text: TextManager.Get("ClickToWriteServerMessage")); + AssignGUIComponent(nameof(ServerMessageText), serverMessageBox); + + void updateServerMessageScrollBasedOnCaret() + { + float caretY = serverMessageBox.CaretScreenPos.Y; + float bottomCaretExtent = serverMessageBox.Font.LineHeight * 1.5f; + float topCaretExtent = -serverMessageBox.Font.LineHeight * 0.5f; + if (caretY + bottomCaretExtent > serverMessageContainer.Rect.Bottom) + { + serverMessageContainer.ScrollBar.BarScroll + = (caretY - serverMessageBox.Rect.Top - serverMessageContainer.Rect.Height + bottomCaretExtent) + / (serverMessageBox.Rect.Height - serverMessageContainer.Rect.Height); + } + else if (caretY + topCaretExtent < serverMessageContainer.Rect.Top) + { + serverMessageContainer.ScrollBar.BarScroll + = (caretY - serverMessageBox.Rect.Top + topCaretExtent) + / (serverMessageBox.Rect.Height - serverMessageContainer.Rect.Height); + } + } + serverMessageBox.OnSelected += (textBox, key) => + { + serverMessageHint.Visible = false; + updateServerMessageScrollBasedOnCaret(); + }; + serverMessageBox.OnTextChanged += (textBox, text) => + { + serverMessageHint.Visible = !textBox.Selected && !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text); + RefreshServerInfoSize(); + return true; + }; + serverMessageBox.RectTransform.SizeChanged += RefreshServerInfoSize; + motdCharacterCount.TextGetter += () => { return serverMessageBox.Text.Length + " / " + NetConfig.ServerMessageMaxLength; }; + + void RefreshServerInfoSize() + { + serverMessageHint.Visible = !serverMessageBox.Selected && !serverMessageBox.Readonly && string.IsNullOrWhiteSpace(serverMessageBox.Text); + Vector2 textSize = serverMessageBox.Font.MeasureString(serverMessageBox.WrappedText); + serverMessageBox.RectTransform.NonScaledSize = new Point(serverMessageBox.RectTransform.NonScaledSize.X, Math.Max(serverMessageContainer.Content.Rect.Height, (int)textSize.Y + 10)); + serverMessageContainer.UpdateScrollBarSize(); + } + + serverMessageBox.OnEnterPressed += (textBox, text) => + { + string str = textBox.Text; + int caretIndex = textBox.CaretIndex; + textBox.Text = $"{str[..caretIndex]}\n{str[caretIndex..]}"; + textBox.CaretIndex = caretIndex + 1; + + return true; + }; + serverMessageBox.OnDeselected += (textBox, key) => + { + if (!textBox.Readonly) + { + GameMain.Client?.ServerSettings?.ClientAdminWrite(NetFlags.Properties); + } + serverMessageHint.Visible = !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text); + }; + serverMessageBox.OnKeyHit += (sender, key) => updateServerMessageScrollBasedOnCaret(); + + // ************************************************************************* + + var playStyleLayoutLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), + TextManager.Get("ServerSettingsPlayStyle")); + var playStyleSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), playStyleLayoutLabel.RectTransform, Anchor.CenterRight)); + foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) + { + playStyleSelection.AddElement(playStyle, TextManager.Get("servertag." + playStyle), TextManager.Get("servertagdescription." + playStyle)); + } + AssignGUIComponent(nameof(PlayStyle), playStyleSelection); + + var passwordLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), TextManager.Get("Password")); + new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), passwordLabel.RectTransform, Anchor.CenterRight), + TextManager.Get("ServerSettingsSetPassword"), style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => { CreateChangePasswordPrompt(); return true; } + }; + + var wrongPasswordBanBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), TextManager.Get("ServerSettingsBanAfterWrongPassword")); + AssignGUIComponent(nameof(BanAfterWrongPassword), wrongPasswordBanBox); + var allowedPasswordRetries = NetLobbyScreen.CreateLabeledNumberInput(parent, "ServerSettingsPasswordRetriesBeforeBan", 0, 10); + AssignGUIComponent(nameof(MaxPasswordRetriesBeforeBan), allowedPasswordRetries); + + var maxPlayers = NetLobbyScreen.CreateLabeledNumberInput(parent, "MaxPlayers", 0, NetConfig.MaxPlayers); + AssignGUIComponent(nameof(MaxPlayers), maxPlayers); + + // Language + var languageLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("Language")); + var languageDD = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), languageLabel.RectTransform, Anchor.CenterRight)); + foreach (var language in ServerLanguageOptions.Options) + { + languageDD.AddItem(language.Label, language.Identifier); + } + languageLabel.InheritTotalChildrenMinHeight(); + AssignGUIComponent(nameof(Language), languageDD); + + } + + private static void CreateChangePasswordPrompt() + { + var passwordMsgBox = new GUIMessageBox( + TextManager.Get("ServerSettingsSetPassword"), + "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, + relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170))); + var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), passwordMsgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); + var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform)) + { + Censor = true + }; + + passwordMsgBox.Content.Recalculate(); + passwordMsgBox.Content.InheritTotalChildrenHeight(); + passwordMsgBox.Content.Parent.RectTransform.MinSize = new Point(0, (int)(passwordMsgBox.Content.RectTransform.MinSize.Y / passwordMsgBox.Content.RectTransform.RelativeSize.Y)); + + var okButton = passwordMsgBox.Buttons[0]; + okButton.OnClicked += (_, __) => + { + DebugConsole.ExecuteCommand($"setpassword \"{passwordBox.Text}\""); + return true; + }; + okButton.OnClicked += passwordMsgBox.Close; + + var cancelButton = passwordMsgBox.Buttons[1]; + cancelButton.OnClicked = (_, __) => + { + passwordMsgBox?.Close(); + passwordMsgBox = null; + return true; + }; + passwordBox.OnEnterPressed += (_, __) => + { + okButton.OnClicked.Invoke(okButton, okButton.UserData); + return true; + }; + + passwordBox.Select(); + } + + private void CreateGeneralTab(GUIComponent parent) + { + var listBox = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform), style: "GUIListBoxNoBorder") + { + AutoHideScrollBar = true, + CurrentSelectMode = GUIListBox.SelectMode.None + }; + + NetLobbyScreen.CreateSubHeader("serversettingscategory.roundmanagement", listBox.Content); + + var endVoteBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsEndRoundVoting")); + AssignGUIComponent(nameof(AllowEndVoting), endVoteBox); + + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: "ServerSettingsEndRoundVotesRequired", tooltipTag: string.Empty, out var slider, out var sliderLabel); + + LocalizedString endRoundLabel = sliderLabel.Text; + slider.Step = 0.2f; + slider.Range = new Vector2(0.5f, 1.0f); + AssignGUIComponent(nameof(EndVoteRequiredRatio), slider); + slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + ((GUITextBlock)scrollBar.UserData).Text = endRoundLabel + " " + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; + return true; + }; + slider.OnMoved(slider, slider.BarScroll); + + //*********************************************** + + // Sub Selection + + var subSelectionLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsSubSelection")); + var subSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), subSelectionLabel.RectTransform, Anchor.CenterRight)); + foreach (SelectionMode selectionMode in Enum.GetValues(typeof(SelectionMode))) + { + subSelection.AddElement(selectionMode, TextManager.Get(selectionMode.ToString())); + } + AssignGUIComponent(nameof(SubSelectionMode), subSelection); + + // Mode Selection + var gameModeSelectionLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsModeSelection")); + var gameModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), gameModeSelectionLabel.RectTransform, Anchor.CenterRight)); + foreach (SelectionMode selectionMode in Enum.GetValues(typeof(SelectionMode))) + { + gameModeSelection.AddElement(selectionMode, TextManager.Get(selectionMode.ToString())); + } + AssignGUIComponent(nameof(ModeSelectionMode), gameModeSelection); + + //*********************************************** + + LocalizedString autoRestartDelayLabel = TextManager.Get("ServerSettingsAutoRestartDelay") + " "; + + var autorestartBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("AutoRestart")); + AssignGUIComponent(nameof(AutoRestart), autorestartBox); + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: string.Empty, tooltipTag: string.Empty, + out var startIntervalSlider, out var startIntervalSliderLabel, range: new Vector2(10.0f, 300.0f)); + startIntervalSlider.StepValue = 10.0f; + startIntervalSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = autoRestartDelayLabel + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); + return true; + }; + AssignGUIComponent(nameof(AutoRestartInterval), startIntervalSlider); + startIntervalSlider.OnMoved(startIntervalSlider, startIntervalSlider.BarScroll); + + //*********************************************** + + var startWhenClientsReady = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsStartWhenClientsReady")); + AssignGUIComponent(nameof(StartWhenClientsReady), startWhenClientsReady); + + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: "ServerSettingsStartWhenClientsReadyRatio", tooltipTag: string.Empty, + out slider, out sliderLabel); + LocalizedString clientsReadyRequiredLabel = sliderLabel.Text; + slider.Step = 0.2f; + slider.Range = new Vector2(0.5f, 1.0f); + slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + ((GUITextBlock)scrollBar.UserData).Text = clientsReadyRequiredLabel.Replace("[percentage]", ((int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f)).ToString()); + return true; + }; + AssignGUIComponent(nameof(StartWhenClientsReadyRatio), slider); + slider.OnMoved(slider, slider.BarScroll); + + var randomizeLevelBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsRandomizeSeed")); + AssignGUIComponent(nameof(RandomizeSeed), randomizeLevelBox); + + //*********************************************** + + NetLobbyScreen.CreateSubHeader("serversettingsroundstab", listBox.Content); + + var voiceChatEnabled = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsVoiceChatEnabled")); + AssignGUIComponent(nameof(VoiceChatEnabled), voiceChatEnabled); + + var allowSpecBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsAllowSpectating")); + AssignGUIComponent(nameof(AllowSpectating), allowSpecBox); + + var losModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("LosEffect")); + var losModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 0.6f), losModeLabel.RectTransform, Anchor.CenterRight)); + foreach (var losMode in Enum.GetValues(typeof(LosMode)).Cast()) + { + losModeSelection.AddElement(losMode, TextManager.Get($"LosMode{losMode}"), TextManager.Get($"LosMode{losMode}.tooltip")); + } + AssignGUIComponent(nameof(LosMode), losModeSelection); + + var healthBarModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ShowEnemyHealthBars")); + var healthBarModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 0.6f), healthBarModeLabel.RectTransform, Anchor.CenterRight)); + foreach (var healthBarMode in Enum.GetValues(typeof(EnemyHealthBarMode)).Cast()) + { + healthBarModeSelection.AddElement(healthBarMode, TextManager.Get($"ShowEnemyHealthBars.{healthBarMode}")); + } + AssignGUIComponent(nameof(ShowEnemyHealthBars), healthBarModeSelection); + + var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); + AssignGUIComponent(nameof(DisableBotConversations), disableBotConversationsBox); + + //*********************************************** + + NetLobbyScreen.CreateSubHeader("serversettingscategory.misc", listBox.Content); + + var shareSubsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsShareSubFiles")); + AssignGUIComponent(nameof(AllowFileTransfers), shareSubsBox); + + var saveLogsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsSaveLogs")) + { + OnSelected = (GUITickBox) => + { + //TODO: fix? + //showLogButton.Visible = SaveServerLogs; + return true; + } + }; + AssignGUIComponent(nameof(SaveServerLogs), saveLogsBox); + + //-------------------------------------------------------------------------------- + // game settings + //-------------------------------------------------------------------------------- + + GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + const string MonstersEnabledUserdata = "monstersenabled"; + const string ExtraCargoUserdata = "extracargo"; + + monstersEnabledButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonHolder.RectTransform), + TextManager.Get("ServerSettingsMonsterSpawns"), style: "GUIButtonSmall") + { + Enabled = !GameMain.NetworkMember.GameStarted + }; + monstersEnabledPanel = CreateMonstersEnabledPanel(); + monstersEnabledButton.UserData = MonstersEnabledUserdata; + monstersEnabledButton.OnClicked = ExtraSettingsButtonClicked; + + extraCargoButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonHolder.RectTransform), + TextManager.Get("ServerSettingsAdditionalCargo"), style: "GUIButtonSmall") + { + Enabled = !GameMain.NetworkMember.GameStarted + }; + + extraCargoPanel = CreateExtraCargoPanel(); + extraCargoButton.UserData = ExtraCargoUserdata; + extraCargoButton.OnClicked = ExtraSettingsButtonClicked; + + GUITextBlock.AutoScaleAndNormalize(buttonHolder.Children.Select(c => ((GUIButton)c).TextBlock)); + + bool ExtraSettingsButtonClicked(GUIButton button, object obj) + { + //the extra settings buttons (monsters enabled, cargo) hold a reference to the panel they're supposed to toggle + GUIComponent panel; + switch (obj as string) + { + case MonstersEnabledUserdata: + panel = monstersEnabledPanel; + break; + case ExtraCargoUserdata: + panel = extraCargoPanel; + break; + default: + throw new Exception("Unrecognized extra settings button"); + } + if (GameMain.NetworkMember.GameStarted) + { + panel.Visible = false; + button.Enabled = false; + return true; + } + panel.Visible = !panel.Visible; + return true; + } + } + + private GUIComponent CreateMonstersEnabledPanel() + { + var monsterFrame = new GUIListBox(new RectTransform(new Vector2(0.5f, 0.7f), settingsTabs[SettingsTab.General].RectTransform, Anchor.BottomLeft, Pivot.BottomRight)) + { + Visible = false, + IgnoreLayoutGroups = true + }; + + InitMonstersEnabled(); + List monsterNames = MonsterEnabled.Keys.ToList(); + tempMonsterEnabled = new Dictionary(MonsterEnabled); + foreach (Identifier s in monsterNames) + { + LocalizedString translatedLabel = TextManager.Get($"Character.{s}").Fallback(s.Value); + var monsterEnabledBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), monsterFrame.Content.RectTransform) { MinSize = new Point(0, 25) }, + label: translatedLabel) + { + Selected = tempMonsterEnabled[s], + OnSelected = (GUITickBox tb) => + { + tempMonsterEnabled[s] = tb.Selected; + return true; + } + }; + } + monsterFrame.Content.RectTransform.SortChildren((c1, c2) => + { + var name1 = (c1.GUIComponent as GUITickBox)?.Text ?? string.Empty; + var name2 = (c2.GUIComponent as GUITickBox)?.Text ?? string.Empty; + return name1.CompareTo(name2); + }); + + if (GameMain.Client == null || + !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) + { + SetElementInteractability(monsterFrame.Content, false); + } + + return monsterFrame; + } + + private GUIComponent CreateExtraCargoPanel() + { + var cargoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.7f), settingsTabs[SettingsTab.General].RectTransform, Anchor.BottomRight, Pivot.BottomLeft)) + { + Visible = false, + IgnoreLayoutGroups = true + }; + var cargoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), cargoFrame.RectTransform, Anchor.Center)) + { + Stretch = true + }; + + var filterText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), cargoContent.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.SubHeadingFont); + var entityFilterBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), filterText.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); + filterText.RectTransform.MinSize = new Point(0, entityFilterBox.RectTransform.MinSize.Y); + var cargoList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), cargoContent.RectTransform)); + entityFilterBox.OnTextChanged += (textBox, text) => + { + foreach (var child in cargoList.Content.Children) + { + if (child.UserData is not ItemPrefab itemPrefab) { continue; } + child.Visible = string.IsNullOrEmpty(text) || itemPrefab.Name.Contains(text, StringComparison.OrdinalIgnoreCase); + } + return true; + }; + + foreach (ItemPrefab ip in ItemPrefab.Prefabs.OrderBy(ip => ip.Name)) + { + if (ip.AllowAsExtraCargo.HasValue) + { + if (!ip.AllowAsExtraCargo.Value) { continue; } + } + else + { + if (!ip.CanBeBought) { continue; } + } + + var itemFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), cargoList.Content.RectTransform) { MinSize = new Point(0, 30) }, isHorizontal: true) + { + Stretch = true, + UserData = ip, + RelativeSpacing = 0.05f + }; + + if (ip.InventoryIcon != null || ip.Sprite != null) + { + GUIImage img = new GUIImage(new RectTransform(new Point(itemFrame.Rect.Height), itemFrame.RectTransform), + ip.InventoryIcon ?? ip.Sprite, scaleToFit: true) + { + CanBeFocused = false + }; + img.Color = img.Sprite == ip.InventoryIcon ? ip.InventoryIconColor : ip.SpriteColor; + } + + new GUITextBlock(new RectTransform(new Vector2(0.75f, 1.0f), itemFrame.RectTransform), + ip.Name, font: GUIStyle.SmallFont) + { + Wrap = true, + CanBeFocused = false + }; + + ExtraCargo.TryGetValue(ip, out int cargoVal); + var amountInput = new GUINumberInput(new RectTransform(new Vector2(0.35f, 1.0f), itemFrame.RectTransform), + NumberType.Int, textAlignment: Alignment.CenterLeft) + { + MinValueInt = 0, + MaxValueInt = MaxExtraCargoItemsOfType, + IntValue = cargoVal + }; + amountInput.OnValueChanged += (numberInput) => + { + if (ExtraCargo.ContainsKey(ip)) + { + ExtraCargo[ip] = numberInput.IntValue; + if (numberInput.IntValue <= 0) { ExtraCargo.Remove(ip); } + } + else if (ExtraCargo.Keys.Count < MaxExtraCargoItemTypes) + { + ExtraCargo.Add(ip, numberInput.IntValue); + } + numberInput.IntValue = ExtraCargo.ContainsKey(ip) ? ExtraCargo[ip] : 0; + CoroutineManager.Invoke(() => + { + foreach (var child in cargoList.Content.GetAllChildren()) + { + if (child.GetChild() is GUINumberInput otherNumberInput) + { + otherNumberInput.PlusButton.Enabled = ExtraCargo.Keys.Count < MaxExtraCargoItemTypes && otherNumberInput.IntValue < otherNumberInput.MaxValueInt; + } + } + }, 0.0f); + }; + } + if (GameMain.Client == null || + !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) + { + SetElementInteractability(cargoList.Content, false); + } + + return cargoFrame; + } + + private void CreateAntigriefingTab(GUIComponent parent) + { + var listBox = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform), style: "GUIListBoxNoBorder") + { + AutoHideScrollBar = true, + CurrentSelectMode = GUIListBox.SelectMode.None + }; + + //-------------------------------------------------------------------------------- + // antigriefing + //-------------------------------------------------------------------------------- + + var tickBoxContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), listBox.Content.RectTransform)) + { + AutoHideScrollBar = true, + UseGridLayout = true + }; + tickBoxContainer.Padding *= 2.0f; + + var allowFriendlyFire = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsAllowFriendlyFire")); + AssignGUIComponent(nameof(AllowFriendlyFire), allowFriendlyFire); + + var killableNPCs = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsKillableNPCs")); + AssignGUIComponent(nameof(KillableNPCs), killableNPCs); + + var destructibleOutposts = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsDestructibleOutposts")); + AssignGUIComponent(nameof(DestructibleOutposts), destructibleOutposts); + + var lockAllDefaultWires = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsLockAllDefaultWires")); + AssignGUIComponent(nameof(LockAllDefaultWires), lockAllDefaultWires); + + var allowRewiring = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsAllowRewiring")); + AssignGUIComponent(nameof(AllowRewiring), allowRewiring); + + var allowWifiChatter = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsAllowWifiChat")); + AssignGUIComponent(nameof(AllowLinkingWifiToChat), allowWifiChatter); + + var allowDisguises = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsAllowDisguises")); + AssignGUIComponent(nameof(AllowDisguises), allowDisguises); + + var allowImmediateItemDeliveryBox = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsImmediateItemDelivery")); + AssignGUIComponent(nameof(AllowImmediateItemDelivery), 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)); + + tickBoxContainer.RectTransform.MinSize = new Point(0, (int)(tickBoxContainer.Content.Children.First().Rect.Height * 2.0f + tickBoxContainer.Padding.Y + tickBoxContainer.Padding.W)); + + var voteKickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsAllowVoteKick")); + AssignGUIComponent(nameof(AllowVoteKick), voteKickBox); + + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: "ServerSettingsKickVotesRequired", tooltipTag: string.Empty, out var slider, out var sliderLabel); + LocalizedString votesRequiredLabel = sliderLabel.Text + " "; + slider.Step = 0.2f; + slider.Range = new Vector2(0.5f, 1.0f); + slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + ((GUITextBlock)scrollBar.UserData).Text = votesRequiredLabel + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; + return true; + }; + AssignGUIComponent(nameof(KickVoteRequiredRatio), slider); + slider.OnMoved(slider, slider.BarScroll); + + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: "ServerSettingsAutobanTime", tooltipTag: "ServerSettingsAutobanTime.Tooltip", out slider, out sliderLabel); + LocalizedString autobanLabel = sliderLabel.Text + " "; + slider.Range = new Vector2(0.0f, MaxAutoBanTime); + slider.StepValue = 60.0f * 15.0f; //15 minutes + slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + ((GUITextBlock)scrollBar.UserData).Text = autobanLabel + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); + return true; + }; + AssignGUIComponent(nameof(AutoBanTime), slider); + slider.OnMoved(slider, slider.BarScroll); + + var maximumTransferAmount = NetLobbyScreen.CreateLabeledNumberInput(listBox.Content, "serversettingsmaximumtransferrequest", 0, CampaignMode.MaxMoney, "serversettingsmaximumtransferrequesttooltip"); + AssignGUIComponent(nameof(MaximumMoneyTransferRequest), maximumTransferAmount); + + var lootedMoneyDestination = NetLobbyScreen.CreateLabeledDropdown(listBox.Content, "serversettingslootedmoneydestination", numElements: 2, "serversettingslootedmoneydestinationtooltip"); + lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.bank"), LootedMoneyDestination.Bank); + lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.wallet"), LootedMoneyDestination.Wallet); + AssignGUIComponent(nameof(LootedMoneyDestination), lootedMoneyDestination); + + var enableDosProtection = new GUITickBox(new RectTransform(new Vector2(0.5f, 0.0f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsEnableDoSProtection")) + { + ToolTip = TextManager.Get("ServerSettingsEnableDoSProtectionTooltip") + }; + AssignGUIComponent(nameof(EnableDoSProtection), enableDosProtection); + + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: "ServerSettingsMaxPacketAmount", tooltipTag: string.Empty, out GUIScrollBar maxPacketSlider, out GUITextBlock maxPacketSliderLabel); + LocalizedString maxPacketCountLabel = maxPacketSliderLabel.Text; + maxPacketSlider.Step = 0.001f; + maxPacketSlider.Range = new Vector2(PacketLimitMin, PacketLimitMax); + maxPacketSlider.ToolTip = packetAmountTooltip; + maxPacketSlider.OnMoved = (scrollBar, _) => + { + GUITextBlock textBlock = (GUITextBlock)scrollBar.UserData; + int value = (int)MathF.Floor(scrollBar.BarScrollValue); + + LocalizedString valueText = value > PacketLimitMin + ? value.ToString() + : TextManager.Get("ServerSettingsNoLimit"); + + switch (value) + { + case <= PacketLimitMin: + textBlock.TextColor = GUIStyle.Green; + scrollBar.ToolTip = packetAmountTooltip; + break; + case < PacketLimitWarning: + textBlock.TextColor = GUIStyle.Red; + scrollBar.ToolTip = packetAmountTooltipWarning; + break; + default: + textBlock.TextColor = GUIStyle.TextColorNormal; + scrollBar.ToolTip = packetAmountTooltip; + break; + } + + textBlock.Text = $"{maxPacketCountLabel} {valueText}"; + return true; + }; + AssignGUIComponent(nameof(MaxPacketAmount), maxPacketSlider); + maxPacketSlider.OnMoved(maxPacketSlider, maxPacketSlider.BarScroll); + + // karma -------------------------------------------------------------------------- + + NetLobbyScreen.CreateSubHeader("Karma", listBox.Content, toolTipTag: "KarmaExplanation"); + + var karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsUseKarma")) + { + ToolTip = TextManager.Get("KarmaExplanation") + }; + AssignGUIComponent(nameof(KarmaEnabled), karmaBox); + + karmaPresetDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform)); + foreach (string karmaPreset in GameMain.NetworkMember.KarmaManager.Presets.Keys) + { + karmaPresetDD.AddItem(TextManager.Get("KarmaPreset." + karmaPreset), karmaPreset); + } + karmaElements.Add(karmaPresetDD); + + var karmaSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), listBox.Content.RectTransform), style: null); + karmaElements.Add(karmaSettingsContainer); + karmaSettingsList = new GUIListBox(new RectTransform(Vector2.One, karmaSettingsContainer.RectTransform)) + { + Spacing = (int)(8 * GUI.Scale) + }; + karmaSettingsList.Padding *= 2.0f; + + karmaPresetDD.SelectItem(KarmaPreset); + SetElementInteractability(karmaSettingsList.Content, !karmaBox.Selected || KarmaPreset != "custom"); + GameMain.NetworkMember.KarmaManager.CreateSettingsFrame(karmaSettingsList.Content); + karmaPresetDD.OnSelected = (selected, obj) => + { + string newKarmaPreset = obj as string; + if (newKarmaPreset == KarmaPreset) { return true; } + + List properties = netProperties.Values.ToList(); + List prevValues = new List(); + foreach (NetPropertyData prop in netProperties.Values) + { + prevValues.Add(prop.TempValue); + if (prop.GUIComponent != null) { prop.Value = prop.GUIComponentValue; } + } + if (KarmaPreset == "custom") + { + GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); + GameMain.NetworkMember?.KarmaManager?.Save(); + } + KarmaPreset = newKarmaPreset; + GameMain.NetworkMember.KarmaManager.SelectPreset(KarmaPreset); + karmaSettingsList.Content.ClearChildren(); + GameMain.NetworkMember.KarmaManager.CreateSettingsFrame(karmaSettingsList.Content); + SetElementInteractability(karmaSettingsList.Content, !karmaBox.Selected || KarmaPreset != "custom"); + for (int i = 0; i < netProperties.Count; i++) + { + properties[i].TempValue = prevValues[i]; + } + return true; + }; + AssignGUIComponent(nameof(KarmaPreset), karmaPresetDD); + karmaBox.OnSelected = (tb) => + { + SetElementInteractability(karmaSettingsList.Content, !karmaBox.Selected || KarmaPreset != "custom"); + karmaElements.ForEach(e => e.Visible = tb.Selected); + return true; + }; + karmaElements.ForEach(e => e.Visible = KarmaEnabled); + + listBox.Content.InheritTotalChildrenMinHeight(); + } + + private void CreateBanlistTab(GUIComponent parent) + { + BanList.CreateBanFrame(parent); + } + + private bool SelectSettingsTab(GUIButton button, object obj) + { + selectedTab = (SettingsTab)obj; + foreach (var key in settingsTabs.Keys) + { + settingsTabs[key].Visible = key == selectedTab; + tabButtons[key].Selected = key == selectedTab; + } + return true; + } + + public void Close() + { + if (KarmaPreset == "custom") + { + GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); + GameMain.NetworkMember?.KarmaManager?.Save(); + } + ClientAdminWrite(NetFlags.Properties); + foreach (NetPropertyData prop in netProperties.Values) + { + prop.GUIComponent = null; + } + settingsFrame = null; + //give control of server settings back to elements in the lobby + GameMain.NetLobbyScreen.AssignComponentsToServerSettings(); + } + + public bool ToggleSettingsFrame(GUIButton button, object obj) + { + if (GameMain.NetworkMember == null) { return false; } + if (settingsFrame == null) + { + CreateSettingsFrame(); + } + else + { + Close(); + } + return false; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index 23b4c5b10..c3dbcca81 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -275,7 +275,7 @@ namespace Barotrauma.Networking var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; if (GameMain.Client.Character.IsDead) { messageType = ChatMessageType.Dead; } - GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); + GameMain.Client.Character.ShowTextlessSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); } //encode audio and enqueue it lock (buffers) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 6c664b86d..f87161645 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -106,7 +106,7 @@ namespace Barotrauma.Networking if (client.VoipSound == null) { DebugConsole.Log("Recreating voipsound " + queueId); - client.VoipSound = new VoipSound(client.Name, GameMain.SoundManager, client.VoipQueue); + client.VoipSound = new VoipSound(client, GameMain.SoundManager, client.VoipQueue); } GameMain.SoundManager.ForceStreamUpdate(); client.RadioNoise = 0.0f; @@ -122,12 +122,13 @@ namespace Barotrauma.Networking ChatMessage.CanUseRadio(Character.Controlled, out var recipientRadio) && senderRadio.CanReceive(recipientRadio) ? ChatMessageType.Radio : ChatMessageType.Default; - client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); + client.Character.ShowTextlessSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; client.RadioNoise = 0.0f; if (messageType == ChatMessageType.Radio) { + client.VoipSound.UsingRadio = true; client.VoipSound.SetRange(senderRadio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadio.Range * speechImpedimentMultiplier * rangeMultiplier); if (distanceFactor > RangeNear && !spectating) { @@ -137,11 +138,12 @@ namespace Barotrauma.Networking } else { - client.VoipSound.SetRange(ChatMessage.SpeakRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, ChatMessage.SpeakRange * speechImpedimentMultiplier * rangeMultiplier); + client.VoipSound.UsingRadio = false; + client.VoipSound.SetRange(ChatMessage.SpeakRangeVOIP * RangeNear * speechImpedimentMultiplier * rangeMultiplier, ChatMessage.SpeakRangeVOIP * speechImpedimentMultiplier * rangeMultiplier); } client.VoipSound.UseMuffleFilter = messageType != ChatMessageType.Radio && Character.Controlled != null && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters && - SoundPlayer.ShouldMuffleSound(Character.Controlled, client.Character.WorldPosition, ChatMessage.SpeakRange, client.Character.CurrentHull); + SoundPlayer.ShouldMuffleSound(Character.Controlled, client.Character.WorldPosition, ChatMessage.SpeakRangeVOIP, client.Character.CurrentHull); } GameMain.NetLobbyScreen?.SetPlayerSpeaking(client); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 9d762e0d0..acea82954 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -70,6 +70,8 @@ namespace Barotrauma.Particles public Vector4 ColorMultiplier; + public float VelocityChangeMultiplier; + public bool DrawOnTop { get; private set; } public ParticlePrefab.DrawTargetType DrawTarget @@ -121,8 +123,8 @@ namespace Barotrauma.Particles animState = 0; animFrame = 0; - - currentHull = Hull.FindHull(position, hullGuess); + + currentHull = prefab.CanEnterSubs ? Hull.FindHull(position, hullGuess) : null; size = prefab.StartSizeMin + (prefab.StartSizeMax - prefab.StartSizeMin) * Rand.Range(0.0f, 1.0f); @@ -178,6 +180,8 @@ namespace Barotrauma.Particles HighQualityCollisionDetection = false; + VelocityChangeMultiplier = 1.0f; + OnChangeHull = null; OnCollision = null; @@ -247,8 +251,8 @@ namespace Barotrauma.Particles bool inWater = (currentHull == null || (currentHull.Submarine != null && position.Y - currentHull.Submarine.DrawPosition.Y < currentHull.Surface)); if (inWater) { - velocity.X += velocityChangeWater.X * deltaTime; - velocity.Y += velocityChangeWater.Y * deltaTime; + velocity.X += velocityChangeWater.X * VelocityChangeMultiplier * deltaTime; + velocity.Y += velocityChangeWater.Y * VelocityChangeMultiplier * deltaTime; if (prefab.WaterDrag > 0.0f) { ApplyDrag(prefab.WaterDrag, deltaTime); @@ -256,8 +260,8 @@ namespace Barotrauma.Particles } else { - velocity.X += velocityChange.X * deltaTime; - velocity.Y += velocityChange.Y * deltaTime; + velocity.X += velocityChange.X * VelocityChangeMultiplier * deltaTime; + velocity.Y += velocityChange.Y * VelocityChangeMultiplier * deltaTime; if (prefab.Drag > 0.0f) { ApplyDrag(prefab.Drag, deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 050ee9ded..c120eca23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -132,6 +132,9 @@ namespace Barotrauma.Particles } } + [Editable, Serialize(true, IsPropertySaveable.No, description: "Is the particle considered to be inside a submarine if it spawns at a position inside a hull (causing it to move with the sub)?")] + public bool CanEnterSubs { get; private set; } + [Editable(0.0f, 10000.0f), Serialize(0.0f, IsPropertySaveable.No, description: "Radius of the particle's collider. Only has an effect if UseCollision is set to true.")] public float CollisionRadius { get; private set; } @@ -153,10 +156,10 @@ namespace Barotrauma.Particles //size ----------------------------------------- - [Editable, Serialize("1.0,1.0", IsPropertySaveable.No, description: "The minimum initial size of the particle.")] + [Editable(DecimalCount = 3), Serialize("1.0,1.0", IsPropertySaveable.No, description: "The minimum initial size of the particle.")] public Vector2 StartSizeMin { get; private set; } - [Editable, Serialize("1.0,1.0", IsPropertySaveable.No, description: "The maximum initial size of the particle.")] + [Editable(DecimalCount = 3), Serialize("1.0,1.0", IsPropertySaveable.No, description: "The maximum initial size of the particle.")] public Vector2 StartSizeMax { get; private set; } [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index ec8146ae0..b081a7449 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -1,12 +1,10 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; +using Barotrauma.IO; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Collections.Immutable; -using System.Globalization; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -56,12 +54,16 @@ namespace Barotrauma return null; } - var saveFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, style: "ListBoxElement") + var saveFrame = new GUIFrame( + new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, + style: "ListBoxElement") { UserData = saveInfo }; - var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), Path.GetFileNameWithoutExtension(saveInfo.FilePath), + var nameText = new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), + Path.GetFileNameWithoutExtension(saveInfo.FilePath), textColor: GUIStyle.TextColorBright) { CanBeFocused = false @@ -79,8 +81,10 @@ namespace Barotrauma prevSaveFiles ??= new List(); prevSaveFiles.Add(saveInfo); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), - text: saveInfo.SubmarineName, font: GUIStyle.SmallFont) + new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), + text: saveInfo.SubmarineName, + font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = saveInfo.FilePath @@ -91,8 +95,11 @@ namespace Barotrauma { saveTimeStr = time.ToLocalUserString(); } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), - text: saveTimeStr, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) + new GUITextBlock( + new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), + text: saveTimeStr, + textAlignment: Alignment.Right, + font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = saveInfo.FilePath @@ -127,9 +134,19 @@ namespace Barotrauma public SettingValue TutorialEnabled; public SettingValue RadiationEnabled; public SettingValue MaxMissionCount; - public SettingValue StartingFunds; - public SettingValue Difficulty; + public SettingValue StartingFunds; + public SettingValue WorldHostility; public SettingValue StartItemSet; + public SettingValue CrewVitalityMultiplier; + public SettingValue NonCrewVitalityMultiplier; + public SettingValue OxygenMultiplier; + public SettingValue FuelMultiplier; + public SettingValue MissionRewardMultiplier; + public SettingValue ShopPriceMultiplier; + public SettingValue ShipyardPriceMultiplier; + public SettingValue RepairFailMultiplier; + public SettingValue PatdownProbability; + public SettingValue ShowHuskWarning; public readonly CampaignSettings CreateSettings() { @@ -140,8 +157,18 @@ namespace Barotrauma RadiationEnabled = RadiationEnabled.GetValue(), MaxMissionCount = MaxMissionCount.GetValue(), StartingBalanceAmount = StartingFunds.GetValue(), - Difficulty = Difficulty.GetValue(), - StartItemSet = StartItemSet.GetValue() + WorldHostility = WorldHostility.GetValue(), + StartItemSet = StartItemSet.GetValue(), + CrewVitalityMultiplier = CrewVitalityMultiplier.GetValue(), + NonCrewVitalityMultiplier = NonCrewVitalityMultiplier.GetValue(), + OxygenMultiplier = OxygenMultiplier.GetValue(), + FuelMultiplier = FuelMultiplier.GetValue(), + MissionRewardMultiplier = MissionRewardMultiplier.GetValue(), + ShopPriceMultiplier = ShopPriceMultiplier.GetValue(), + ShipyardPriceMultiplier = ShipyardPriceMultiplier.GetValue(), + RepairFailMultiplier = RepairFailMultiplier.GetValue(), + PatdownProbability = PatdownProbability.GetValue(), + ShowHuskWarning = ShowHuskWarning.GetValue(), }; } } @@ -188,9 +215,16 @@ namespace Barotrauma bool loadingPreset = false; - GUILayoutGroup presetDropdownLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), TextManager.Get("campaignsettingpreset")); - GUIDropDown presetDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), elementCount: CampaignModePresets.List.Length + 1); + GUILayoutGroup presetDropdownLayout = new GUILayoutGroup( + new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), + isHorizontal: true, + childAnchor: Anchor.CenterLeft); + new GUITextBlock( + new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), + TextManager.Get("campaignsettingpreset")); + GUIDropDown presetDropdown = new GUIDropDown( + new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), + elementCount: CampaignModePresets.List.Length + 1); presetDropdown.AddItem(TextManager.Get("karmapreset.custom"), null); presetDropdown.Select(0); @@ -216,37 +250,235 @@ namespace Barotrauma Spacing = GUI.IntScale(5) }; - SettingValue tutorialEnabled = isSinglePlayer ? - CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize, OnValuesChanged) : - new SettingValue(static () => false, static _ => { }); - SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize, OnValuesChanged); + // GENERAL CAMPAIGN SETTINGS: - ImmutableArray> startingSetOptions = StartItemSet.Sets.OrderBy(s => s.Order).Select(set => new SettingCarouselElement(set.Identifier, $"startitemset.{set.Identifier}")).ToImmutableArray(); - SettingCarouselElement prevStartingSet = startingSetOptions.FirstOrNull(element => element.Value == prevSettings.StartItemSet) ?? startingSetOptions[1]; - SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions, OnValuesChanged); + NetLobbyScreen.CreateSubHeader("campaignsettingcategories.general", settingsList.Content); - ImmutableArray> fundOptions = ImmutableArray.Create( - new SettingCarouselElement(StartingBalanceAmount.Low, "startingfunds.low"), - new SettingCarouselElement(StartingBalanceAmount.Medium, "startingfunds.medium"), - new SettingCarouselElement(StartingBalanceAmount.High, "startingfunds.high") + // Tutorial + SettingValue tutorialEnabled = isSinglePlayer + ? CreateTickbox( + settingsList.Content, + TextManager.Get("CampaignOption.EnableTutorial"), + TextManager.Get("campaignoption.enabletutorial.tooltip"), + prevSettings.TutorialEnabled, + verticalSize, + OnValuesChanged) + : new SettingValue(static () => false, static _ => { }); + + // Jovian radiation + SettingValue radiationEnabled = CreateTickbox( + settingsList.Content, + TextManager.Get("CampaignOption.EnableRadiation"), + TextManager.Get("campaignoption.enableradiation.tooltip"), + prevSettings.RadiationEnabled, + verticalSize, + OnValuesChanged); + + // RESOURCE-RELATED CAMPAIGN SETTINGS: + + NetLobbyScreen.CreateSubHeader("campaignsettingcategories.resources", settingsList.Content); + + // Starting set + ImmutableArray> startingSetOptions = + StartItemSet.Sets + .OrderBy(s => s.Order) + .Select(set => new SettingCarouselElement( + set.Identifier, + $"startitemset.{set.Identifier}")) + .ToImmutableArray(); + SettingCarouselElement prevStartingSet = startingSetOptions + .FirstOrNull(element => element.Value == prevSettings.StartItemSet) + ?? startingSetOptions[1]; + SettingValue startingSetInput = CreateSelectionCarousel( + settingsList.Content, + TextManager.Get("startitemset"), + TextManager.Get("startitemsettooltip"), + prevStartingSet, + verticalSize, + startingSetOptions, + OnValuesChanged); + + // Starting money + ImmutableArray> fundOptions = ImmutableArray.Create( + new SettingCarouselElement(StartingBalanceAmountOption.Low, "startingfunds.low"), + new SettingCarouselElement(StartingBalanceAmountOption.Medium, "startingfunds.medium"), + new SettingCarouselElement(StartingBalanceAmountOption.High, "startingfunds.high") ); + SettingCarouselElement prevStartingFund = fundOptions + .FirstOrNull(element => element.Value == prevSettings.StartingBalanceAmount) + ?? fundOptions[1]; + SettingValue startingFundsInput = CreateSelectionCarousel( + settingsList.Content, + TextManager.Get("startingfundsdescription"), + TextManager.Get("startingfundstooltip"), + prevStartingFund, + verticalSize, + fundOptions, + OnValuesChanged); - SettingCarouselElement prevStartingFund = fundOptions.FirstOrNull(element => element.Value == prevSettings.StartingBalanceAmount) ?? fundOptions[1]; - SettingValue startingFundsInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startingfundsdescription"), TextManager.Get("startingfundstooltip"), prevStartingFund, verticalSize, fundOptions, OnValuesChanged); - - ImmutableArray> difficultyOptions = ImmutableArray.Create( - new SettingCarouselElement(GameDifficulty.Easy, "difficulty.easy"), - new SettingCarouselElement(GameDifficulty.Medium, "difficulty.medium"), - new SettingCarouselElement(GameDifficulty.Hard, "difficulty.hard"), - new SettingCarouselElement(GameDifficulty.Hellish, "difficulty.hellish", isHidden: true) - ); - - SettingCarouselElement prevDifficulty = difficultyOptions.FirstOrNull(element => element.Value == prevSettings.Difficulty) ?? difficultyOptions[1]; - SettingValue difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions, OnValuesChanged); - - SettingValue maxMissionCountInput = CreateGUINumberInputCarousel(settingsList.Content, TextManager.Get("maxmissioncount"), TextManager.Get("maxmissioncounttooltip"), + // Max mission count + SettingValue maxMissionCountInput = CreateGUIIntegerInputCarousel( + settingsList.Content, + TextManager.Get("maxmissioncount"), + TextManager.Get("maxmissioncounttooltip"), prevSettings.MaxMissionCount, - valueStep: 1, minValue: CampaignSettings.MinMissionCountLimit, maxValue: CampaignSettings.MaxMissionCountLimit, + valueStep: 1, + minValue: CampaignSettings.MinMissionCountLimit, + maxValue: CampaignSettings.MaxMissionCountLimit, + verticalSize, + OnValuesChanged); + + // Mission reward multiplier + CampaignSettings.MultiplierSettings rewardMultiplierSettings = CampaignSettings.GetMultiplierSettings("MissionRewardMultiplier"); + SettingValue rewardMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.missionrewardmultiplier"), + TextManager.Get("campaignoption.missionrewardmultiplier.tooltip"), + prevSettings.MissionRewardMultiplier, + valueStep: rewardMultiplierSettings.Step, + minValue: rewardMultiplierSettings.Min, + maxValue: rewardMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Shop buying prices multiplier + CampaignSettings.MultiplierSettings shopPriceMultiplierSettings = CampaignSettings.GetMultiplierSettings("ShopPriceMultiplier"); + SettingValue shopPriceMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.shoppricemultiplier"), + TextManager.Get("campaignoption.shoppricemultiplier.tooltip"), + prevSettings.ShopPriceMultiplier, + valueStep: shopPriceMultiplierSettings.Step, + minValue: shopPriceMultiplierSettings.Min, + maxValue: shopPriceMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Shipyard prices multiplier + CampaignSettings.MultiplierSettings shipyardPriceMultiplierSettings = CampaignSettings.GetMultiplierSettings("ShipyardPriceMultiplier"); + SettingValue shipyardPriceMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.shipyardpricemultiplier"), + TextManager.Get("campaignoption.shipyardpricemultiplier.tooltip"), + prevSettings.ShipyardPriceMultiplier, + valueStep: shipyardPriceMultiplierSettings.Step, + minValue: shipyardPriceMultiplierSettings.Min, + maxValue: shipyardPriceMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // OVERALL HAZARD-RELATED CAMPAIGN SETTINGS: + + NetLobbyScreen.CreateSubHeader("campaignsettingcategories.hazards", settingsList.Content); + + // World hostility (used to be "Difficulty" or level difficulty) + ImmutableArray> hostilityOptions = ImmutableArray.Create( + new SettingCarouselElement(WorldHostilityOption.Low, "worldhostility.low"), + new SettingCarouselElement(WorldHostilityOption.Medium, "worldhostility.medium"), + new SettingCarouselElement(WorldHostilityOption.High, "worldhostility.high"), + new SettingCarouselElement(WorldHostilityOption.Hellish, "worldhostility.hellish", isHidden: true) + ); + SettingCarouselElement prevHostility = hostilityOptions + .FirstOrNull(element => element.Value == prevSettings.WorldHostility) + ?? hostilityOptions[1]; + SettingValue hostilityInput = CreateSelectionCarousel( + settingsList.Content, + TextManager.Get("worldhostility"), + TextManager.Get("worldhostility.tooltip"), + prevHostility, + verticalSize, + hostilityOptions, + OnValuesChanged); + + // Crew max vitality multiplier + CampaignSettings.MultiplierSettings crewVitalityMultiplierSettings = CampaignSettings.GetMultiplierSettings("CrewVitalityMultiplier"); + SettingValue crewVitalityMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.maxvitalitymultipliercrew"), + TextManager.Get("campaignoption.maxvitalitymultipliercrew.tooltip"), + prevSettings.CrewVitalityMultiplier, + valueStep: crewVitalityMultiplierSettings.Step, + minValue: crewVitalityMultiplierSettings.Min, + maxValue: crewVitalityMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Non-crew max vitality multiplier + CampaignSettings.MultiplierSettings nonCrewVitalityMultiplierSettings = CampaignSettings.GetMultiplierSettings("NonCrewVitalityMultiplier"); + SettingValue nonCrewVitalityMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.maxvitalitymultipliernoncrew"), + TextManager.Get("campaignoption.maxvitalitymultipliernoncrew.tooltip"), + prevSettings.NonCrewVitalityMultiplier, + valueStep: nonCrewVitalityMultiplierSettings.Step, + minValue: nonCrewVitalityMultiplierSettings.Min, + maxValue: nonCrewVitalityMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Oxygen source multiplier + CampaignSettings.MultiplierSettings oxygenSourceMultiplierSettings = CampaignSettings.GetMultiplierSettings("OxygenMultiplier"); + SettingValue oxygenMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.oxygensourcemultiplier"), + TextManager.Get("campaignoption.oxygensourcemultiplier.tooltip"), + prevSettings.OxygenMultiplier, + valueStep: oxygenSourceMultiplierSettings.Step, + minValue: oxygenSourceMultiplierSettings.Min, + maxValue: oxygenSourceMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Reactor fuel multiplier + CampaignSettings.MultiplierSettings reactorFuelMultiplierSettings = CampaignSettings.GetMultiplierSettings("FuelMultiplier"); + SettingValue fuelMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.reactorfuelmultiplier"), + TextManager.Get("campaignoption.reactorfuelmultiplier.tooltip"), + prevSettings.FuelMultiplier, + valueStep: reactorFuelMultiplierSettings.Step, + minValue: reactorFuelMultiplierSettings.Min, + maxValue: reactorFuelMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Repair fail effect multiplier + CampaignSettings.MultiplierSettings repairFailMultiplierSettings = CampaignSettings.GetMultiplierSettings("RepairFailMultiplier"); + SettingValue repairFailMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.repairfailmultiplier"), + TextManager.Get("campaignoption.repairfailmultiplier.tooltip"), + prevSettings.RepairFailMultiplier, + valueStep: repairFailMultiplierSettings.Step, + minValue: repairFailMultiplierSettings.Min, + maxValue: repairFailMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + ImmutableArray> patdownProbabilityPresets = ImmutableArray.Create( + new SettingCarouselElement(PatdownProbabilityOption.Off, "probability.off"), + new SettingCarouselElement(PatdownProbabilityOption.Low, "probability.low"), + new SettingCarouselElement(PatdownProbabilityOption.Medium, "probability.medium"), + new SettingCarouselElement(PatdownProbabilityOption.High, "probability.high") + ); + SettingCarouselElement prevPatdownProbability = patdownProbabilityPresets + .FirstOrNull(element => element.Value == prevSettings.PatdownProbability) + ?? patdownProbabilityPresets[1]; // middle option + SettingValue patdownProbability = CreateSelectionCarousel( + settingsList.Content, + TextManager.Get("campaignoption.patdownprobability"), + TextManager.Get("campaignoption.patdownprobability.tooltip"), + prevPatdownProbability, + verticalSize, + patdownProbabilityPresets, + OnValuesChanged); + + // Show initial husk warning + SettingValue huskWarning = CreateTickbox( + settingsList.Content, + TextManager.Get("campaignoption.showhuskwarning"), + TextManager.Get("campaignoption.showhuskwarning.tooltip"), + prevSettings.ShowHuskWarning, verticalSize, OnValuesChanged); @@ -259,8 +491,18 @@ namespace Barotrauma radiationEnabled.SetValue(settings.RadiationEnabled); maxMissionCountInput.SetValue(settings.MaxMissionCount); startingFundsInput.SetValue(settings.StartingBalanceAmount); - difficultyInput.SetValue(settings.Difficulty); + hostilityInput.SetValue(settings.WorldHostility); startingSetInput.SetValue(settings.StartItemSet); + crewVitalityMultiplier.SetValue(settings.CrewVitalityMultiplier); + nonCrewVitalityMultiplier.SetValue(settings.NonCrewVitalityMultiplier); + oxygenMultiplier.SetValue(settings.OxygenMultiplier); + fuelMultiplier.SetValue(settings.FuelMultiplier); + rewardMultiplier.SetValue(settings.MissionRewardMultiplier); + shopPriceMultiplier.SetValue(settings.ShopPriceMultiplier); + shipyardPriceMultiplier.SetValue(settings.ShipyardPriceMultiplier); + repairFailMultiplier.SetValue(settings.RepairFailMultiplier); + patdownProbability.SetValue(settings.PatdownProbability); + huskWarning.SetValue(settings.ShowHuskWarning); loadingPreset = false; return true; }; @@ -268,7 +510,7 @@ namespace Barotrauma void OnValuesChanged() { if (loadingPreset) { return; } - presetDropdown.Select(0); + presetDropdown.Select(0); // Switch to the Custom preset if this is an actual user-made change } return new CampaignSettingElements @@ -278,70 +520,178 @@ namespace Barotrauma RadiationEnabled = radiationEnabled, MaxMissionCount = maxMissionCountInput, StartingFunds = startingFundsInput, - Difficulty = difficultyInput, - StartItemSet = startingSetInput + WorldHostility = hostilityInput, + StartItemSet = startingSetInput, + CrewVitalityMultiplier = crewVitalityMultiplier, + NonCrewVitalityMultiplier = nonCrewVitalityMultiplier, + OxygenMultiplier = oxygenMultiplier, + FuelMultiplier = fuelMultiplier, + MissionRewardMultiplier = rewardMultiplier, + ShopPriceMultiplier = shopPriceMultiplier, + ShipyardPriceMultiplier = shipyardPriceMultiplier, + RepairFailMultiplier = repairFailMultiplier, + PatdownProbability = patdownProbability, + ShowHuskWarning = huskWarning, }; - // Create a number input with plus and minus buttons because for some reason the default GUINumberInput buttons don't work when in a GUIMessageBox - static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, int minValue, int maxValue, float verticalSize, Action onChanged) + // Create a number input with plus and minus buttons because for some reason + // the default GUINumberInput buttons don't work when in a GUIMessageBox + static SettingValue CreateGUIIntegerInputCarousel( + GUIComponent parent, + LocalizedString description, + LocalizedString tooltip, + int defaultValue, + int valueStep, + int minValue, + int maxValue, + float verticalSize, + Action onChanged) { - GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); + GUILayoutGroup inputContainer = CreateSettingBase( + parent, + description, + tooltip, + horizontalSize: 0.55f, + verticalSize: verticalSize); - GUIButton minusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) - { - ClickSound = GUISoundType.Decrease, - UserData = -valueStep - }; - GUINumberInput numberInput = new GUINumberInput(new RectTransform(Vector2.One, inputContainer.RectTransform, Anchor.Center), NumberType.Int, textAlignment: Alignment.Center, style: "GUITextBox", - hidePlusMinusButtons: true) + GUIButton minusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIMinusButton", + textAlignment: Alignment.Center); + RectTransform numberInputRect = new(Vector2.One, inputContainer.RectTransform, Anchor.Center); + GUIButton plusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIPlusButton", + textAlignment: Alignment.Center); + GUINumberInput numberInput = new GUINumberInput( + numberInputRect, + NumberType.Int, + textAlignment: Alignment.Center, + style: "GUITextBox", + buttonVisibility: GUINumberInput.ButtonVisibility.ForceVisible, + customPlusMinusButtons: (plusButton, minusButton)) { IntValue = defaultValue, MinValueInt = minValue, - MaxValueInt = maxValue + MaxValueInt = maxValue, + ValueStep = valueStep, + ToolTip = tooltip }; inputContainer.RectTransform.Parent.MinSize = new Point(0, numberInput.RectTransform.MinSize.Y); - GUIButton plusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) - { - ClickSound = GUISoundType.Increase, - UserData = valueStep - }; - - minusButton.OnClicked = plusButton.OnClicked = ChangeValue; numberInput.OnValueChanged += _ => onChanged(); - bool ChangeValue(GUIButton btn, object userData) - { - if (userData is not int change) { return false; } - - numberInput.IntValue += change; - return true; - } - - return new SettingValue(() => numberInput.IntValue, i => numberInput.IntValue = i); + return new SettingValue( + () => numberInput.IntValue, + i => numberInput.IntValue = i); } - static SettingValue CreateSelectionCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, SettingCarouselElement defaultValue, float verticalSize, - ImmutableArray> options, Action onChanged) + static SettingValue CreateGUIFloatInputCarousel( + GUIComponent parent, + LocalizedString description, + LocalizedString tooltip, + float defaultValue, + float valueStep, + float minValue, + float maxValue, + float verticalSize, + Action onChanged) { - GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); + GUILayoutGroup inputContainer = CreateSettingBase( + parent, + description, + tooltip, + horizontalSize: 0.55f, + verticalSize: verticalSize); - GUIButton minusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonToggleLeft", textAlignment: Alignment.Center) { UserData = -1 }; - GUIFrame inputFrame = new GUIFrame(new RectTransform(Vector2.One, inputContainer.RectTransform), style: null); - GUINumberInput numberInput = new GUINumberInput(new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), NumberType.Int, textAlignment: Alignment.Center, style: "GUITextBox", hidePlusMinusButtons: true) + GUIButton minusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIMinusButton", + textAlignment: Alignment.Center); + RectTransform numberInputRect = new(Vector2.One, inputContainer.RectTransform, Anchor.Center); + GUIButton plusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIPlusButton", + textAlignment: Alignment.Center); + GUINumberInput numberInput = new GUINumberInput( + numberInputRect, + NumberType.Float, + textAlignment: Alignment.Center, + style: "GUITextBox", + buttonVisibility: GUINumberInput.ButtonVisibility.ForceVisible, + customPlusMinusButtons: (plusButton, minusButton)) + { + FloatValue = defaultValue, + MinValueFloat = minValue, + MaxValueFloat = maxValue, + ValueStep = valueStep, + ToolTip = tooltip + }; + numberInput.RectTransform.Parent.MinSize = new Point(0, numberInput.RectTransform.MinSize.Y); + + numberInput.OnValueChanged += _ => onChanged(); + + return new SettingValue( + () => numberInput.FloatValue, + i => numberInput.FloatValue = (float)Math.Round(i, 1)); + } + + static SettingValue CreateSelectionCarousel( + GUIComponent parent, + LocalizedString description, + LocalizedString tooltip, + SettingCarouselElement defaultValue, + float verticalSize, + ImmutableArray> options, + Action onChanged) + { + GUILayoutGroup inputContainer = CreateSettingBase( + parent, + description, + tooltip, + horizontalSize: 0.55f, + verticalSize: verticalSize); + + GUIButton minusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIButtonToggleLeft", + textAlignment: Alignment.Center) + { + UserData = -1 + }; + GUIFrame inputFrame = new GUIFrame( + new RectTransform(Vector2.One, inputContainer.RectTransform), + style: null); + GUINumberInput numberInput = new GUINumberInput( + new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), + NumberType.Int, + textAlignment: Alignment.Center, + style: "GUITextBox", + buttonVisibility: GUINumberInput.ButtonVisibility.ForceHidden) { IntValue = options.IndexOf(defaultValue), MinValueInt = 0, MaxValueInt = options.Length, - Visible = false + Visible = false, + ToolTip = tooltip }; inputContainer.RectTransform.Parent.MinSize = new Point(0, numberInput.RectTransform.MinSize.Y); - GUITextBox inputLabel = new GUITextBox(new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), text: defaultValue.Label.Value, textAlignment: Alignment.Center, createPenIcon: false) + GUITextBox inputLabel = new GUITextBox( + new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), + text: defaultValue.Label.Value, + textAlignment: Alignment.Center, + createPenIcon: false) { CanBeFocused = false }; - GUIButton plusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonToggleRight", textAlignment: Alignment.Center) { UserData = 1 }; + GUIButton plusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIButtonToggleRight", + textAlignment: Alignment.Center) + { + UserData = 1 + }; minusButton.OnClicked = plusButton.OnClicked = ChangeValue; @@ -381,20 +731,33 @@ namespace Barotrauma inputLabel.Text = options[value].Label.Value; } - return new SettingValue(() => options[numberInput.IntValue].Value, t => SetValue(options.IndexOf(e => Equals(e.Value, t)))); + return new SettingValue( + () => options[numberInput.IntValue].Value, + t => SetValue(options.IndexOf(e => Equals(e.Value, t))) + ); } - static SettingValue CreateTickbox(GUIComponent parent, LocalizedString description, LocalizedString tooltip, bool defaultValue, float verticalSize, Action onChanged) + static SettingValue CreateTickbox( + GUIComponent parent, + LocalizedString description, + LocalizedString tooltip, + bool defaultValue, + float verticalSize, + Action onChanged) { - GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, 0.7f, verticalSize); - GUILayoutGroup tickboxContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), inputContainer.RectTransform), childAnchor: Anchor.Center); - GUITickBox tickBox = new GUITickBox(new RectTransform(Vector2.One, tickboxContainer.RectTransform), string.Empty) + GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, 0.625f, verticalSize); + GUILayoutGroup tickboxContainer = new GUILayoutGroup( + new RectTransform(new Vector2(0.375f, 1.0f), inputContainer.RectTransform), + childAnchor: Anchor.Center); + GUITickBox tickBox = new GUITickBox( + new RectTransform(Vector2.One, tickboxContainer.RectTransform), + string.Empty) { Selected = defaultValue, ToolTip = tooltip }; tickBox.Box.IgnoreLayoutGroups = true; - tickBox.Box.RectTransform.SetPosition(Anchor.CenterRight); + tickBox.Box.RectTransform.SetPosition(Anchor.CenterLeft); inputContainer.RectTransform.Parent.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); tickBox.OnSelected += _ => @@ -406,11 +769,29 @@ namespace Barotrauma return new SettingValue(() => tickBox.Selected, b => tickBox.Selected = b); } - static GUILayoutGroup CreateSettingBase(GUIComponent parent, LocalizedString description, LocalizedString tooltip, float horizontalSize, float verticalSize) + static GUILayoutGroup CreateSettingBase( + GUIComponent parent, + LocalizedString description, + LocalizedString tooltip, + float horizontalSize, + float verticalSize) { - GUILayoutGroup settingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(horizontalSize, 1f), settingHolder.RectTransform), description, font: parent.Rect.Width < 320 ? GUIStyle.SmallFont : GUIStyle.Font, wrap: true) { ToolTip = tooltip }; - GUILayoutGroup inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f - horizontalSize, 0.8f), settingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + GUILayoutGroup settingHolder = new GUILayoutGroup( + new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), + isHorizontal: true, + childAnchor: Anchor.CenterLeft); + GUITextBlock descriptionBlock = new GUITextBlock( + new RectTransform(new Vector2(horizontalSize, 1f), settingHolder.RectTransform), + description, + font: parent.Rect.Width < 320 ? GUIStyle.SmallFont : GUIStyle.Font, + wrap: true) + { + ToolTip = tooltip + }; + GUILayoutGroup inputContainer = new GUILayoutGroup( + new RectTransform(new Vector2(1f - horizontalSize, 0.8f), settingHolder.RectTransform), + isHorizontal: true, + childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 0a4a9958e..4d153ba33 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -1,10 +1,7 @@ -using Barotrauma.Extensions; -using Barotrauma.IO; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; namespace Barotrauma { @@ -14,16 +11,20 @@ namespace Barotrauma private int prevInitialMoney; + private CampaignSettingElements campaignSettingElements; + + public bool LoadGameMenuVisible => loadGameContainer is { Visible: true }; + public MultiPlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, List saveFiles = null) : base(newGameContainer, loadGameContainer) { var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: false) { Stretch = true, - RelativeSpacing = 0.05f + RelativeSpacing = 0.025f }; - GUILayoutGroup nameSeedLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), verticalLayout.RectTransform), isHorizontal: false) + GUILayoutGroup nameSeedLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), verticalLayout.RectTransform), isHorizontal: false) { Stretch = true }; @@ -31,119 +32,41 @@ namespace Barotrauma GUILayoutGroup campaignSettingLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), verticalLayout.RectTransform), isHorizontal: false) { Stretch = true, - RelativeSpacing = 0.05f + RelativeSpacing = 0.0f }; // New game - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform), string.Empty) + var saveLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("SaveName"), textAlignment: Alignment.CenterLeft); + saveNameBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), saveLabel.RectTransform, Anchor.CenterRight), string.Empty) { textFilterFunction = ToolBox.RemoveInvalidFileNameChars }; + saveLabel.InheritTotalChildrenMinHeight(); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform), ToolBox.RandomSeed(8)); + var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("MapSeed"), textAlignment: Alignment.CenterLeft); + seedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight), ToolBox.RandomSeed(8)); + seedLabel.InheritTotalChildrenMinHeight(); - nameSeedLayout.RectTransform.MinSize = new Point(0, nameSeedLayout.Children.Sum(c => c.RectTransform.MinSize.Y)); + nameSeedLayout.InheritTotalChildrenMinHeight(); - CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty, false); + campaignSettingElements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty, false); var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), - verticalLayout.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); + verticalLayout.RectTransform) { MaxSize = new Point(int.MaxValue, GUI.IntScale(30)) }, childAnchor: Anchor.BottomRight, isHorizontal: true); - StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("StartCampaignButton")) - { - OnClicked = (GUIButton btn, object userData) => - { - if (string.IsNullOrWhiteSpace(saveNameBox.Text)) - { - saveNameBox.Flash(GUIStyle.Red); - return false; - } - - SubmarineInfo selectedSub = null; - - if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } - selectedSub = GameMain.NetLobbyScreen.SelectedSub; - - if (selectedSub.SubmarineClass == SubmarineClass.Undefined) - { - new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); - return false; - } - - if (string.IsNullOrEmpty(selectedSub.MD5Hash.StringRepresentation)) - { - new GUIMessageBox(TextManager.Get("error"), TextManager.Get("nohashsubmarineselected")); - return false; - } - - string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveNameBox.Text); - bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - - CampaignSettings settings = elements.CreateSettings(); - - if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) - { - if (!hasRequiredContentPackages) - { - var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), - TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), - new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); - - msgBox.Buttons[0].OnClicked = msgBox.Close; - msgBox.Buttons[0].OnClicked += (button, obj) => - { - if (GUIMessageBox.MessageBoxes.Count == 0) - { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } - return true; - }; - - msgBox.Buttons[1].OnClicked = msgBox.Close; - } - - if (selectedSub.HasTag(SubmarineTag.Shuttle)) - { - var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), - TextManager.Get("ShuttleWarning"), - new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); - - msgBox.Buttons[0].OnClicked = (button, obj) => - { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - - msgBox.Buttons[1].OnClicked = msgBox.Close; - return false; - } - } - else - { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } - - return true; - } - }; - StartButton.RectTransform.MaxSize = RectTransform.MaxPoint; - StartButton.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); - - prevInitialMoney = 8000; - InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUIStyle.SmallFont, textColor: GUIStyle.Green) + prevInitialMoney = CampaignSettings.DefaultInitialMoney; + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUIStyle.SmallFont, textColor: GUIStyle.Green, textAlignment: Alignment.CenterRight) { TextGetter = () => { - int initialMoney = 8000; - if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition)) + int defaultInitialMoney = CampaignSettings.DefaultInitialMoney; + int initialMoney = defaultInitialMoney; + if (CampaignModePresets.TryGetAttribute( + nameof(CampaignSettings.StartingBalanceAmount).ToIdentifier(), + campaignSettingElements.StartingFunds.GetValue().ToIdentifier(), + out var attribute)) { - initialMoney = definition.GetInt(elements.StartingFunds.GetValue().ToIdentifier()); + initialMoney = attribute.GetAttributeInt(defaultInitialMoney); } if (prevInitialMoney != initialMoney) { @@ -165,6 +88,87 @@ namespace Barotrauma CreateLoadMenu(saveFiles); } + public bool StartGameClicked(GUIButton button, object userdata) + { + if (string.IsNullOrWhiteSpace(saveNameBox.Text)) + { + saveNameBox.Flash(GUIStyle.Red, flashDuration: 5.0f); + saveNameBox.Pulsate(Vector2.One, Vector2.One * 1.2f, duration: 2.0f); + newGameContainer?.Flash(GUIStyle.Red, flashDuration: 0.5f); + return false; + } + + SubmarineInfo selectedSub = null; + + if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } + selectedSub = GameMain.NetLobbyScreen.SelectedSub; + + if (selectedSub.SubmarineClass == SubmarineClass.Undefined) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); + return false; + } + + if (string.IsNullOrEmpty(selectedSub.MD5Hash.StringRepresentation)) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("nohashsubmarineselected")); + return false; + } + + string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveNameBox.Text); + bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; + + CampaignSettings settings = campaignSettingElements.CreateSettings(); + + if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) + { + if (!hasRequiredContentPackages) + { + var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), + TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + + msgBox.Buttons[0].OnClicked = msgBox.Close; + msgBox.Buttons[0].OnClicked += (button, obj) => + { + if (GUIMessageBox.MessageBoxes.Count == 0) + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + } + return true; + }; + + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + if (selectedSub.HasTag(SubmarineTag.Shuttle)) + { + var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), + TextManager.Get("ShuttleWarning"), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + + msgBox.Buttons[0].OnClicked = (button, obj) => + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + + msgBox.Buttons[1].OnClicked = msgBox.Close; + return false; + } + } + else + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + } + + return true; + } + private IEnumerable WaitForCampaignSetup() { GUI.SetCursorWaiting(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 7dc80f765..9c1871345 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -127,7 +127,7 @@ namespace Barotrauma.CharacterEditor { Submarine.MainSub.PhysicsBody.Enabled = false; } - originalWall = new WallGroup(new List(Structure.WallList)); + wallGroups[0] = new WallGroup(new List(MapEntity.MapEntityList)); CloneWalls(); CalculateMovementLimits(); isEndlessRunner = true; @@ -554,8 +554,8 @@ namespace Barotrauma.CharacterEditor selectedJoints.Clear(); foreach (var w in jointSelectionWidgets.Values) { - w.refresh(); - w.linkedWidget?.refresh(); + w.Refresh(); + w.LinkedWidget?.Refresh(); } reset = true; } @@ -677,8 +677,8 @@ namespace Barotrauma.CharacterEditor } character.ControlLocalPlayer((float)deltaTime, Cam, false); character.Control((float)deltaTime, Cam); - character.AnimController.UpdateAnim((float)deltaTime); - character.AnimController.Update((float)deltaTime, Cam); + character.AnimController.UpdateAnimations((float)deltaTime); + character.AnimController.UpdateRagdoll((float)deltaTime, Cam); character.CurrentHull = character.AnimController.CurrentHull; if (isEndlessRunner) { @@ -722,7 +722,7 @@ namespace Barotrauma.CharacterEditor limbEditWidgets.Values.ForEach(w => w.Update((float)deltaTime)); animationWidgets.Values.ForEach(w => w.Update((float)deltaTime)); // Handle limb selection - if (PlayerInput.PrimaryMouseButtonDown() && GUI.MouseOn == null && Widget.selectedWidgets.None()) + if (PlayerInput.PrimaryMouseButtonDown() && GUI.MouseOn == null && Widget.SelectedWidgets.None()) { foreach (Limb limb in character.AnimController.Limbs) { @@ -779,7 +779,7 @@ namespace Barotrauma.CharacterEditor Submarine.MainSub?.UpdateTransform(); // Lightmaps - if (GameMain.LightManager.LightingEnabled) + if (GameMain.LightManager.LightingEnabled && Character.Controlled != null) { GameMain.LightManager.ObstructVision = Character.Controlled.ObstructVision; GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam); @@ -791,7 +791,8 @@ namespace Barotrauma.CharacterEditor // Submarine spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: Cam.Transform); - Submarine.Draw(spriteBatch, isEndlessRunner); + Submarine.DrawBack(spriteBatch, editing: isEndlessRunner); + Submarine.DrawFront(spriteBatch, editing: isEndlessRunner); spriteBatch.End(); // Character(s) @@ -831,7 +832,7 @@ namespace Barotrauma.CharacterEditor { if (selectedLimbs.Contains(limb) || selectedLimbs.None()) { - limb.DrawDamageModifiers(spriteBatch, cam, SimToScreen(limb.SimPosition), isScreenSpace: true); + limb.DrawDamageModifiers(spriteBatch, cam, cam.WorldToScreen(limb.DrawPosition), isScreenSpace: true); } } } @@ -902,8 +903,8 @@ namespace Barotrauma.CharacterEditor if (jointStartLimb != null) { // TODO: there's something wrong here - var offset = anchor1Pos.HasValue ? Vector2.Transform(ConvertUnits.ToSimUnits(anchor1Pos.Value), Matrix.CreateRotationZ(jointStartLimb.Rotation)) : Vector2.Zero; - var startPos = SimToScreen(jointStartLimb.SimPosition + offset); + var offset = anchor1Pos.HasValue ? Vector2.Transform(anchor1Pos.Value, Matrix.CreateRotationZ(jointStartLimb.Rotation)) : Vector2.Zero; + var startPos = cam.WorldToScreen(jointStartLimb.DrawPosition + offset); GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUIStyle.Green, width: 3); } } @@ -915,8 +916,7 @@ namespace Barotrauma.CharacterEditor } if (isEndlessRunner) { - Structure wall = CurrentWall.walls.FirstOrDefault(); - Vector2 indicatorPos = wall == null ? originalWall.walls.First().DrawPosition : wall.DrawPosition; + Vector2 indicatorPos = MiddleWall.Entities.First().DrawPosition; GUI.DrawIndicator(spriteBatch, indicatorPos, Cam, 700, GUIStyle.SubmarineLocationIcon.Value.Sprite, Color.White); } GUI.Draw(Cam, spriteBatch); @@ -951,9 +951,10 @@ namespace Barotrauma.CharacterEditor { var topLeft = spriteSheetControls.RectTransform.TopLeft; bool useSpritesheetOrientation = float.IsNaN(lastLimb.Params.SpriteOrientation); - GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteOrientation") + ":", useSpritesheetOrientation ? Color.White : Color.Yellow, Color.Gray * 0.5f, 10, GUIStyle.Font); + GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteOrientation")+":", useSpritesheetOrientation ? Color.White : Color.Yellow, Color.Gray * 0.5f, 10, GUIStyle.Font); float orientation = useSpritesheetOrientation ? RagdollParams.SpritesheetOrientation : lastLimb.Params.SpriteOrientation; - DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), orientation, string.Empty, useSpritesheetOrientation ? Color.White : Color.Yellow, + DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), orientation, + GetCharacterEditorTranslation("spriteorientationtooltip") + "\n\n" + GetCharacterEditorTranslation("generalorientationtooltip"), useSpritesheetOrientation ? Color.White : Color.Yellow, angle => { TryUpdateSubParam(lastLimb.Params, "spriteorientation".ToIdentifier(), angle); @@ -968,7 +969,8 @@ namespace Barotrauma.CharacterEditor { var topLeft = spriteSheetControls.RectTransform.TopLeft; GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteSheetOrientation") + ":", Color.White, Color.Gray * 0.5f, 10, GUIStyle.Font); - DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), RagdollParams.SpritesheetOrientation, string.Empty, Color.White, + DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), RagdollParams.SpritesheetOrientation, + GetCharacterEditorTranslation("spritesheetorientationtooltip") + "\n\n" + GetCharacterEditorTranslation("generalorientationtooltip"), Color.White, angle => TryUpdateRagdollParam("spritesheetorientation", angle), circleRadius: 40, widgetSize: 15, rotationOffset: 0, autoFreeze: false, rounding: 10); } } @@ -1342,92 +1344,61 @@ namespace Barotrauma.CharacterEditor private int max; private void CalculateMovementLimits() { - min = CurrentWall.walls.Select(w => w.Rect.Left).OrderBy(p => p).First(); - max = CurrentWall.walls.Select(w => w.Rect.Right).OrderBy(p => p).Last(); + min = MiddleWall.Entities.Select(w => w.Rect.Left).OrderBy(p => p).First(); + max = MiddleWall.Entities.Select(w => w.Rect.Right).OrderBy(p => p).Last(); } - private WallGroup originalWall; - private WallGroup[] clones = new WallGroup[3]; - private IEnumerable AllWalls => originalWall.walls.Concat(clones.SelectMany(c => c.walls)); + private readonly WallGroup[] wallGroups = new WallGroup[3]; - private WallGroup _currentWall; - private WallGroup CurrentWall - { - get - { - if (_currentWall == null) - { - _currentWall = originalWall; - } - return _currentWall; - } - set - { - _currentWall = value; - } - } + private WallGroup MiddleWall => wallGroups[1]; + + private IEnumerable AllStructures => wallGroups.SelectMany(c => c.Entities); private class WallGroup { - public readonly List walls; + public readonly List Entities; - public WallGroup(List walls) + public WallGroup(List entities) { - this.walls = walls; + Entities = entities; } public WallGroup Clone() { - var clones = new List(); - walls.ForEachMod(w => clones.Add(w.Clone() as Structure)); + var clones = new List(); + Entities.ForEachMod(w => clones.Add(w.Clone())); return new WallGroup(clones); } } private void CloneWalls() { - for (int i = 0; i < 3; i++) + var originalWall = wallGroups[0]; + int moveAmount = originalWall.Entities.FirstOrDefault(e => e is Structure).Rect.Width; + for (int i = 1; i <= 2; i++) { - clones[i] = originalWall.Clone(); - for (int j = 0; j < originalWall.walls.Count; j++) + wallGroups[i] = originalWall.Clone(); + foreach (var entity in wallGroups[i].Entities) { - if (i == 1) - { - clones[i].walls[j].Move(new Vector2(originalWall.walls[j].Rect.Width, 0)); - } - else if (i == 2) - { - clones[i].walls[j].Move(new Vector2(-originalWall.walls[j].Rect.Width, 0)); - } + entity.Move(new Vector2(moveAmount * i, 0)); } } } - private WallGroup SelectClosestWallGroup(Vector2 pos) - { - var closestWall = clones.SelectMany(c => c.walls).OrderBy(w => Vector2.Distance(pos, w.Position)).First(); - return clones.Where(c => c.walls.Contains(closestWall)).FirstOrDefault(); - } - - private WallGroup SelectLastClone(bool right) - { - var lastWall = right - ? clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Right).Last() - : clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Left).First(); - return clones.Where(c => c.walls.Contains(lastWall)).FirstOrDefault(); - } - private void UpdateWalls(bool right) { - CurrentWall = SelectClosestWallGroup(character.Position); - CalculateMovementLimits(); - var lastClone = SelectLastClone(!right); - for (int i = 0; i < lastClone.walls.Count; i++) + int moveAmount = wallGroups[0].Entities.FirstOrDefault(e => e is Structure).Rect.Width; + int amount = right ? moveAmount : -moveAmount; + foreach (var wallGroup in wallGroups) { - var amount = right ? lastClone.walls[i].Rect.Width : -lastClone.walls[i].Rect.Width; - var distance = CurrentWall.walls[i].Position.X - lastClone.walls[i].Position.X; - lastClone.walls[i].Move(new Vector2(amount + distance, 0)); + foreach (var entity in wallGroup.Entities) + { + entity.Move(new Vector2(amount, 0)); + } } + + CalculateMovementLimits(); + GameMain.World.ProcessChanges(); } @@ -1437,7 +1408,7 @@ namespace Barotrauma.CharacterEditor if (!isEndlessRunner) { return; } wallCollisionsEnabled = enabled; var collisionCategory = enabled ? FarseerPhysics.Dynamics.Category.Cat1 : FarseerPhysics.Dynamics.Category.None; - AllWalls.ForEach(w => w.SetCollisionCategory(collisionCategory)); + AllStructures.ForEach(w => (w as Structure)?.SetCollisionCategory(collisionCategory)); GameMain.World.ProcessChanges(); } #endregion @@ -1606,7 +1577,7 @@ namespace Barotrauma.CharacterEditor private void ClearWidgets() { - Widget.selectedWidgets.Clear(); + Widget.SelectedWidgets.Clear(); animationWidgets.Clear(); jointSelectionWidgets.Clear(); limbEditWidgets.Clear(); @@ -1618,8 +1589,8 @@ namespace Barotrauma.CharacterEditor selectedJoints.Clear(); foreach (var w in jointSelectionWidgets.Values) { - w.refresh(); - w.linkedWidget?.refresh(); + w.Refresh(); + w.LinkedWidget?.Refresh(); } } @@ -1666,6 +1637,11 @@ namespace Barotrauma.CharacterEditor public bool CreateCharacter(Identifier name, string mainFolder, bool isHumanoid, ContentPackage contentPackage, XElement ragdoll, XElement config = null, IEnumerable animations = null) { + if (name.IsEmpty) + { + throw new ArgumentException("Name cannot be empty."); + } + var vanilla = GameMain.VanillaContent; if (contentPackage == null) @@ -2865,6 +2841,8 @@ namespace Barotrauma.CharacterEditor #endif character.AnimController.SaveRagdoll(inputField.Text); GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.Path.Value), Color.Green, font: GUIStyle.Font); + RagdollParams.ClearCache(); + ResetParamsEditor(); box.Close(); return true; }; @@ -2950,7 +2928,10 @@ namespace Barotrauma.CharacterEditor loadBox.Buttons[1].OnClicked += (btn, data) => { string fileName = Path.GetFileNameWithoutExtension(selectedFile); - var ragdoll = character.IsHumanoid ? HumanRagdollParams.GetRagdollParams(character.SpeciesName, fileName) as RagdollParams : RagdollParams.GetRagdollParams(character.SpeciesName, fileName); + Identifier baseSpecies = character.GetBaseCharacterSpeciesName(); + var ragdoll = character.IsHumanoid + ? RagdollParams.GetRagdollParams(character.SpeciesName, baseSpecies, fileName, character.Prefab.ContentPackage) as RagdollParams + : RagdollParams.GetRagdollParams(character.SpeciesName, baseSpecies, fileName, character.Prefab.ContentPackage); ragdoll.Reset(true); GUI.AddMessage(GetCharacterEditorTranslation("RagdollLoadedFrom").Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUIStyle.Font); RecreateRagdoll(ragdoll); @@ -3003,8 +2984,11 @@ namespace Barotrauma.CharacterEditor #endif var animParams = character.AnimController.GetAnimationParamsFromType(selectedType); if (animParams == null) { return true; } - animParams.Save(inputField.Text); - GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeSavedTo").Replace("[type]", animParams.AnimationType.ToString()).Replace("[path]", animParams.Path.Value), Color.Green, font: GUIStyle.Font); + string fileName = inputField.Text; + animParams.Save(fileName); + string newPath = animParams.Path.ToString(); + GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeSavedTo").Replace("[type]", selectedType.ToString()).Replace("[path]", newPath), Color.Green, font: GUIStyle.Font); + AnimationParams.ClearCache(); ResetParamsEditor(); box.Close(); return true; @@ -3047,7 +3031,7 @@ namespace Barotrauma.CharacterEditor { listBox.ClearChildren(); var filePaths = Directory.GetFiles(CurrentAnimation.Folder); - foreach (var path in AnimationParams.FilterFilesByType(filePaths, selectedType)) + foreach (var path in AnimationParams.FilterAndSortFiles(filePaths, selectedType)) { GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUIStyle.Font, listBox.Rect.Width - 80)) { @@ -3068,7 +3052,7 @@ namespace Barotrauma.CharacterEditor { selectedFile = data as string; // Don't allow to delete the animation that is currently in use, nor the default file. - var fileName = Path.GetFileNameWithoutExtension(selectedFile); + string fileName = Path.GetFileNameWithoutExtension(selectedFile); deleteButton.Enabled = fileName != CurrentAnimation.Name && fileName != AnimationParams.GetDefaultFileName(character.SpeciesName, CurrentAnimation.AnimationType); return true; }; @@ -3108,54 +3092,11 @@ namespace Barotrauma.CharacterEditor }; loadBox.Buttons[1].OnClicked += (btn, data) => { - string fileName = Path.GetFileNameWithoutExtension(selectedFile); - if (character.IsHumanoid && character.AnimController is HumanoidAnimController humanAnimController) + if (character.AnimController.TryLoadAnimation(selectedType, Path.GetFileNameWithoutExtension(selectedFile), out AnimationParams animationParams, throwErrors: true)) { - switch (selectedType) - { - case AnimationType.Walk: - humanAnimController.WalkParams = HumanWalkParams.GetAnimParams(character, fileName); - break; - case AnimationType.Run: - humanAnimController.RunParams = HumanRunParams.GetAnimParams(character, fileName); - break; - case AnimationType.Crouch: - humanAnimController.HumanCrouchParams = HumanCrouchParams.GetAnimParams(character, fileName); - break; - case AnimationType.SwimSlow: - humanAnimController.SwimSlowParams = HumanSwimSlowParams.GetAnimParams(character, fileName); - break; - case AnimationType.SwimFast: - humanAnimController.SwimFastParams = HumanSwimFastParams.GetAnimParams(character, fileName); - break; - default: - DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("AnimationTypeNotImplemented").Replace("[type]", selectedType.ToString())); - break; - } + animationParams.Reset(forceReload: true); + GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeLoaded").Replace("[type]", selectedType.ToString()).Replace("[file]", animationParams.FileNameWithoutExtension), Color.WhiteSmoke, font: GUIStyle.Font); } - else - { - switch (selectedType) - { - case AnimationType.Walk: - character.AnimController.WalkParams = FishWalkParams.GetAnimParams(character, fileName); - break; - case AnimationType.Run: - character.AnimController.RunParams = FishRunParams.GetAnimParams(character, fileName); - break; - case AnimationType.SwimSlow: - character.AnimController.SwimSlowParams = FishSwimSlowParams.GetAnimParams(character, fileName); - break; - case AnimationType.SwimFast: - character.AnimController.SwimFastParams = FishSwimFastParams.GetAnimParams(character, fileName); - break; - default: - DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("AnimationTypeNotImplemented").Replace("[type]", selectedType.ToString())); - break; - } - } - GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeLoaded").Replace("[type]", selectedType.ToString()).Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUIStyle.Font); - character.AnimController.AllAnimParams.ForEach(a => a.Reset(forceReload: true)); ResetParamsEditor(); loadBox.Close(); return true; @@ -3810,7 +3751,7 @@ namespace Barotrauma.CharacterEditor private void DrawAnimationControls(SpriteBatch spriteBatch, float deltaTime) { var collider = character.AnimController.Collider; - var colliderDrawPos = SimToScreen(collider.SimPosition); + var colliderDrawPos = cam.WorldToScreen(collider.DrawPosition); var animParams = character.AnimController.CurrentAnimationParams; var groundedParams = animParams as GroundedMovementParams; var humanParams = animParams as IHumanAnimation; @@ -3837,20 +3778,20 @@ namespace Barotrauma.CharacterEditor GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 120, 150), GetCharacterEditorTranslation("HoldLeftAltToAdjustCycleSpeed"), Color.White, Color.Black * 0.5f, 10, GUIStyle.Font); } // Widgets for all anims --> - Vector2 referencePoint = SimToScreen(head != null ? head.SimPosition: collider.SimPosition); + Vector2 referencePoint = cam.WorldToScreen(head != null ? head.DrawPosition: collider.DrawPosition); Vector2 drawPos = referencePoint; if (ShowCycleWidget()) { - GetAnimationWidget("CycleSpeed", Color.MediumPurple, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: Widget.Shape.Circle, initMethod: w => + GetAnimationWidget("CycleSpeed", Color.MediumPurple, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: WidgetShape.Circle, initMethod: w => { float multiplier = 0.5f; - w.tooltip = GetCharacterEditorTranslation("CycleSpeed"); - w.refresh = () => + w.Tooltip = GetCharacterEditorTranslation("CycleSpeed"); + w.Refresh = () => { - var refPoint = SimToScreen(head != null ? head.SimPosition : collider.SimPosition); + var refPoint = cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition); w.DrawPos = refPoint + GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(CurrentAnimation.CycleSpeed * multiplier) * Cam.Zoom; // Update tooltip, because the cycle speed might be automatically adjusted by the movement speed widget. - w.tooltip = $"{GetCharacterEditorTranslation("CycleSpeed")}: {CurrentAnimation.CycleSpeed.FormatDoubleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("CycleSpeed")}: {CurrentAnimation.CycleSpeed.FormatDoubleDecimal()}"; }; w.MouseHeld += dTime => { @@ -3859,7 +3800,7 @@ namespace Barotrauma.CharacterEditor //w.DrawPos = newPos; float speed = CurrentAnimation.CycleSpeed + ConvertUnits.ToSimUnits(Vector2.Multiply(PlayerInput.MouseSpeed / multiplier, GetScreenSpaceForward()).Combine()) / Cam.Zoom; TryUpdateAnimParam("cyclespeed", speed); - w.tooltip = $"{GetCharacterEditorTranslation("CycleSpeed")}: {CurrentAnimation.CycleSpeed.FormatDoubleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("CycleSpeed")}: {CurrentAnimation.CycleSpeed.FormatDoubleDecimal()}"; }; // Additional check, which overrides the previous value (because evaluated last) w.PreUpdate += dTime => @@ -3874,27 +3815,27 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; w.PostDraw += (sp, dTime) => { if (w.IsSelected) { - GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(head != null ? head.SimPosition : collider.SimPosition), Color.MediumPurple); + GUI.DrawLine(spriteBatch, w.DrawPos, cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition), Color.MediumPurple); } }; }).Draw(spriteBatch, deltaTime); } else { - GetAnimationWidget("MovementSpeed", Color.Turquoise, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: Widget.Shape.Circle, initMethod: w => + GetAnimationWidget("MovementSpeed", Color.Turquoise, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: WidgetShape.Circle, initMethod: w => { float multiplier = 0.5f; - w.tooltip = GetCharacterEditorTranslation("MovementSpeed"); - w.refresh = () => + w.Tooltip = GetCharacterEditorTranslation("MovementSpeed"); + w.Refresh = () => { - var refPoint = SimToScreen(head != null ? head.SimPosition : collider.SimPosition); + var refPoint = cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition); w.DrawPos = refPoint + GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(CurrentAnimation.MovementSpeed * multiplier) * Cam.Zoom; }; w.MouseHeld += dTime => @@ -3909,7 +3850,7 @@ namespace Barotrauma.CharacterEditor { TryUpdateAnimParam("cyclespeed", character.AnimController.CurrentAnimationParams.MovementSpeed); } - w.tooltip = $"{GetCharacterEditorTranslation("MovementSpeed")}: {CurrentAnimation.MovementSpeed.FormatSingleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("MovementSpeed")}: {CurrentAnimation.MovementSpeed.FormatSingleDecimal()}"; }; // Additional check, which overrides the previous value (because evaluated last) w.PreUpdate += dTime => @@ -3924,14 +3865,14 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; w.PostDraw += (sp, dTime) => { if (w.IsSelected) { - GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(head != null ? head.SimPosition : collider.SimPosition), Color.Turquoise); + GUI.DrawLine(spriteBatch, w.DrawPos, Cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition), Color.Turquoise); } }; }).Draw(spriteBatch, deltaTime); @@ -3940,7 +3881,7 @@ namespace Barotrauma.CharacterEditor if (head != null) { // Head angle - DrawRadialWidget(spriteBatch, SimToScreen(head.SimPosition), animParams.HeadAngle, GetCharacterEditorTranslation("HeadAngle"), Color.White, + DrawRadialWidget(spriteBatch, Cam.WorldToScreen(head.DrawPosition), animParams.HeadAngle, GetCharacterEditorTranslation("HeadAngle"), Color.White, angle => TryUpdateAnimParam("headangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + head.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); // Head position and leaning Color color = GUIStyle.Red; @@ -3950,8 +3891,11 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("Head"); - w.refresh = () => w.DrawPos = SimToScreen(head.SimPosition.X + humanAnimController.HeadLeanAmount * character.AnimController.Dir, head.PullJointWorldAnchorB.Y); + w.Tooltip = GetCharacterEditorTranslation("Head"); + w.Refresh = () => w.DrawPos = Cam.WorldToScreen( + new Vector2( + head.DrawPosition.X + ConvertUnits.ToDisplayUnits(humanAnimController.HeadLeanAmount * character.AnimController.Dir), + ConvertUnits.ToDisplayUnits(head.PullJointWorldAnchorB.Y))); bool isHorizontal = false; bool isDirectionSet = false; w.MouseDown += () => isDirectionSet = false; @@ -3969,23 +3913,23 @@ namespace Barotrauma.CharacterEditor if (isHorizontal) { TryUpdateAnimParam("headleanamount", humanGroundedParams.HeadLeanAmount + scaledInput.X * character.AnimController.Dir); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y); } else { TryUpdateAnimParam("headposition", humanGroundedParams.HeadPosition - scaledInput.Y / RagdollParams.JointScale); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y); } } else { TryUpdateAnimParam("headleanamount", humanGroundedParams.HeadLeanAmount + scaledInput.X * character.AnimController.Dir); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y); TryUpdateAnimParam("headposition", humanGroundedParams.HeadPosition - scaledInput.Y / RagdollParams.JointScale); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y); } }; @@ -4012,7 +3956,7 @@ namespace Barotrauma.CharacterEditor } else if (w.IsSelected) { - GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(head.SimPosition), color); + GUI.DrawLine(spriteBatch, w.DrawPos, cam.WorldToScreen(head.DrawPosition), color); } }; }).Draw(spriteBatch, deltaTime); @@ -4021,11 +3965,11 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("HeadPosition"); - w.refresh = () => w.DrawPos = SimToScreen(head.SimPosition.X, head.PullJointWorldAnchorB.Y); + w.Tooltip = GetCharacterEditorTranslation("HeadPosition"); + w.Refresh = () => w.DrawPos = cam.WorldToScreen(new Vector2(head.DrawPosition.X, ConvertUnits.ToDisplayUnits(head.PullJointWorldAnchorB.Y))); w.MouseHeld += dTime => { - w.DrawPos = SimToScreen(head.SimPosition.X, head.PullJointWorldAnchorB.Y); + w.DrawPos = cam.WorldToScreen(new Vector2(head.DrawPosition.X, ConvertUnits.ToDisplayUnits(head.PullJointWorldAnchorB.Y))); var scaledInput = ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed) / Cam.Zoom / RagdollParams.JointScale; TryUpdateAnimParam("headposition", groundedParams.HeadPosition - scaledInput.Y); }; @@ -4042,14 +3986,14 @@ namespace Barotrauma.CharacterEditor } if (torso != null) { - referencePoint = torso.SimPosition; + referencePoint = torso.DrawPosition; if (animParams is HumanGroundedParams || animParams is HumanSwimParams) { var f = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation)); - referencePoint -= f * 0.25f; + referencePoint -= f * 25f; } // Torso angle - DrawRadialWidget(spriteBatch, SimToScreen(referencePoint), animParams.TorsoAngle, GetCharacterEditorTranslation("TorsoAngle"), Color.White, + DrawRadialWidget(spriteBatch, cam.WorldToScreen(referencePoint), animParams.TorsoAngle, GetCharacterEditorTranslation("TorsoAngle"), Color.White, angle => TryUpdateAnimParam("torsoangle", angle), rotationOffset: -collider.Rotation + torso.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); Color color = Color.DodgerBlue; if (animParams.IsGroundedAnimation) @@ -4059,8 +4003,10 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("Torso"); - w.refresh = () => w.DrawPos = SimToScreen(torso.SimPosition.X + humanAnimController.TorsoLeanAmount * character.AnimController.Dir, torso.PullJointWorldAnchorB.Y); + w.Tooltip = GetCharacterEditorTranslation("Torso"); + w.Refresh = () => w.DrawPos = cam.WorldToScreen( + new Vector2(torso.DrawPosition.X + ConvertUnits.ToDisplayUnits(humanAnimController.TorsoLeanAmount * character.AnimController.Dir), + ConvertUnits.ToDisplayUnits(torso.PullJointWorldAnchorB.Y))); bool isHorizontal = false; bool isDirectionSet = false; w.MouseDown += () => isDirectionSet = false; @@ -4078,23 +4024,23 @@ namespace Barotrauma.CharacterEditor if (isHorizontal) { TryUpdateAnimParam("torsoleanamount", humanGroundedParams.TorsoLeanAmount + scaledInput.X * character.AnimController.Dir); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y); } else { TryUpdateAnimParam("torsoposition", humanGroundedParams.TorsoPosition - scaledInput.Y / RagdollParams.JointScale); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y); } } else { TryUpdateAnimParam("torsoleanamount", humanGroundedParams.TorsoLeanAmount + scaledInput.X * character.AnimController.Dir); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y); TryUpdateAnimParam("torsoposition", humanGroundedParams.TorsoPosition - scaledInput.Y / RagdollParams.JointScale); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y); } }; @@ -4121,7 +4067,7 @@ namespace Barotrauma.CharacterEditor } else if (w.IsSelected) { - GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(torso.SimPosition), color); + GUI.DrawLine(spriteBatch, w.DrawPos, cam.WorldToScreen(torso.DrawPosition), color); } }; }).Draw(spriteBatch, deltaTime); @@ -4130,8 +4076,8 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("TorsoPosition"); - w.refresh = () => w.DrawPos = SimToScreen(torso.SimPosition.X, torso.PullJointWorldAnchorB.Y); + w.Tooltip = GetCharacterEditorTranslation("TorsoPosition"); + w.Refresh = () => w.DrawPos = SimToScreen(torso.SimPosition.X, torso.PullJointWorldAnchorB.Y); w.MouseHeld += dTime => { w.DrawPos = SimToScreen(torso.SimPosition.X, torso.PullJointWorldAnchorB.Y); @@ -4152,7 +4098,7 @@ namespace Barotrauma.CharacterEditor // Tail angle if (tail != null && fishParams != null) { - DrawRadialWidget(spriteBatch, SimToScreen(tail.SimPosition), fishParams.TailAngle, GetCharacterEditorTranslation("TailAngle"), Color.White, + DrawRadialWidget(spriteBatch, cam.WorldToScreen(tail.DrawPosition), fishParams.TailAngle, GetCharacterEditorTranslation("TailAngle"), Color.White, angle => TryUpdateAnimParam("tailangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + tail.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); } // Foot angle @@ -4171,7 +4117,7 @@ namespace Barotrauma.CharacterEditor } DrawRadialWidget(spriteBatch, - SimToScreen(new Vector2(limb.SimPosition.X, colliderBottom.Y)), + cam.WorldToScreen(new Vector2(limb.DrawPosition.X, ConvertUnits.ToDisplayUnits(colliderBottom.Y))), MathHelper.ToDegrees(fishParams.FootAnglesInRadians[limb.Params.ID]), GetCharacterEditorTranslation("FootAngle"), Color.White, angle => @@ -4184,7 +4130,7 @@ namespace Barotrauma.CharacterEditor } else if (humanParams != null) { - DrawRadialWidget(spriteBatch, SimToScreen(foot.SimPosition), humanParams.FootAngle, GetCharacterEditorTranslation("FootAngle"), Color.White, + DrawRadialWidget(spriteBatch, cam.WorldToScreen(foot.DrawPosition), humanParams.FootAngle, GetCharacterEditorTranslation("FootAngle"), Color.White, angle => TryUpdateAnimParam("footangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + foot.Params.GetSpriteOrientation() * dir, clockWise: dir > 0, wrapAnglePi: true); } // Grounded only @@ -4192,10 +4138,12 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("StepSize", Color.LimeGreen, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("StepSize"); - w.refresh = () => + w.Tooltip = GetCharacterEditorTranslation("StepSize"); + w.Refresh = () => { - var refPoint = SimToScreen(character.AnimController.GetColliderBottom()); + var refPoint = cam.WorldToScreen(new Vector2( + character.AnimController.Collider.DrawPosition.X, + character.AnimController.GetColliderBottom().Y)); var stepSize = ConvertUnits.ToDisplayUnits(character.AnimController.StepSize.Value); w.DrawPos = refPoint + new Vector2(stepSize.X * character.AnimController.Dir, -stepSize.Y) * Cam.Zoom; }; @@ -4204,7 +4152,7 @@ namespace Barotrauma.CharacterEditor w.DrawPos = PlayerInput.MousePosition; var transformedInput = ConvertUnits.ToSimUnits(new Vector2(PlayerInput.MouseSpeed.X * character.AnimController.Dir, -PlayerInput.MouseSpeed.Y)) / Cam.Zoom / RagdollParams.JointScale; TryUpdateAnimParam("stepsize", groundedParams.StepSize + transformedInput); - w.tooltip = $"{GetCharacterEditorTranslation("StepSize")}: {groundedParams.StepSize.FormatDoubleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("StepSize")}: {groundedParams.StepSize.FormatDoubleDecimal()}"; }; w.PostDraw += (sp, dTime) => { @@ -4223,11 +4171,11 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("HandMoveAmount", GUIStyle.Green, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("HandMoveAmount"); - float offset = 0.1f; - w.refresh = () => + w.Tooltip = GetCharacterEditorTranslation("HandMoveAmount"); + float offset = 10f; + w.Refresh = () => { - var refPoint = SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset); + var refPoint = cam.WorldToScreen(character.AnimController.Collider.DrawPosition + GetSimSpaceForward() * offset); var handMovement = ConvertUnits.ToDisplayUnits(humanGroundedParams.HandMoveAmount); w.DrawPos = refPoint + new Vector2(handMovement.X * character.AnimController.Dir, handMovement.Y) * Cam.Zoom; }; @@ -4236,13 +4184,13 @@ namespace Barotrauma.CharacterEditor w.DrawPos = PlayerInput.MousePosition; var transformedInput = ConvertUnits.ToSimUnits(new Vector2(PlayerInput.MouseSpeed.X * character.AnimController.Dir, PlayerInput.MouseSpeed.Y) / Cam.Zoom); TryUpdateAnimParam("handmoveamount", humanGroundedParams.HandMoveAmount + transformedInput); - w.tooltip = $"{GetCharacterEditorTranslation("HandMoveAmount")}: {humanGroundedParams.HandMoveAmount.FormatDoubleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("HandMoveAmount")}: {humanGroundedParams.HandMoveAmount.FormatDoubleDecimal()}"; }; w.PostDraw += (sp, dTime) => { if (w.IsSelected) { - GUI.DrawLine(sp, w.DrawPos, SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset), GUIStyle.Green); + GUI.DrawLine(sp, w.DrawPos, cam.WorldToScreen(character.AnimController.Collider.DrawPosition + GetSimSpaceForward() * offset), GUIStyle.Green); } }; }).Draw(spriteBatch, deltaTime); @@ -4256,15 +4204,15 @@ namespace Barotrauma.CharacterEditor int points = 1000; float GetAmplitude() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveAmplitude) * Cam.Zoom / amplitudeMultiplier; float GetWaveLength() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveLength) * Cam.Zoom / lengthMultiplier; - Vector2 GetRefPoint() => SimToScreen(collider.SimPosition) - GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(collider.Radius) * 3 * Cam.Zoom; + Vector2 GetRefPoint() => cam.WorldToScreen(collider.DrawPosition) - GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(collider.Radius) * 3 * Cam.Zoom; Vector2 GetDrawPos() => GetRefPoint() - GetScreenSpaceForward() * GetWaveLength(); Vector2 GetDir() => GetRefPoint() - GetDrawPos(); Vector2 GetStartPoint() => GetDrawPos() + GetDir() / 2; Vector2 GetControlPoint() => GetStartPoint() + GetScreenSpaceForward().Right() * character.AnimController.Dir * GetAmplitude(); - var lengthWidget = GetAnimationWidget("WaveLength", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var lengthWidget = GetAnimationWidget("WaveLength", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("TailMovementSpeed"); - w.refresh = () => w.DrawPos = GetDrawPos(); + w.Tooltip = GetCharacterEditorTranslation("TailMovementSpeed"); + w.Refresh = () => w.DrawPos = GetDrawPos(); w.MouseHeld += dTime => { float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward()).Combine() / Cam.Zoom * lengthMultiplier; @@ -4275,14 +4223,14 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; }); - var amplitudeWidget = GetAnimationWidget("WaveAmplitude", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var amplitudeWidget = GetAnimationWidget("WaveAmplitude", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("TailMovementAmount"); - w.refresh = () => w.DrawPos = GetControlPoint(); + w.Tooltip = GetCharacterEditorTranslation("TailMovementAmount"); + w.Refresh = () => w.DrawPos = GetControlPoint(); w.MouseHeld += dTime => { float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward().Right()).Combine() * character.AnimController.Dir / Cam.Zoom * amplitudeMultiplier; @@ -4293,7 +4241,7 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; }); @@ -4313,15 +4261,15 @@ namespace Barotrauma.CharacterEditor int points = 1000; float GetAmplitude() => ConvertUnits.ToDisplayUnits(humanSwimParams.LegMoveAmount) * Cam.Zoom / amplitudeMultiplier; float GetWaveLength() => ConvertUnits.ToDisplayUnits(humanSwimParams.LegCycleLength) * Cam.Zoom / lengthMultiplier; - Vector2 GetRefPoint() => SimToScreen(character.SimPosition - GetScreenSpaceForward() / 2); + Vector2 GetRefPoint() => cam.WorldToScreen(character.DrawPosition - GetScreenSpaceForward().FlipY() * 75); Vector2 GetDrawPos() => GetRefPoint() - GetScreenSpaceForward() * GetWaveLength(); Vector2 GetDir() => GetRefPoint() - GetDrawPos(); Vector2 GetStartPoint() => GetDrawPos() + GetDir() / 2; Vector2 GetControlPoint() => GetStartPoint() + GetScreenSpaceForward().Right() * character.AnimController.Dir * GetAmplitude(); - var lengthWidget = GetAnimationWidget("LegMovementSpeed", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var lengthWidget = GetAnimationWidget("LegMovementSpeed", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("LegMovementSpeed"); - w.refresh = () => w.DrawPos = GetDrawPos(); + w.Tooltip = GetCharacterEditorTranslation("LegMovementSpeed"); + w.Refresh = () => w.DrawPos = GetDrawPos(); w.MouseHeld += dTime => { float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward()).Combine() / Cam.Zoom * lengthMultiplier; @@ -4332,14 +4280,14 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; }); - var amplitudeWidget = GetAnimationWidget("LegMovementAmount", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var amplitudeWidget = GetAnimationWidget("LegMovementAmount", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("LegMovementAmount"); - w.refresh = () => w.DrawPos = GetControlPoint(); + w.Tooltip = GetCharacterEditorTranslation("LegMovementAmount"); + w.Refresh = () => w.DrawPos = GetControlPoint(); w.MouseHeld += dTime => { float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward().Right()).Combine() * character.AnimController.Dir / Cam.Zoom * amplitudeMultiplier; @@ -4350,7 +4298,7 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; }); @@ -4363,11 +4311,11 @@ namespace Barotrauma.CharacterEditor // Arms GetAnimationWidget("HandMoveAmount", GUIStyle.Green, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("HandMoveAmount"); - float offset = 0.4f; - w.refresh = () => + w.Tooltip = GetCharacterEditorTranslation("HandMoveAmount"); + float offset = 40f; + w.Refresh = () => { - var refPoint = SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset); + var refPoint = cam.WorldToScreen(collider.DrawPosition + GetSimSpaceForward() * offset); var handMovement = ConvertUnits.ToDisplayUnits(humanSwimParams.HandMoveAmount); w.DrawPos = refPoint + new Vector2(handMovement.X * character.AnimController.Dir, handMovement.Y) * Cam.Zoom; }; @@ -4378,13 +4326,13 @@ namespace Barotrauma.CharacterEditor Vector2 handMovement = humanSwimParams.HandMoveAmount + transformedInput; TryUpdateAnimParam("handmoveamount", handMovement); TryUpdateAnimParam("handcyclespeed", handMovement.X * 4); - w.tooltip = $"{GetCharacterEditorTranslation("HandMoveAmount")}: {humanSwimParams.HandMoveAmount.FormatDoubleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("HandMoveAmount")}: {humanSwimParams.HandMoveAmount.FormatDoubleDecimal()}"; }; w.PostDraw += (sp, dTime) => { if (w.IsSelected) { - GUI.DrawLine(sp, w.DrawPos, SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset), GUIStyle.Green); + GUI.DrawLine(sp, w.DrawPos, cam.WorldToScreen(collider.DrawPosition + GetSimSpaceForward() * offset), GUIStyle.Green); } }; }).Draw(spriteBatch, deltaTime); @@ -4407,7 +4355,7 @@ namespace Barotrauma.CharacterEditor { Vector2 size = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) * Cam.Zoom; Vector2 up = VectorExtensions.BackwardFlipped(limb.Rotation); - Vector2 limbScreenPos = SimToScreen(limb.SimPosition); + Vector2 limbScreenPos = cam.WorldToScreen(limb.DrawPosition); corners = MathUtils.GetImaginaryRect(corners, up, limbScreenPos, size); return corners; } @@ -4420,14 +4368,14 @@ namespace Barotrauma.CharacterEditor if (limb == null || limb.ActiveSprite == null) { continue; } var origin = limb.ActiveSprite.Origin; var sourceRect = limb.ActiveSprite.SourceRect; - Vector2 limbScreenPos = SimToScreen(limb.SimPosition); + Vector2 limbScreenPos = cam.WorldToScreen(limb.DrawPosition); bool isSelected = selectedLimbs.Contains(limb); corners = GetLimbPhysicRect(limb); if (isSelected && jointStartLimb != limb && jointEndLimb != limb) { GUI.DrawRectangle(spriteBatch, corners, Color.Yellow, thickness: 3); } - if (GUI.MouseOn == null && Widget.selectedWidgets.None() && !spriteSheetRect.Contains(PlayerInput.MousePosition) && MathUtils.RectangleContainsPoint(corners, PlayerInput.MousePosition)) + if (GUI.MouseOn == null && Widget.SelectedWidgets.None() && !spriteSheetRect.Contains(PlayerInput.MousePosition) && MathUtils.RectangleContainsPoint(corners, PlayerInput.MousePosition)) { if (isSelected) { @@ -4489,7 +4437,7 @@ namespace Barotrauma.CharacterEditor if (limb.type == LimbType.LeftFoot || limb.type == LimbType.RightFoot || limb.type == LimbType.LeftHand || limb.type == LimbType.RightHand) { var pullJointWidgetSize = new Vector2(5, 5); - Vector2 tformedPullPos = SimToScreen(limb.PullJointWorldAnchorA); + Vector2 tformedPullPos = SimToScreen(limb.PullJointWorldAnchorA) + limb.body.DrawPositionOffset; GUI.DrawRectangle(spriteBatch, tformedPullPos - pullJointWidgetSize / 2, pullJointWidgetSize, GUIStyle.Red, true); DrawWidget(spriteBatch, tformedPullPos, WidgetType.Rectangle, 8, Color.Cyan, $"IK ({limb.Name})", () => { @@ -4524,7 +4472,7 @@ namespace Barotrauma.CharacterEditor { continue; } - Vector2 limbScreenPos = SimToScreen(limb.SimPosition); + Vector2 limbScreenPos = cam.WorldToScreen(limb.DrawPosition); var f = Vector2.Transform(jointPos, Matrix.CreateRotationZ(limb.Rotation)); f.Y = -f.Y; Vector2 tformedJointPos = limbScreenPos + f * Cam.Zoom; @@ -4560,16 +4508,19 @@ namespace Barotrauma.CharacterEditor } DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: true, allowPairEditing: true, rotationOffset: rotation, holdPosition: true); } + Limb referenceLimb = altDown ? joint.LimbB : joint.LimbA; // Is the direction inversed incorrectly? - Vector2 to = tformedJointPos + VectorExtensions.ForwardFlipped(joint.LimbB.Rotation - joint.LimbB.Params.GetSpriteOrientation(), 20); - GUI.DrawLine(spriteBatch, tformedJointPos, to, Color.Magenta, width: 2); + Vector2 to = tformedJointPos - VectorExtensions.ForwardFlipped(referenceLimb.Rotation - referenceLimb.Params.GetSpriteOrientation(), 150); + GUI.DrawLine(spriteBatch, tformedJointPos, to, Color.LightGray * 0.7f, width: 2); var dotSize = new Vector2(5, 5); var rect = new Rectangle((tformedJointPos - dotSize / 2).ToPoint(), dotSize.ToPoint()); //GUI.DrawRectangle(spriteBatch, tformedJointPos - dotSize / 2, dotSize, color, true); //GUI.DrawLine(spriteBatch, tformedJointPos, tformedJointPos + up * 20, Color.White, width: 3); - GUI.DrawLine(spriteBatch, limbScreenPos, tformedJointPos, Color.Yellow, width: 3); + //GUI.DrawLine(spriteBatch, limbScreenPos, tformedJointPos, Color.Yellow * 0.5f, width: 3); //GUI.DrawRectangle(spriteBatch, inputRect, GUIStyle.Red); - GUI.DrawString(spriteBatch, tformedJointPos + new Vector2(dotSize.X, -dotSize.Y) * 2, $"{joint.Params.Name} {jointPos.FormatZeroDecimal()}", Color.White, Color.Black * 0.5f); + + string tooltip = $"{joint.Params.Name} {jointPos.FormatZeroDecimal()}"; + GUI.DrawString(spriteBatch, tformedJointPos - new Vector2(1.2f, 0.5f) * GUIStyle.Font.MeasureString(tooltip), tooltip, Color.White, Color.Black * 0.5f); if (PlayerInput.PrimaryMouseButtonHeld()) { if (!selectionWidget.IsControlled) { continue; } @@ -4843,10 +4794,10 @@ namespace Barotrauma.CharacterEditor Vector2 GetTopLeft() => sprite.SourceRect.Location.ToVector2(); Vector2 GetTopRight() => new Vector2(GetTopLeft().X + sprite.SourceRect.Width, GetTopLeft().Y); Vector2 GetBottomRight() => new Vector2(GetTopRight().X, GetTopRight().Y + sprite.SourceRect.Height); - var originWidget = GetLimbEditWidget($"{limb.Params.ID}_origin", limb, widgetSize, Widget.Shape.Cross, initMethod: w => + var originWidget = GetLimbEditWidget($"{limb.Params.ID}_origin", limb, widgetSize, WidgetShape.Cross, initMethod: w => { - w.refresh = () => w.tooltip = $"{GetCharacterEditorTranslation("Origin")}: {sprite.RelativeOrigin.FormatDoubleDecimal()}"; - w.refresh(); + w.Refresh = () => w.Tooltip = $"{GetCharacterEditorTranslation("Origin")}: {sprite.RelativeOrigin.FormatDoubleDecimal()}"; + w.Refresh(); w.MouseHeld += dTime => { var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(limb.ActiveSprite)); @@ -4883,16 +4834,16 @@ namespace Barotrauma.CharacterEditor var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(limb.ActiveSprite)); w.DrawPos = (spritePos + (sprite.Origin + sprite.SourceRect.Location.ToVector2()) * spriteSheetZoom) .Clamp(spritePos + GetTopLeft() * spriteSheetZoom, spritePos + GetBottomRight() * spriteSheetZoom); - w.refresh(); + w.Refresh(); }; }); originWidget.Draw(spriteBatch, deltaTime); if (!lockSpritePosition && (limb.type != LimbType.Head || !character.IsHuman)) { - var positionWidget = GetLimbEditWidget($"{limb.Params.ID}_position", limb, widgetSize, Widget.Shape.Rectangle, initMethod: w => + var positionWidget = GetLimbEditWidget($"{limb.Params.ID}_position", limb, widgetSize, WidgetShape.Rectangle, initMethod: w => { - w.refresh = () => w.tooltip = $"{GetCharacterEditorTranslation("Position")}: {limb.ActiveSprite.SourceRect.Location}"; - w.refresh(); + w.Refresh = () => w.Tooltip = $"{GetCharacterEditorTranslation("Position")}: {limb.ActiveSprite.SourceRect.Location}"; + w.Refresh(); w.MouseHeld += dTime => { w.DrawPos = PlayerInput.MousePosition; @@ -4924,7 +4875,7 @@ namespace Barotrauma.CharacterEditor }); }; }; - w.PreDraw += (sb, dTime) => w.refresh(); + w.PreDraw += (sb, dTime) => w.Refresh(); }); if (!positionWidget.IsControlled) { @@ -4934,10 +4885,10 @@ namespace Barotrauma.CharacterEditor } if (!lockSpriteSize && (limb.type != LimbType.Head || !character.IsHuman)) { - var sizeWidget = GetLimbEditWidget($"{limb.Params.ID}_size", limb, widgetSize, Widget.Shape.Rectangle, initMethod: w => + var sizeWidget = GetLimbEditWidget($"{limb.Params.ID}_size", limb, widgetSize, WidgetShape.Rectangle, initMethod: w => { - w.refresh = () => w.tooltip = $"{GetCharacterEditorTranslation("Size")}: {limb.ActiveSprite.SourceRect.Size}"; - w.refresh(); + w.Refresh = () => w.Tooltip = $"{GetCharacterEditorTranslation("Size")}: {limb.ActiveSprite.SourceRect.Size}"; + w.Refresh(); w.MouseHeld += dTime => { w.DrawPos = PlayerInput.MousePosition; @@ -4979,7 +4930,7 @@ namespace Barotrauma.CharacterEditor }); }; }; - w.PreDraw += (sb, dTime) => w.refresh(); + w.PreDraw += (sb, dTime) => w.Refresh(); }); if (!sizeWidget.IsControlled) { @@ -4988,7 +4939,7 @@ namespace Barotrauma.CharacterEditor sizeWidget.Draw(spriteBatch, deltaTime); } } - else if (isMouseOn && GUI.MouseOn == null && Widget.selectedWidgets.None()) + else if (isMouseOn && GUI.MouseOn == null && Widget.SelectedWidgets.None()) { // TODO: only one limb name should be displayed (needs to be done in a separate loop) GUI.DrawString(spriteBatch, limbScreenPos + new Vector2(10, -10), limb.Name, Color.White, Color.Black * 0.5f); @@ -4997,7 +4948,7 @@ namespace Barotrauma.CharacterEditor else { GUI.DrawRectangle(spriteBatch, rect, isMouseOn ? Color.White : Color.Gray); - if (isMouseOn && GUI.MouseOn == null && Widget.selectedWidgets.None()) + if (isMouseOn && GUI.MouseOn == null && Widget.SelectedWidgets.None()) { // TODO: only one limb name should be displayed (needs to be done in a separate loop) GUI.DrawString(spriteBatch, limbScreenPos + new Vector2(10, -10), limb.Name, Color.White, Color.Black * 0.5f); @@ -5089,7 +5040,7 @@ namespace Barotrauma.CharacterEditor bool isHovered = jointSelectionWidget.IsSelected || otherWidget.IsSelected; if (isSelected || isHovered) { - GUI.DrawLine(spriteBatch, jointSelectionWidget.DrawPos, otherWidget.DrawPos, jointSelectionWidget.color, width: 2); + GUI.DrawLine(spriteBatch, jointSelectionWidget.DrawPos, otherWidget.DrawPos, jointSelectionWidget.Color, width: 2); } } if (selectedJoints.Contains(joint)) @@ -5369,14 +5320,14 @@ namespace Barotrauma.CharacterEditor GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: color, font: GUIStyle.SmallFont); } onClick(angle); - var zeroPos = drawPos + VectorExtensions.Forward(rotationOffset - MathHelper.PiOver2, circleRadius); - GUI.DrawLine(spriteBatch, drawPos, zeroPos, GUIStyle.Red, width: 3); }, autoFreeze, holdPosition, onHovered: () => { if (!PlayerInput.PrimaryMouseButtonHeld()) { - GUI.DrawString(spriteBatch, new Vector2(drawPos.X + 5, drawPos.Y - widgetSize / 2), - $"{toolTip} ({angle.FormatZeroDecimal()})", color, Color.Black * 0.5f); + GUIComponent.DrawToolTip( + spriteBatch, + $"{toolTip} ({angle.FormatZeroDecimal()})", + new Vector2(drawPos.X + 50, drawPos.Y - widgetSize / 2 - 50)); } }); } @@ -5388,7 +5339,7 @@ namespace Barotrauma.CharacterEditor var inputRect = drawRect; inputRect.Inflate(size * 0.75f, size * 0.75f); bool isMouseOn = inputRect.Contains(PlayerInput.MousePosition); - bool isSelected = isMouseOn && GUI.MouseOn == null && Widget.selectedWidgets.None(); + bool isSelected = isMouseOn && GUI.MouseOn == null && Widget.SelectedWidgets.None(); switch (widgetType) { case WidgetType.Rectangle: @@ -5420,7 +5371,7 @@ namespace Barotrauma.CharacterEditor // Label/tooltip if (onHovered == null) { - GUI.DrawString(spriteBatch, new Vector2(drawRect.Right + 5, drawRect.Y - drawRect.Height / 2), toolTip, color, Color.Black); + GUIComponent.DrawToolTip(spriteBatch, toolTip, new Vector2(drawRect.Right + 5, drawRect.Y - drawRect.Height / 2)); } else { @@ -5457,7 +5408,7 @@ namespace Barotrauma.CharacterEditor private Dictionary jointSelectionWidgets = new Dictionary(); private Dictionary limbEditWidgets = new Dictionary(); - private Widget GetAnimationWidget(string name, Color innerColor, Color? outerColor = null, int size = 10, float sizeMultiplier = 2, Widget.Shape shape = Widget.Shape.Rectangle, Action initMethod = null) + private Widget GetAnimationWidget(string name, Color innerColor, Color? outerColor = null, int size = 10, float sizeMultiplier = 2, WidgetShape shape = WidgetShape.Rectangle, Action initMethod = null) { string id = $"{character.SpeciesName}_{character.AnimController.CurrentAnimationParams.AnimationType.ToString()}_{name}"; if (!animationWidgets.TryGetValue(id, out Widget widget)) @@ -5465,32 +5416,32 @@ namespace Barotrauma.CharacterEditor int selectedSize = (int)Math.Round(size * sizeMultiplier); widget = new Widget(id, size, shape) { - tooltipOffset = new Vector2(selectedSize / 2 + 5, -10), - data = character.AnimController.CurrentAnimationParams + TooltipOffset = new Vector2(selectedSize / 2 + 5, -10), + Data = character.AnimController.CurrentAnimationParams }; widget.MouseUp += () => CurrentAnimation.StoreSnapshot(); - widget.color = innerColor; - widget.secondaryColor = outerColor; + widget.Color = innerColor; + widget.SecondaryColor = outerColor; widget.PreUpdate += dTime => { widget.Enabled = editAnimations; if (widget.Enabled) { - AnimationParams data = widget.data as AnimationParams; + AnimationParams data = widget.Data as AnimationParams; widget.Enabled = data.AnimationType == character.AnimController.CurrentAnimationParams.AnimationType; } }; widget.PostUpdate += dTime => { - widget.inputAreaMargin = widget.IsControlled ? 1000 : 0; - widget.size = widget.IsSelected ? selectedSize : size; - widget.isFilled = widget.IsControlled; + widget.InputAreaMargin = widget.IsControlled ? 1000 : 0; + widget.Size = widget.IsSelected ? selectedSize : size; + widget.IsFilled = widget.IsControlled; }; widget.PreDraw += (sp, dTime) => { if (!widget.IsControlled) { - widget.refresh(); + widget.Refresh(); } }; animationWidgets.Add(id, widget); @@ -5511,8 +5462,8 @@ namespace Barotrauma.CharacterEditor { linkedWidget = CreateJointSelectionWidget(linkedId, joint); } - jointWidget.linkedWidget = linkedWidget; - linkedWidget.linkedWidget = jointWidget; + jointWidget.LinkedWidget = linkedWidget; + linkedWidget.LinkedWidget = jointWidget; } } return jointWidget; @@ -5522,21 +5473,18 @@ namespace Barotrauma.CharacterEditor { int normalSize = 10; int selectedSize = 20; - var widget = new Widget(ID, normalSize, Widget.Shape.Circle) + var widget = new Widget(ID, normalSize, WidgetShape.Circle); + widget.Refresh = () => { - tooltipOffset = new Vector2(selectedSize / 2 + 5, -10) + widget.ShowTooltip = !selectedJoints.Contains(joint); + widget.Color = selectedJoints.Contains(joint) ? Color.Yellow : GUIStyle.Red; }; - widget.refresh = () => - { - widget.showTooltip = !selectedJoints.Contains(joint); - widget.color = selectedJoints.Contains(joint) ? Color.Yellow : GUIStyle.Red; - }; - widget.refresh(); + widget.Refresh(); widget.PreUpdate += dTime => widget.Enabled = editJoints; widget.PostUpdate += dTime => { - widget.inputAreaMargin = widget.IsControlled ? 1000 : 0; - widget.size = widget.IsSelected ? selectedSize : normalSize; + widget.InputAreaMargin = widget.IsControlled ? 1000 : 0; + widget.Size = widget.IsSelected ? selectedSize : normalSize; }; widget.MouseDown += () => { @@ -5555,8 +5503,8 @@ namespace Barotrauma.CharacterEditor } foreach (var w in jointSelectionWidgets.Values) { - w.refresh(); - w.linkedWidget?.refresh(); + w.Refresh(); + w.LinkedWidget?.Refresh(); } ResetParamsEditor(); }; @@ -5567,13 +5515,14 @@ namespace Barotrauma.CharacterEditor RagdollParams.StoreSnapshot(); } }; - widget.tooltip = joint.Params.Name; + widget.Tooltip = joint.Params.Name; + widget.TooltipOffset = new Vector2(-GUIStyle.Font.MeasureString(widget.Tooltip).X - 30, -10); jointSelectionWidgets.Add(ID, widget); return widget; } } - private Widget GetLimbEditWidget(string ID, Limb limb, int size = 5, Widget.Shape shape = Widget.Shape.Rectangle, Action < Widget> initMethod = null) + private Widget GetLimbEditWidget(string ID, Limb limb, int size = 5, WidgetShape shape = WidgetShape.Rectangle, Action < Widget> initMethod = null) { if (!limbEditWidgets.TryGetValue(ID, out Widget widget)) { @@ -5588,18 +5537,18 @@ namespace Barotrauma.CharacterEditor int selectedSize = (int)Math.Round(size * 1.5f); var w = new Widget(ID, size, shape) { - tooltipOffset = new Vector2(selectedSize / 2 + 5, -10), - data = limb, - color = Color.Yellow, - secondaryColor = Color.Gray, - textColor = Color.Yellow + TooltipOffset = new Vector2(selectedSize / 2 + 5, -10), + Data = limb, + Color = Color.Yellow, + SecondaryColor = Color.Gray, + TextColor = Color.Yellow }; w.PreUpdate += dTime => w.Enabled = editLimbs && selectedLimbs.Contains(limb); w.PostUpdate += dTime => { - w.inputAreaMargin = w.IsControlled ? 1000 : 0; - w.size = w.IsSelected ? selectedSize : normalSize; - w.isFilled = w.IsControlled; + w.InputAreaMargin = w.IsControlled ? 1000 : 0; + w.Size = w.IsSelected ? selectedSize : normalSize; + w.IsFilled = w.IsControlled; }; w.MouseUp += () => RagdollParams.StoreSnapshot(); initMethod?.Invoke(w); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 64a6b65c9..5fb552402 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -13,7 +13,7 @@ namespace Barotrauma.CharacterEditor // Ragdoll data private Identifier name; private bool isHumanoid; - private bool canEnterSubmarine = true; + private CanEnterSubmarine canEnterSubmarine = CanEnterSubmarine.True; private bool canWalk; private string texturePath; private string xmlPath; @@ -153,6 +153,7 @@ namespace Barotrauma.CharacterEditor }; var topGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.99f, 1), frame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 2 }; var fields = new List(); + GUITextBox nameField = null; GUITextBox texturePathElement = null; GUITextBox xmlPathElement = null; GUIDropDown contentPackageDropDown = null; @@ -177,7 +178,7 @@ namespace Barotrauma.CharacterEditor { case 0: new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), TextManager.Get("Name")); - var nameField = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), Name.Value ?? GetCharacterEditorTranslation("DefaultName").Value) { CaretColor = Color.White }; + nameField = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), Name.Value ?? GetCharacterEditorTranslation("DefaultName").Value) { CaretColor = Color.White }; string ProcessText(string text) => text.RemoveWhitespace().CapitaliseFirstInvariant(); Name = ProcessText(nameField.Text).ToIdentifier(); nameField.OnTextChanged += (tb, text) => @@ -204,9 +205,14 @@ namespace Barotrauma.CharacterEditor var l = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), GetCharacterEditorTranslation("CanEnterSubmarines")); var t = new GUITickBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), string.Empty) { - Selected = CanEnterSubmarine, + //TODO: allow ternary selection (true, false, partial) + Selected = CanEnterSubmarine == CanEnterSubmarine.True, Enabled = !IsCopy, - OnSelected = (tB) => CanEnterSubmarine = tB.Selected + OnSelected = (tB) => + { + CanEnterSubmarine = tB.Selected ? CanEnterSubmarine.True : CanEnterSubmarine.False; + return true; + } }; if (!t.Enabled) { @@ -409,6 +415,12 @@ namespace Barotrauma.CharacterEditor return false; } + if (Name.Value.IsNullOrWhiteSpace()) + { + nameField?.Flash(useRectangleFlash: true); + return false; + } + string evaluatedTexturePath = ContentPath.FromRaw( contentPackageDropDown.SelectedData as ContentPackage, TexturePath).Value; @@ -905,7 +917,7 @@ namespace Barotrauma.CharacterEditor get => Instance.isHumanoid; set => Instance.isHumanoid = value; } - public bool CanEnterSubmarine + public CanEnterSubmarine CanEnterSubmarine { get => Instance.canEnterSubmarine; set => Instance.canEnterSubmarine = value; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs index 6e102be08..e0d94c1c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs @@ -440,13 +440,13 @@ namespace Barotrauma { widget.MouseDown += () => { - widget.color = GUIStyle.Green; + widget.Color = GUIStyle.Green; prevAngle = Rotation; disableMove = true; }; widget.Deselected += () => { - widget.color = Color.Yellow; + widget.Color = Color.Yellow; disableMove = false; }; widget.MouseHeld += (deltaTime) => @@ -484,7 +484,7 @@ namespace Barotrauma }; widget.PreDraw += (sprtBtch, deltaTime) => { - widget.tooltip = $"Scale: {Math.Round(Scale, 2)}\n" + + widget.Tooltip = $"Scale: {Math.Round(Scale, 2)}\n" + $"Rotation: {(int) MathHelper.ToDegrees(Rotation)}"; float rotation = Rotation - (float) Math.PI / 2f; widget.DrawPos = Position + new Vector2((float) Math.Cos(rotation), (float) Math.Sin(rotation)) * (Scale * widgetSize); @@ -519,17 +519,17 @@ namespace Barotrauma { if (!widgets.TryGetValue(id, out Widget? widget)) { - widget = new Widget(id, size, Widget.Shape.Rectangle) + widget = new Widget(id, size, WidgetShape.Rectangle) { - color = Color.Yellow, + Color = Color.Yellow, RequireMouseOn = false }; widgets.Add(id, widget); initMethod?.Invoke(widget); } - widget.size = size; - widget.thickness = thickness; + widget.Size = size; + widget.Thickness = thickness; return widget; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index e8806ca32..138112575 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -457,7 +457,14 @@ namespace Barotrauma } else { - connection.OverrideValue = ChangeType(attribute.Value, connection.ValueType); + try + { + connection.OverrideValue = ChangeType(attribute.Value, connection.ValueType); + } + catch + { + DebugConsole.ThrowError($"Failed to convert the value {attribute.Value} of the attribute {attribute.Name} to {connection.ValueType}."); + } } } } @@ -813,7 +820,6 @@ namespace Barotrauma Vector2 size = type == typeof(string) ? new Vector2(0.2f, 0.3f) : new Vector2(0.2f, 0.175f); var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.Edit"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("OK") }, size, minSize: new Point(300, 175)); - Vector2 layoutSize = type == typeof(string) ? new Vector2(1f, 0.5f) : new Vector2(1f, 0.25f); var layout = new GUILayoutGroup(new RectTransform(layoutSize, msgBox.Content.RectTransform), isHorizontal: true); @@ -838,7 +844,7 @@ namespace Barotrauma valueInput.OnTextChanged += (component, o) => { Vector2 textSize = valueInput.Font.MeasureString(valueInput.WrappedText); - valueInput.RectTransform.NonScaledSize = new Point(valueInput.RectTransform.NonScaledSize.X, (int) textSize.Y + 10); + valueInput.RectTransform.NonScaledSize = new Point(valueInput.RectTransform.NonScaledSize.X, (int)textSize.Y + 10); listBox.UpdateScrollBarSize(); listBox.BarScroll = 1.0f; newValue = o; @@ -855,14 +861,31 @@ namespace Barotrauma return true; }; } - else if (type == typeof(float) || type == typeof(int)) + else if (type == typeof(float)) { - GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float) { FloatValue = (float) (newValue ?? 0.0f) }; + GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float); + if (newValue is float floatVal) + { + valueInput.FloatValue = floatVal; + } valueInput.OnValueChanged += component => { newValue = component.FloatValue; }; } + else if (type == typeof(int)) + { + GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Int); + if (newValue is int intVal) + { + valueInput.IntValue = intVal; + } + valueInput.OnValueChanged += component => { newValue = component.IntValue; }; + } else if (type == typeof(bool)) { - GUITickBox valueInput = new GUITickBox(new RectTransform(Vector2.One, layout.RectTransform), "Value") { Selected = (bool) (newValue ?? false) }; + GUITickBox valueInput = new GUITickBox(new RectTransform(Vector2.One, layout.RectTransform), "Value"); + if (newValue is bool val) + { + valueInput.Selected = val; + } valueInput.OnSelected += component => { newValue = component.Selected; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index bc85fcb99..a85d4790c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -518,6 +518,8 @@ namespace Barotrauma } GraphicsQuad.Render(); + Character.DrawSpeechBubbles(spriteBatch, cam); + if (fadeToBlackState > 0.0f) { spriteBatch.Begin(SpriteSortMode.Deferred); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index e0a8da297..7606eefc9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -35,6 +35,8 @@ namespace Barotrauma private readonly GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; private readonly GUIDropDown selectedSubDropDown; + private readonly GUIDropDown selectedBeaconStationDropdown; + private readonly GUIDropDown selectedWreckDropdown; private Sprite editingSprite; @@ -195,6 +197,28 @@ namespace Barotrauma } subDropDownContainer.RectTransform.MinSize = new Point(0, selectedSubDropDown.RectTransform.MinSize.Y); + var beaconStationDropDownContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), beaconStationDropDownContainer.RectTransform), TextManager.Get("submarinetype.beaconstation")); + selectedBeaconStationDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), beaconStationDropDownContainer.RectTransform)); + selectedBeaconStationDropdown.AddItem(TextManager.Get("Any"), userData: null); + foreach (SubmarineInfo beaconStation in SubmarineInfo.SavedSubmarines) + { + if (beaconStation.Type != SubmarineType.BeaconStation) { continue; } + selectedBeaconStationDropdown.AddItem(beaconStation.DisplayName, userData: beaconStation); + } + beaconStationDropDownContainer.RectTransform.MinSize = new Point(0, selectedBeaconStationDropdown.RectTransform.MinSize.Y); + + var wreckDropDownContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), wreckDropDownContainer.RectTransform), TextManager.Get("submarinetype.wreck")); + selectedWreckDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), wreckDropDownContainer.RectTransform)); + selectedWreckDropdown.AddItem(TextManager.Get("Any"), userData: null); + foreach (SubmarineInfo wreck in SubmarineInfo.SavedSubmarines) + { + if (wreck.Type != SubmarineType.Wreck) { continue; } + selectedWreckDropdown.AddItem(wreck.DisplayName, userData: wreck); + } + wreckDropDownContainer.RectTransform.MinSize = new Point(0, selectedWreckDropdown.RectTransform.MinSize.Y); + mirrorLevel = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), TextManager.Get("mirrorentityx")); allowInvalidOutpost = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.025f), paddedRightPanel.RectTransform), @@ -218,6 +242,8 @@ namespace Barotrauma GameMain.LightManager.ClearLights(); currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; + currentLevelData.ForceBeaconStation = selectedBeaconStationDropdown.SelectedData as SubmarineInfo; + currentLevelData.ForceWreck = selectedWreckDropdown.SelectedData as SubmarineInfo; currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); @@ -269,7 +295,7 @@ namespace Barotrauma var nonPlayerFiles = ContentPackageManager.EnabledPackages.All.SelectMany(p => p .GetFiles() - .Where(f => !(f is SubmarineFile))).ToArray(); + .Where(f => f is not SubmarineFile)).ToArray(); SubmarineInfo subInfo = selectedSubDropDown.SelectedData as SubmarineInfo; subInfo ??= SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && @@ -339,6 +365,9 @@ namespace Barotrauma currentLevelData = LevelData.CreateRandom(ToolBox.RandomSeed(10), generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; + currentLevelData.ForceBeaconStation = selectedBeaconStationDropdown.SelectedData as SubmarineInfo; + currentLevelData.ForceWreck = selectedWreckDropdown.SelectedData as SubmarineInfo; + currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); DebugConsole.NewMessage("*****************************************************************************"); @@ -967,7 +996,7 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - if (item == null) { continue; } + if (item == null || item.HiddenInGame) { continue; } foreach (var light in item.GetComponents()) { light.Update((float)deltaTime, Cam); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs index 76d16400e..5a62f17fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs @@ -152,7 +152,9 @@ namespace Barotrauma { SetMenuTabPositioning(); CreateHostServerFields(); + bool prevMenuOpen = GUI.SettingsMenuOpen; SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); + GUI.SettingsMenuOpen = prevMenuOpen; if (remoteContentDoc?.Root != null) { remoteContentContainer.ClearChildren(); @@ -1269,7 +1271,7 @@ namespace Barotrauma selectedSub = new SubmarineInfo(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub")); GameMain.GameSession = new GameSession(selectedSub, savePath, GameModePreset.SinglePlayerCampaign, settings, mapSeed); - GameMain.GameSession.CrewManager.CharacterInfos.Clear(); + GameMain.GameSession.CrewManager.ClearCharacterInfos(); foreach (var characterInfo in campaignSetupUI.CharacterMenus.Select(m => m.CharacterInfo)) { GameMain.GameSession.CrewManager.AddCharacterInfo(characterInfo); @@ -1366,7 +1368,7 @@ namespace Barotrauma float bannerAspectRatio = (float) playstyleBanner.Sprite.SourceRect.Width / playstyleBanner.Sprite.SourceRect.Height; playstyleBanner.RectTransform.NonScaledSize = new Point(playstyleBanner.Rect.Width, (int)(playstyleBanner.Rect.Width / bannerAspectRatio)); playstyleBanner.RectTransform.IsFixedSize = true; - new GUIFrame(new RectTransform(Vector2.One, playstyleBanner.RectTransform), "InnerGlow", color: Color.Black); + new GUIFrame(new RectTransform(playstyleBanner.Rect.Size + new Point(1), playstyleBanner.RectTransform, Anchor.Center), "InnerGlow", color: Color.Black); new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.05f), playstyleBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.03f) }, "playstyle name goes here", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index b6169da32..167275754 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -53,7 +53,9 @@ namespace Barotrauma base.Select(); DeletePrevDownloads(); Reset(); - + + bool allowDownloads = GameMain.Client.ClientPeer is { AllowModDownloads: true }; + Frame.ClearChildren(); var mainVisibleFrame = new GUIFrame(new RectTransform((0.6f, 0.8f), Frame.RectTransform, Anchor.Center)); @@ -66,7 +68,7 @@ namespace Barotrauma "", font: GUIStyle.LargeFont, textAlignment: Alignment.CenterLeft) { - TextGetter = () => GameMain.NetLobbyScreen.ServerName.Text + TextGetter = () => GameMain.Client.ServerName }; mainLayoutSpacing(); var downloadList = new GUIListBox(new RectTransform((1.0f, 0.76f), mainLayout.RectTransform)); @@ -163,7 +165,7 @@ namespace Barotrauma var msgBoxModList = new GUIListBox(new RectTransform(Vector2.One, innerLayout.RectTransform)); innerLayoutSpacing(0.05f); - var footer = textBlock(TextManager.Get("ModDownloadFooter"), GUIStyle.Font, Alignment.Center); + var footer = textBlock(TextManager.Get(allowDownloads ? "ModDownloadFooter" : "ModDownloadFooterFail"), GUIStyle.Font, Alignment.Center); innerLayoutSpacing(0.05f); GUILayoutGroup buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), innerLayout.RectTransform), isHorizontal: true); @@ -182,15 +184,28 @@ namespace Barotrauma } }; - buttonContainerSpacing(0.1f); - button(TextManager.Get("Yes"), () => confirmDownload = true); - buttonContainerSpacing(0.2f); - button(TextManager.Get("No"), () => + if (allowDownloads) { - GameMain.Client?.Quit(); - GameMain.MainMenuScreen.Select(); - }); - buttonContainerSpacing(0.1f); + buttonContainerSpacing(0.1f); + button(TextManager.Get("Yes"), () => confirmDownload = true); + buttonContainerSpacing(0.2f); + button(TextManager.Get("No"), () => + { + GameMain.Client?.Quit(); + GameMain.MainMenuScreen.Select(); + }); + buttonContainerSpacing(0.1f); + } + else + { + buttonContainerSpacing(0.15f); + button(TextManager.Get("Cancel"), () => + { + GameMain.Client?.Quit(); + GameMain.MainMenuScreen.Select(); + }, width: 0.7f); + buttonContainerSpacing(0.15f); + } var missingIds = missingPackages .Where(p => p.IsMandatory) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index af8541d80..14bd2d8a1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; @@ -12,18 +13,16 @@ namespace Barotrauma { partial class NetLobbyScreen : Screen { - private readonly GUIFrame infoFrame, modeFrame; - private readonly GUILayoutGroup infoFrameContent; - private readonly GUIFrame myCharacterFrame; - - private readonly GUIListBox chatBox; - private readonly GUIButton serverLogReverseButton; - private readonly GUIListBox serverLogBox, serverLogFilterTicks; + private GUIListBox chatBox; + private GUIButton serverLogReverseButton; + private GUIListBox serverLogBox, serverLogFilterTicks; private GUIComponent jobVariantTooltip; - private readonly GUITextBox chatInput; - private readonly GUITextBox serverLogFilter; + private GUIComponent playStyleIconContainer; + + private GUITextBox chatInput; + private GUITextBox serverLogFilter; public GUITextBox ChatInput { get @@ -32,63 +31,52 @@ namespace Barotrauma } } - private readonly GUIImage micIcon; + private GUIImage micIcon; - private readonly GUIScrollBar levelDifficultyScrollBar; + private GUIScrollBar levelDifficultySlider; private readonly List traitorElements = new List(); - private readonly GUIScrollBar traitorProbabilitySlider; - private readonly GUITextBlock traitorProbabilityText; - private readonly GUILayoutGroup traitorDangerGroup; + private GUIScrollBar traitorProbabilitySlider; + private GUILayoutGroup traitorDangerGroup; - private readonly GUIButton[] botCountButtons; - private readonly GUITextBlock botCountText; + public GUIFrame MissionTypeFrame { get; private set; } + public GUIFrame CampaignSetupFrame { get; private set; } + public GUIFrame CampaignFrame { get; private set; } - private readonly GUIButton[] botSpawnModeButtons; - private readonly GUITextBlock botSpawnModeText; + public GUIButton QuitCampaignButton { get; private set; } - public readonly GUIFrame MissionTypeFrame; - public readonly GUIFrame CampaignSetupFrame; - public readonly GUIFrame CampaignFrame; - public readonly GUIButton ContinueCampaignButton, QuitCampaignButton; + private GUITickBox[] missionTypeTickBoxes; + private GUIListBox missionTypeList; - private readonly GUITickBox[] missionTypeTickBoxes; - private readonly GUIListBox missionTypeList; + public GUITextBox LevelSeedBox { get; private set; } - public GUITextBox SeedBox - { - get; private set; - } + private GUIButton joinOnGoingRoundButton; + /// + /// Contains the elements that control starting the round (start button, spectate button, "ready to start" tickbox) + /// + private GUILayoutGroup roundControlsHolder; - private readonly GUIComponent gameModeContainer; - private readonly GUIButton spectateButton; - private readonly GUILayoutGroup roundControlsHolder; + public GUIButton SettingsButton { get; private set; } + public static GUIButton JobInfoFrame { get; set; } - public readonly GUIButton SettingsButton; - public static GUIButton JobInfoFrame; - - private readonly GUITickBox spectateBox; + private GUITickBox spectateBox; public bool Spectating => spectateBox is { Selected: true, Visible: true }; - private readonly GUIFrame playerInfoContainer; - - private GUILayoutGroup infoContainer; + private GUILayoutGroup playerInfoContent; private GUIComponent changesPendingText; private bool createPendingChangesText = true; - public GUIButton PlayerFrame; + public GUIButton PlayerFrame { get; private set; } - public readonly GUIButton SubVisibilityButton; + public GUIButton SubVisibilityButton { get; private set; } - private readonly GUITextBox subSearchBox; + private GUITextBox subSearchBox; - private readonly GUIComponent subPreviewContainer; + private GUIComponent subPreviewContainer; - private readonly GUITickBox autoRestartBox; - private readonly GUITextBlock autoRestartText; + private GUITickBox autoRestartBox; + private GUITextBlock autoRestartText; - private readonly GUITickBox shuttleTickBox; - - private readonly GUIComponent settingsBlocker; + private GUITickBox shuttleTickBox; private Sprite backgroundSprite; @@ -98,11 +86,14 @@ namespace Barotrauma private GUIFrame characterInfoFrame; private GUIFrame appearanceFrame; - public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu; - public GUIFrame JobSelectionFrame; + private readonly List respawnSettingsElements = new List(); + private readonly List campaignDisabledElements = new List(); - public GUIFrame JobPreferenceContainer; - public GUIListBox JobList; + public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu { get; set; } + public GUIFrame JobSelectionFrame { get; private set; } + + public GUIFrame JobPreferenceContainer { get; private set; } + public GUIListBox JobList { get; private set; } private Identifier micIconStyle; private float micCheckTimer; @@ -119,52 +110,39 @@ namespace Barotrauma set; } - //elements that can only be used by the host + /// + /// Elements that can only be used by the host or people with server settings management permissions (but are visible to everyone) + /// private readonly List clientDisabledElements = new List(); - //elements that can't be interacted with but don't look disabled - private readonly List clientReadonlyElements = new List(); - //elements that aren't shown client-side + + /// + /// Elements that are only visible to the host or people with server settings management permissions + /// private readonly List clientHiddenElements = new List(); + private readonly List botSettingsElements = new List(); + + private readonly Dictionary settingAssignedComponents = new Dictionary(); + public GUIComponent FileTransferFrame { get; private set; } public GUITextBlock FileTransferTitle { get; private set; } public GUIProgressBar FileTransferProgressBar { get; private set; } public GUITextBlock FileTransferProgressText { get; private set; } - public GUITextBox ServerName - { - get; - private set; - } + public GUITickBox Favorite { get; private set; } - public GUITickBox Favorite - { - get; - private set; - } + public GUILayoutGroup LogButtons { get; private set; } - public GUITextBox ServerMessage - { - get; - private set; - } + /// + /// Tab buttons above the chat panel (chat and server log tabs) + /// + private readonly List chatPanelTabButtons = new List(); - public GUILayoutGroup LogButtons - { - get; - private set; - } + private GUITextBlock publicOrPrivateText, playstyleText; - private readonly GUIButton showChatButton; - private readonly GUIButton showLogButton; - - private readonly GUITextBlock publicOrPrivate; - - public readonly GUIListBox SubList; - - public readonly GUIDropDown ShuttleList; - - public readonly GUIListBox ModeList; + public GUIListBox SubList { get; private set; } + public GUIDropDown ShuttleList { get; private set; } + public GUIListBox ModeList { get; private set; } private int selectedModeIndex; public int SelectedModeIndex @@ -189,35 +167,20 @@ namespace Barotrauma } } + //No, this should not be static even though your IDE might say so! There's a server-side version of this which needs to be an instance method. public IReadOnlyList GetSubList() => (IReadOnlyList)GameMain.Client?.ServerSubmarines ?? Array.Empty(); - public readonly GUIListBox PlayerList; + public GUIListBox PlayerList; - public GUITextBox CharacterNameBox - { - get; - private set; - } + public GUITextBox CharacterNameBox { get; private set; } - public GUIListBox TeamPreferenceListBox - { - get; - private set; - } + public GUIListBox TeamPreferenceListBox { get; private set; } - public GUIButton StartButton - { - get; - private set; - } + public GUIButton StartButton { get; private set; } - public GUITickBox ReadyToStartBox - { - get; - private set; - } + public GUITickBox ReadyToStartBox { get; private set; } public SubmarineInfo SelectedSub => SubList.SelectedData as SubmarineInfo; @@ -282,7 +245,7 @@ namespace Barotrauma List jobPreferences = new List(); foreach (GUIComponent child in JobList.Content.Children) { - if (!(child.UserData is JobVariant jobPrefab)) { continue; } + if (child.UserData is not JobVariant jobPrefab) { continue; } jobPreferences.Add(jobPrefab); } return jobPreferences; @@ -297,130 +260,962 @@ namespace Barotrauma } set { - if (levelSeed == value) return; + if (levelSeed == value) { return; } levelSeed = value; int intSeed = ToolBox.StringToInt(levelSeed); backgroundSprite = LocationType.Random(new MTRandom(intSeed), predicate: lt => lt.UsePortraitInRandomLoadingScreens)?.GetPortrait(intSeed); - SeedBox.Text = levelSeed; + LevelSeedBox.Text = levelSeed; } } + private const float MainPanelWidth = 0.7f; + private const float SidePanelWidth = 0.3f; + /// + /// Spacing between different elements in the panels + /// + private const float PanelSpacing = 0.005f; + + /// + /// Size of the outer border of the panels (= empty area round the contents of the panel) + /// + private static int PanelBorderSize => GUI.IntScale(20); + + private static Point GetSizeWithoutBorder(GUIComponent parent) => new Point(parent.Rect.Width - PanelBorderSize * 2, parent.Rect.Height - PanelBorderSize * 2); + public NetLobbyScreen() { - float panelSpacing = 0.005f; - var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), Frame.RectTransform, Anchor.Center) { MaxSize = new Point(int.MaxValue, GameMain.GraphicsHeight - 50) }, isHorizontal: false) + var contentArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), Frame.RectTransform, Anchor.Center), isHorizontal: false) { Stretch = true, - RelativeSpacing = panelSpacing + RelativeSpacing = PanelSpacing }; - var panelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), innerFrame.RectTransform, Anchor.Center), isHorizontal: true) + var horizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, contentArea.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, - RelativeSpacing = panelSpacing + RelativeSpacing = PanelSpacing }; - GUILayoutGroup panelHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), panelContainer.RectTransform)) + var mainPanel = new GUIFrame(new RectTransform(new Vector2(MainPanelWidth, 1.0f), horizontalLayout.RectTransform)); + + var mainPanelLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width, mainPanel.Rect.Height - PanelBorderSize), mainPanel.RectTransform, Anchor.TopCenter), childAnchor: Anchor.TopCenter) { Stretch = true, - RelativeSpacing = panelSpacing + //more spacing to more clearly separate the top and bottom + RelativeSpacing = PanelSpacing * 4 }; - GUILayoutGroup bottomBar = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), innerFrame.RectTransform), childAnchor: Anchor.CenterLeft) - { - Stretch = true, - IsHorizontal = true, - RelativeSpacing = panelSpacing - }; - GUILayoutGroup bottomBarLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) - { - Stretch = true, - IsHorizontal = true, - RelativeSpacing = panelSpacing - }; - GUILayoutGroup bottomBarMid = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) - { - Stretch = true, - IsHorizontal = true, - RelativeSpacing = panelSpacing - }; - GUILayoutGroup bottomBarRight = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) - { - Stretch = true, - IsHorizontal = true, - RelativeSpacing = panelSpacing - }; - - //server info panel ------------------------------------------------------------ - - infoFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), panelHolder.RectTransform)); - infoFrameContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), infoFrame.RectTransform, Anchor.Center)) + GUILayoutGroup serverInfoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), mainPanelLayout.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.025f }; + CreateServerInfoContents(serverInfoHolder); - //server game panel ------------------------------------------------------------ - - modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), panelHolder.RectTransform)) - { - CanBeFocused = false - }; - - gameModeContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), modeFrame.RectTransform, Anchor.Center)) - { - RelativeSpacing = panelSpacing * 4.0f, - Stretch = true - }; - - var disconnectButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), bottomBarLeft.RectTransform), TextManager.Get("disconnect")) - { - OnClicked = (bt, userdata) => { GameMain.QuitToMainMenu(save: false, showVerificationPrompt: true); return true; } - }; - disconnectButton.TextBlock.AutoScaleHorizontal = true; - - // file transfers ------------------------------------------------------------ - FileTransferFrame = new GUIFrame(new RectTransform(Vector2.One, bottomBarLeft.RectTransform), style: "TextFrame"); - var fileTransferContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), FileTransferFrame.RectTransform, Anchor.Center)) + var mainPanelTopLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width - PanelBorderSize * 2, mainPanel.Rect.Height / 2), mainPanelLayout.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, - RelativeSpacing = 0.05f + RelativeSpacing = PanelSpacing }; - FileTransferTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), "", font: GUIStyle.SmallFont); - var fileTransferBottom = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + + var mainPanelBottomLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width - PanelBorderSize * 2, mainPanel.Rect.Height / 2), mainPanelLayout.RectTransform, Anchor.Center), isHorizontal: true) { + Stretch = true, + RelativeSpacing = PanelSpacing + }; + + //-------------------------------------------------------------------------------------------------------------------------------- + //top panel (game mode, submarine) + //-------------------------------------------------------------------------------------------------------------------------------- + + CreateGameModeDropdown(mainPanelTopLayout); + CreateSubmarineListPanel(mainPanelTopLayout); + CreateSubmarineInfoPanel(mainPanelTopLayout); + + //-------------------------------------------------------------------------------------------------------------------------------- + //bottom panel (settings) + //-------------------------------------------------------------------------------------------------------------------------------- + + CreateGameModePanel(mainPanelBottomLayout); + CreateGameModeSettingsPanel(mainPanelBottomLayout); + CreateGeneralSettingsPanel(mainPanelBottomLayout); + mainPanelBottomLayout.Recalculate(); + + foreach (var child in mainPanelBottomLayout.GetAllChildren()) + { + if (traitorDangerGroup.Children.Contains(child)) + { + //don't touch the colors of the traitor danger indicators, they're intentionally very dim when disabled + continue; + } + //make the disabled colors slightly less dim (these should be readable, despite being non-interactable) + child.DisabledColor = new Color(child.Color, child.Color.A / 255.0f * 0.8f); + if (child is GUITextBlock textBlock) + { + textBlock.DisabledTextColor = new Color(textBlock.TextColor, textBlock.TextColor.A / 255.0f * 0.8f); + } + } + + //-------------------------------------------------------------------------------------------------------------------------------- + //right panel (Character customization/Chat) + //-------------------------------------------------------------------------------------------------------------------------------- + + var sidePanel = new GUIFrame(new RectTransform(new Vector2(SidePanelWidth, 1.0f), horizontalLayout.RectTransform)); + GUILayoutGroup sidePanelLayout = new GUILayoutGroup(new RectTransform(GetSizeWithoutBorder(sidePanel), + sidePanel.RectTransform, Anchor.Center)) + { + RelativeSpacing = PanelSpacing * 4, Stretch = true }; - FileTransferProgressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), fileTransferBottom.RectTransform), 0.0f, Color.DarkGreen); - FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "", - font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); - new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), fileTransferBottom.RectTransform), TextManager.Get("cancel"), style: "GUIButtonSmall") + + CreateSidePanelContents(sidePanelLayout); + + //-------------------------------------------------------------------------------------------------------------------------------- + // bottom panel (start round, quit, transfers, ready to start...) ------------------------------------------------------------ + //-------------------------------------------------------------------------------------------------------------------------------- + GUILayoutGroup bottomBar = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), contentArea.RectTransform), childAnchor: Anchor.CenterLeft) { - OnClicked = (btn, userdata) => + Stretch = true, + IsHorizontal = true, + RelativeSpacing = PanelSpacing + }; + CreateBottomPanelContents(bottomBar); + } + + private void AssignComponentToServerSetting(GUIComponent component, string settingName) + { + settingAssignedComponents[component] = settingName; + } + + public void AssignComponentsToServerSettings() + { + settingAssignedComponents.ForEach(kvp => GameMain.Client.ServerSettings.AssignGUIComponent(kvp.Value, kvp.Key)); + } + + private void CreateServerInfoContents(GUIComponent parent) + { + GUIFrame serverInfoFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); + var serverBanner = new GUICustomComponent(new RectTransform(Vector2.One, serverInfoFrame.RectTransform), DrawServerBanner) + { + HideElementsOutsideFrame = true, + IgnoreLayoutGroups = true + }; + + GUIFrame serverInfoContent = new GUIFrame(new RectTransform(new Vector2(0.98f, 0.9f), serverInfoFrame.RectTransform, Anchor.Center), style: null); + + var serverLabelContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.05f), serverInfoContent.RectTransform), isHorizontal: true) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + + playstyleText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), serverLabelContainer.RectTransform), + "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); + publicOrPrivateText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), serverLabelContainer.RectTransform), + "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); + + var serverNameShadow = new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.3f), serverInfoContent.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(GUI.IntScale(3)) }, + string.Empty, font: GUIStyle.LargeFont, textColor: Color.Black) + { + IgnoreLayoutGroups = true + }; + var serverName = new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.3f), serverInfoContent.RectTransform, Anchor.CenterLeft), + string.Empty, font: GUIStyle.LargeFont, textColor: GUIStyle.TextColorBright) + { + IgnoreLayoutGroups = true, + TextGetter = serverNameShadow.TextGetter = () => GameMain.Client?.ServerName + }; + + playStyleIconContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.4f), serverInfoContent.RectTransform, Anchor.BottomRight), isHorizontal: true, childAnchor: Anchor.BottomRight) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + + Favorite = new GUITickBox(new RectTransform(new Vector2(0.5f, 0.5f), serverInfoContent.RectTransform, Anchor.TopRight, scaleBasis: ScaleBasis.BothHeight), + "", null, "GUIServerListFavoriteTickBox") + { + IgnoreLayoutGroups = true, + Selected = false, + ToolTip = TextManager.Get("addtofavorites"), + OnSelected = (tickbox) => { - if (!(FileTransferFrame.UserData is FileReceiver.FileTransferIn transfer)) { return false; } - GameMain.Client?.CancelFileTransfer(transfer); - GameMain.Client?.FileReceiver.StopTransfer(transfer); + if (GameMain.Client == null) { return true; } + ServerInfo info = GameMain.Client.CreateServerInfoFromSettings(); + if (tickbox.Selected) + { + GameMain.ServerListScreen.AddToFavoriteServers(info); + } + else + { + GameMain.ServerListScreen.RemoveFromFavoriteServers(info); + } + tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites"); return true; } }; - // Sidebar area (Character customization/Chat) + SettingsButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), serverInfoContent.RectTransform, Anchor.TopRight), + TextManager.Get("ServerSettingsButton"), style: "GUIButtonSmall"); + } - GUILayoutGroup sideBar = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), panelContainer.RectTransform, maxSize: new Point(650, panelContainer.RectTransform.Rect.Height))) + public void RefreshPlaystyleIcons() + { + playStyleIconContainer?.ClearChildren(); + if (GameMain.Client?.ClientPeer?.ServerConnection is not { } serverConnection || serverConnection.Endpoint == null) { return; } + var serverInfo = ServerInfo.FromServerEndpoints(serverConnection.Endpoint.ToEnumerable().ToImmutableArray(), GameMain.Client.ServerSettings); + + var playStyleTags = serverInfo.GetPlayStyleTags(); + foreach (var tag in playStyleTags) + { + var playStyleIcon = GUIStyle.GetComponentStyle($"PlayStyleIcon.{tag}") + ?.GetSprite(GUIComponent.ComponentState.None); + if (playStyleIcon is null) { continue; } + + new GUIImage(new RectTransform(Vector2.One, playStyleIconContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + playStyleIcon, scaleToFit: true) + { + ToolTip = TextManager.Get($"servertagdescription.{tag}"), + Color = Color.White + }; + } + } + + private void CreateGameModeDropdown(GUIComponent parent) + { + //------------------------------------------------------------------------------------------------------------------ + // Gamemode panel + //------------------------------------------------------------------------------------------------------------------ + + GUILayoutGroup gameModeHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.005f + }; + + var modeLabel = CreateSubHeader("GameMode", gameModeHolder); + var voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), modeLabel.RectTransform, Anchor.TopRight), + TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) + { + UserData = "modevotes", + Visible = false + }; + ModeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform)) + { + PlaySoundOnSelect = true, + OnSelected = VotableClicked + }; + + foreach (GameModePreset mode in GameModePreset.List) + { + if (mode.IsSinglePlayer) { continue; } + + var modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), ModeList.Content.RectTransform), style: null) + { + UserData = mode + }; + + var modeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.76f, 0.9f), modeFrame.RectTransform, Anchor.CenterRight)) + { + AbsoluteSpacing = GUI.IntScale(5), + Stretch = true + }; + + var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont); + modeTitle.RectTransform.NonScaledSize = new Point(int.MaxValue, (int)modeTitle.TextSize.Y); + modeTitle.RectTransform.IsFixedSize = true; + var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true); + //leave some padding for the vote count text + modeDescription.Padding = new Vector4(modeDescription.Padding.X, modeDescription.Padding.Y, GUI.IntScale(30), modeDescription.Padding.W); + modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent; + modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor; + modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f; + modeFrame.OnAddedToGUIUpdateList = (c) => + { + modeTitle.State = modeDescription.State = c.State; + }; + modeDescription.RectTransform.SizeChanged += () => + { + modeDescription.RectTransform.NonScaledSize = new Point(modeDescription.Rect.Width, (int)modeDescription.TextSize.Y); + modeFrame.RectTransform.MinSize = new Point(0, (int)(modeContent.Children.Sum(c => c.Rect.Height + modeContent.AbsoluteSpacing) / modeContent.RectTransform.RelativeSize.Y)); + }; + + new GUIImage(new RectTransform(new Vector2(0.2f, 0.8f), modeFrame.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f) }, + style: "GameModeIcon." + mode.Identifier, scaleToFit: true); + } + } + + private void CreateSubmarineListPanel(GUIComponent parent) + { + var submarineListHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.005f + }; + + var subLabel = CreateSubHeader("Submarine", submarineListHolder); + SubVisibilityButton + = new GUIButton( + new RectTransform(Vector2.One * 1.2f, subLabel.RectTransform, anchor: Anchor.CenterRight, + scaleBasis: ScaleBasis.BothHeight) + { AbsoluteOffset = new Point(0, GUI.IntScale(5)) }, + style: "EyeButton") + { + OnClicked = (button, o) => + { + CreateSubmarineVisibilityMenu(); + return false; + } + }; + clientHiddenElements.Add(SubVisibilityButton); + + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), submarineListHolder.RectTransform), isHorizontal: true) + { + Stretch = true + }; + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUIStyle.Font); + subSearchBox = new GUITextBox(new RectTransform(Vector2.One, filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = subSearchBox.RectTransform.MinSize; + subSearchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + subSearchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + subSearchBox.OnTextChanged += (textBox, text) => + { + UpdateSubVisibility(); + return true; + }; + + SubList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.93f), submarineListHolder.RectTransform)) + { + PlaySoundOnSelect = true, + OnSelected = VotableClicked + }; + + var voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), subLabel.RectTransform, Anchor.TopRight), + TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) + { + UserData = "subvotes", + Visible = false, + CanBeFocused = false + }; + } + + private void CreateSubmarineInfoPanel(GUIComponent parent) + { + var submarineInfoHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.005f + }; + //submarine preview ------------------------------------------------------------------ + + subPreviewContainer = new GUIFrame(new RectTransform(Vector2.One, submarineInfoHolder.RectTransform), style: null); + subPreviewContainer.RectTransform.SizeChanged += () => + { + if (SelectedSub != null) { CreateSubPreview(SelectedSub); } + }; + } + + private GUIComponent CreateGameModePanel(GUIComponent parent) + { + var gameModeSpecificFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); + CampaignSetupFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) + { + Visible = false + }; + CampaignFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) + { + Visible = false + }; + GUILayoutGroup campaignContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.5f), CampaignFrame.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), + TextManager.Get("gamemode.multiplayercampaign"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); + + QuitCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), + TextManager.Get("quitbutton"), textAlignment: Alignment.Center) + { + OnClicked = (_, __) => + { + if (GameMain.Client == null) { return false; } + if (GameMain.Client.GameStarted) + { + GameMain.Client.RequestRoundEnd(save: false); + } + else + { + GameMain.Client.RequestRoundEnd(save: false, quitCampaign: true); + } + return true; + } + }; + + //mission type ------------------------------------------------------------------ + MissionTypeFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null); + + GUILayoutGroup missionHolder = new GUILayoutGroup(new RectTransform(Vector2.One, MissionTypeFrame.RectTransform)) { - RelativeSpacing = panelSpacing, Stretch = true }; + CreateSubHeader("MissionType", missionHolder); + missionTypeList = new GUIListBox(new RectTransform(Vector2.One, missionHolder.RectTransform)) + { + OnSelected = (component, obj) => + { + return false; + } + }; + clientDisabledElements.Add(missionTypeList); + + var missionTypes = (MissionType[])Enum.GetValues(typeof(MissionType)); + missionTypeTickBoxes = new GUITickBox[missionTypes.Length - 2]; + int index = 0; + for (int i = 0; i < missionTypes.Length; i++) + { + var missionType = missionTypes[i]; + if (missionType == MissionType.None || missionType == MissionType.All) { continue; } + + GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), missionTypeList.Content.RectTransform) { MinSize = new Point(0, GUI.IntScale(30)) }, style: null) + { + UserData = missionType, + }; + + if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) + { + missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), string.Empty) + { + UserData = (int)missionType, + Visible = false, + CanBeFocused = false + }; + } + else + { + missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), + TextManager.Get("MissionType." + missionType.ToString())) + { + UserData = (int)missionType, + ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString()), + OnSelected = (tickbox) => + { + int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; + int missionTypeAnd = (int)MissionType.All & (!tickbox.Selected ? (~(int)tickbox.UserData) : (int)MissionType.All); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); + return true; + } + }; + frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; + } + index++; + } + clientDisabledElements.AddRange(missionTypeTickBoxes); + + return gameModeSpecificFrame; + } + + private GUIComponent CreateGameModeSettingsPanel(GUIComponent parent) + { + //------------------------------------------------------------------ + // settings panel + //------------------------------------------------------------------ + + GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) + { + Stretch = true + }; + CreateSubHeader("GameModeSettings", settingsLayout); + + var settingsContent = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)).Content; + + //seed ------------------------------------------------------------------ + + var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), TextManager.Get("LevelSeed")) + { + CanBeFocused = false + }; + LevelSeedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight)); + LevelSeedBox.OnDeselected += (textBox, key) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); + }; + campaignDisabledElements.Add(LevelSeedBox); + campaignDisabledElements.Add(seedLabel); + clientDisabledElements.Add(LevelSeedBox); + clientDisabledElements.Add(seedLabel); + LevelSeed = ToolBox.RandomSeed(8); + + //level difficulty ------------------------------------------------------------------ + + var levelDifficultyHolder = CreateLabeledSlider(settingsContent, "LevelDifficulty", "", "LevelDifficultyExplanation", out levelDifficultySlider, out var difficultySliderLabel, + step: 0.01f, range: new Vector2(0.0f, 100.0f)); + levelDifficultySlider.OnReleased = (scrollbar, value) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + levelDifficultySlider.OnMoved = (scrollbar, value) => + { + if (!EventManagerSettings.Prefabs.Any()) { return true; } + difficultySliderLabel.Text = + EventManagerSettings.GetByDifficultyPercentile(value).Name + + $" ({TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue)).ToString())})"; + difficultySliderLabel.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + return true; + }; + AssignComponentToServerSetting(levelDifficultySlider, nameof(ServerSettings.SelectedLevelDifficulty)); + campaignDisabledElements.AddRange(levelDifficultyHolder.GetAllChildren()); + clientDisabledElements.AddRange(levelDifficultyHolder.GetAllChildren()); + + //bot count ------------------------------------------------------------------ + CreateSubHeader("BotSettings", settingsContent); + + var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botCountSettingHolder.RectTransform), TextManager.Get("BotCount"), wrap: true); + var botCountSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), botCountSettingHolder.RectTransform)); + for (int i = 0; i <= NetConfig.MaxPlayers; i++) + { + botCountSelection.AddElement(i, i.ToString()); + } + AssignComponentToServerSetting(botCountSelection, nameof(ServerSettings.BotCount)); + clientDisabledElements.AddRange(botCountSettingHolder.GetAllChildren()); + botSettingsElements.Add(botCountSelection); + + var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botSpawnModeSettingHolder.RectTransform), TextManager.Get("BotSpawnMode"), wrap: true); + var botSpawnModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeSettingHolder.RectTransform)); + foreach (var botSpawnMode in Enum.GetValues(typeof(BotSpawnMode)).Cast()) + { + botSpawnModeSelection.AddElement(botSpawnMode, botSpawnMode.ToString(), TextManager.Get($"botspawnmode.{botSpawnMode}.tooltip")); + } + botSpawnModeSelection.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + AssignComponentToServerSetting(botSpawnModeSelection, nameof(ServerSettings.BotSpawnMode)); + clientDisabledElements.AddRange(botSpawnModeSettingHolder.GetAllChildren()); + botSettingsElements.Add(botSpawnModeSelection); + + botCountSelection.OnValueChanged += (_) => + { + botSpawnModeSelection.Enabled = GameMain.Client.ServerSettings.BotCount > 0; + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + }; + + //traitor probability ------------------------------------------------------------------ + + CreateSubHeader("TraitorSettings", settingsContent); + + //spacing + new GUIFrame(new RectTransform(new Point(1, GUI.IntScale(5)), settingsContent.RectTransform), style: null); + + //the probability slider is a traitor element, but we don't add it to traitorElements + //because we don't want to disable it when sliding it to 0 (need to be able to slide it back!) + var traitorProbabilityHolder = CreateLabeledSlider(settingsContent, "traitor.probability", "", "traitor.probability.tooltip", + out traitorProbabilitySlider, out var traitorProbabilityText, + step: 0.01f, range: new Vector2(0.0f, 1.0f)); + traitorProbabilitySlider.OnMoved = (scrollbar, value) => + { + traitorProbabilityText.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue * 100)).ToString()); + traitorProbabilityText.TextColor = + value <= 0.0f ? + GUIStyle.Green : + ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red); + RefreshEnabledElements(); + return true; + }; + traitorProbabilitySlider.OnMoved(traitorProbabilitySlider, traitorProbabilitySlider.BarScroll); + traitorProbabilitySlider.OnReleased += (scrollbar, value) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; + AssignComponentToServerSetting(traitorProbabilitySlider, nameof(ServerSettings.TraitorProbability)); + traitorElements.Clear(); + clientDisabledElements.AddRange(traitorProbabilityHolder.GetAllChildren()); + + var traitorDangerHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + var dangerLevelLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerHolder.RectTransform), TextManager.Get("traitor.dangerlevelsetting"), wrap: true) + { + ToolTip = TextManager.Get("traitor.dangerlevelsetting.tooltip") + }; + + var traitorDangerContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), traitorDangerHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; + var traitorDangerButtons = new GUIButton[2]; + traitorDangerButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleLeft") + { + OnClicked = (button, obj) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: -1); + return true; + } + }; + + traitorDangerGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + AbsoluteSpacing = 1 + }; + for (int i = TraitorEventPrefab.MinDangerLevel; i <= TraitorEventPrefab.MaxDangerLevel; i++) + { + var difficultyColor = Mission.GetDifficultyColor(i); + new GUIImage(new RectTransform(new Vector2(0.75f), traitorDangerGroup.RectTransform), "DifficultyIndicator", scaleToFit: true) + { + ToolTip = + RichString.Rich( + $"‖color:{Color.White.ToStringHex()}‖{TextManager.Get($"traitor.dangerlevel.{i}")}‖color:end‖" + '\n' + + TextManager.Get($"traitor.dangerlevel.{i}.description")), + Color = difficultyColor, + DisabledColor = Color.Gray * 0.5f, + }; + } + + traitorDangerButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleRight") + { + OnClicked = (button, obj) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: 1); + return true; + } + }; + + traitorDangerContainer.InheritTotalChildrenMinHeight(); + SetTraitorDangerIndicators(GameMain.Client?.ServerSettings.TraitorDangerLevel ?? TraitorEventPrefab.MinDangerLevel); + traitorElements.Add(dangerLevelLabel); + traitorElements.AddRange(traitorDangerGroup.Children); + traitorElements.AddRange(traitorDangerButtons); + + var traitorsMinPlayerCountHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), traitorsMinPlayerCountHolder.RectTransform), TextManager.Get("ServerSettingsTraitorsMinPlayerCount"), wrap: true) + { + ToolTip = TextManager.Get("ServerSettingsTraitorsMinPlayerCountToolTip") + }; + var traitorsMinPlayerCount = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), traitorsMinPlayerCountHolder.RectTransform)); + for (int i = 1; i <= NetConfig.MaxPlayers; i++) + { + traitorsMinPlayerCount.AddElement(i, i.ToString()); + } + traitorsMinPlayerCount.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + AssignComponentToServerSetting(traitorsMinPlayerCount, nameof(ServerSettings.TraitorsMinPlayerCount)); + traitorElements.AddRange(traitorsMinPlayerCountHolder.Children); + + foreach (var traitorElement in traitorElements) + { + if (!clientDisabledElements.Contains(traitorElement)) + { + clientDisabledElements.Add(traitorElement); + } + } + + return settingsContent; + } + + private GUIComponent CreateGeneralSettingsPanel(GUIComponent parent) + { + //------------------------------------------------------------------ + // settings panel + //------------------------------------------------------------------ + + GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) + { + Stretch = true + }; + var respawnSettingsHeader = CreateSubHeader("RespawnSettings", settingsLayout); + + var settingsContent = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)).Content; + + // ------------------------------------------------------------------ + + var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), settingsContent.RectTransform) { AbsoluteOffset = new Point((int)respawnSettingsHeader.Padding.X, 0) }, + TextManager.Get("ServerSettingsAllowRespawning")) + { + ToolTip = TextManager.Get("RespawnExplanation"), + OnSelected = (tickbox) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + RefreshEnabledElements(); + return true; + } + }; + AssignComponentToServerSetting(respawnBox, nameof(ServerSettings.AllowRespawn)); + clientDisabledElements.Add(respawnBox); + + GUILayoutGroup shuttleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), settingsContent.RectTransform), isHorizontal: true) + { + Stretch = true + }; + + shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle")) + { + ToolTip = TextManager.Get("RespawnShuttleExplanation"), + Selected = true, + OnSelected = (GUITickBox box) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + } + }; + AssignComponentToServerSetting(shuttleTickBox, nameof(ServerSettings.UseRespawnShuttle)); + respawnSettingsElements.Add(shuttleTickBox); + + shuttleTickBox.TextBlock.RectTransform.SizeChanged += () => + { + shuttleTickBox.TextBlock.AutoScaleHorizontal = true; + shuttleTickBox.TextBlock.TextScale = 1.0f; + if (shuttleTickBox.TextBlock.TextScale < 0.75f) + { + shuttleTickBox.TextBlock.Wrap = true; + shuttleTickBox.TextBlock.AutoScaleHorizontal = true; + shuttleTickBox.TextBlock.TextScale = 1.0f; + } + }; + ShuttleList = new GUIDropDown(new RectTransform(Vector2.One, shuttleHolder.RectTransform), elementCount: 10) + { + OnSelected = (component, obj) => + { + GameMain.Client?.RequestSelectSub(obj as SubmarineInfo, isShuttle: true); + return true; + } + }; + ShuttleList.ListBox.RectTransform.MinSize = new Point(250, 0); + shuttleHolder.RectTransform.MinSize = new Point(0, ShuttleList.RectTransform.Children.Max(c => c.MinSize.Y)); + respawnSettingsElements.Add(ShuttleList); + + var respawnIntervalElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnInterval", "", "", out var respawnIntervalSlider, out var respawnIntervalSliderLabel, + range: new Vector2(10.0f, 600.0f)); + LocalizedString intervalLabel = respawnIntervalSliderLabel.Text; + respawnIntervalSlider.StepValue = 10.0f; + respawnIntervalSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = intervalLabel + " " + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); + return true; + }; + respawnIntervalSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + respawnIntervalSlider.OnMoved(respawnIntervalSlider, respawnIntervalSlider.BarScroll); + respawnSettingsElements.AddRange(respawnIntervalElement.GetAllChildren()); + AssignComponentToServerSetting(respawnIntervalSlider, nameof(ServerSettings.RespawnInterval)); + + var minRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsMinRespawn", "", "ServerSettingsMinRespawnToolTip", out var minRespawnSlider, out var minRespawnSliderLabel, + step: 0.1f, range: new Vector2(0.0f, 1.0f)); + minRespawnSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = ToolBox.GetFormattedPercentage(scrollBar.BarScrollValue); + return true; + }; + minRespawnSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + minRespawnSlider.OnMoved(minRespawnSlider, minRespawnSlider.BarScroll); + respawnSettingsElements.AddRange(minRespawnElement.GetAllChildren()); + AssignComponentToServerSetting(minRespawnSlider, nameof(ServerSettings.MinRespawnRatio)); + + var respawnDurationElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnDuration", "", "ServerSettingsRespawnDurationTooltip", out var respawnDurationSlider, out var respawnDurationSliderLabel, + step: 0.1f, range: new Vector2(60.0f, 660.0f)); + respawnDurationSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = scrollBar.BarScrollValue <= 0 ? TextManager.Get("Unlimited") : ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); + return true; + }; + respawnDurationSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + respawnDurationSlider.ScrollToValue = (GUIScrollBar scrollBar, float barScroll) => + { + return barScroll >= 1.0f ? 0.0f : barScroll * (scrollBar.Range.Y - scrollBar.Range.X) + scrollBar.Range.X; + }; + respawnDurationSlider.ValueToScroll = (GUIScrollBar scrollBar, float value) => + { + return value <= 0.0f ? 1.0f : (value - scrollBar.Range.X) / (scrollBar.Range.Y - scrollBar.Range.X); + }; + respawnDurationSlider.OnMoved(respawnDurationSlider, respawnDurationSlider.BarScroll); + respawnSettingsElements.AddRange(respawnDurationElement.GetAllChildren()); + AssignComponentToServerSetting(respawnDurationSlider, nameof(ServerSettings.MaxTransportTime)); + + var skillLossElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnDeath", "", "ServerSettingsSkillLossPercentageOnDeathToolTip", + out var skillLossSlider, out var skillLossSliderLabel, range: new Vector2(0, 100)); + skillLossSlider.StepValue = 1; + skillLossSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString()); + return true; + }; + skillLossSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + respawnSettingsElements.AddRange(skillLossElement.GetAllChildren()); + AssignComponentToServerSetting(skillLossSlider, nameof(ServerSettings.SkillLossPercentageOnDeath)); + skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll); + + var skillLossImmediateRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnImmediateRespawn", "", "ServerSettingsSkillLossPercentageOnImmediateRespawnToolTip", + out var skillLossImmediateRespawnSlider, out var skillLossImmediateRespawnSliderLabel, range: new Vector2(0, 100)); + skillLossImmediateRespawnSlider.StepValue = 1; + skillLossImmediateRespawnSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString()); + return true; + }; + skillLossImmediateRespawnSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + respawnSettingsElements.AddRange(skillLossImmediateRespawnElement.GetAllChildren()); + AssignComponentToServerSetting(skillLossImmediateRespawnSlider, nameof(ServerSettings.SkillLossPercentageOnImmediateRespawn)); + skillLossImmediateRespawnSlider.OnMoved(skillLossImmediateRespawnSlider, skillLossImmediateRespawnSlider.BarScroll); + + foreach (var respawnElement in respawnSettingsElements) + { + if (!clientDisabledElements.Contains(respawnElement)) + { + clientDisabledElements.Add(respawnElement); + } + } + + return settingsContent; + } + + public static GUITextBlock CreateSubHeader(string textTag, GUIComponent parent, string toolTipTag = null) + { + var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), parent.RectTransform) { MinSize = new Point(0, GUI.IntScale(28)) }, + TextManager.Get(textTag), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft, textColor: GUIStyle.TextColorBright) + { + CanBeFocused = false + }; + if (!toolTipTag.IsNullOrEmpty()) + { + header.ToolTip = TextManager.Get(toolTipTag); + } + return header; + } + + public static GUIComponent CreateLabeledSlider(GUIComponent parent, string headerTag, string valueLabelTag, string tooltipTag, out GUIScrollBar slider, out GUITextBlock label, float? step = null, Vector2? range = null) + { + GUILayoutGroup verticalLayout = null; + if (!headerTag.IsNullOrEmpty()) + { + verticalLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), isHorizontal: false) + { + Stretch = true + }; + var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), verticalLayout.RectTransform), + TextManager.Get(headerTag), textAlignment: Alignment.CenterLeft) + { + CanBeFocused = false + }; + header.RectTransform.MinSize = new Point(0, (int)header.TextSize.Y); + } + + var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, headerTag == null ? 0.0f : 0.5f), (verticalLayout ?? parent).RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + //spacing + new GUIFrame(new RectTransform(new Point(GUI.IntScale(5), 0), container.RectTransform), style: null); + + slider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), barSize: 0.1f, style: "GUISlider"); + if (step.HasValue) { slider.Step = step.Value; } + if (range.HasValue) { slider.Range = range.Value; } + + container.RectTransform.MinSize = new Point(0, slider.RectTransform.MinSize.Y); + container.RectTransform.MaxSize = new Point(int.MaxValue, slider.RectTransform.MaxSize.Y); + verticalLayout?.InheritTotalChildrenMinHeight(); + + label = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform, Anchor.CenterRight), + string.IsNullOrEmpty(valueLabelTag) ? "" : TextManager.Get(valueLabelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) + { + CanBeFocused = false + }; + + //slider has a reference to the label to change the text when it's used + slider.UserData = label; + + slider.ToolTip = label.ToolTip = TextManager.Get(tooltipTag); + return verticalLayout ?? container; + } + + public static GUINumberInput CreateLabeledNumberInput(GUIComponent parent, string labelTag, int min, int max, string toolTipTag = null, GUIFont font = null) + { + var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f, + ToolTip = TextManager.Get(labelTag) + }; + + var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), + TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: font) + { + AutoScaleHorizontal = true + }; + if (!string.IsNullOrEmpty(toolTipTag)) + { + label.ToolTip = TextManager.Get(toolTipTag); + } + var input = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), NumberType.Int) + { + MinValueInt = min, + MaxValueInt = max + }; + + container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y); + container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y); + + return input; + } + + public static GUIDropDown CreateLabeledDropdown(GUIComponent parent, string labelTag, int numElements, string toolTipTag = null) + { + var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f, + ToolTip = TextManager.Get(labelTag) + }; + + var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), + TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft) + { + AutoScaleHorizontal = true + }; + if (!string.IsNullOrEmpty(toolTipTag)) + { + label.ToolTip = TextManager.Get(toolTipTag); + } + var input = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), elementCount: numElements); + + container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y); + container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y); + + return input; + } + + private void CreateSidePanelContents(GUIComponent rightPanel) + { //player info panel ------------------------------------------------------------ - myCharacterFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), sideBar.RectTransform)); - playerInfoContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), myCharacterFrame.RectTransform, Anchor.Center), style: null); + var myCharacterFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.55f), rightPanel.RectTransform), style: null); + var myCharacterContent = new GUILayoutGroup(new RectTransform(new Vector2(1), myCharacterFrame.RectTransform, Anchor.Center)) + { + Stretch = true + }; - spectateBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.06f), myCharacterFrame.RectTransform) { RelativeOffset = new Vector2(0.05f, 0.05f) }, + spectateBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.06f), myCharacterContent.RectTransform), TextManager.Get("spectatebutton")) { Selected = false, @@ -428,17 +1223,22 @@ namespace Barotrauma UserData = "spectate" }; + playerInfoContent = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), myCharacterContent.RectTransform)) + { + Stretch = true + }; + // Social area - GUIFrame logBackground = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), sideBar.RectTransform)); - GUILayoutGroup logHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), logBackground.RectTransform, Anchor.Center)) + GUIFrame logFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightPanel.RectTransform), style: null); + GUILayoutGroup logContents = new GUILayoutGroup(new RectTransform(Vector2.One, logFrame.RectTransform, Anchor.Center)) { Stretch = true }; GUILayoutGroup socialHolder = null; GUILayoutGroup serverLogHolder = null; - LogButtons = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), logHolder.RectTransform), true) + LogButtons = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), logContents.RectTransform), true) { Stretch = true, RelativeSpacing = 0.02f @@ -447,7 +1247,7 @@ namespace Barotrauma clientHiddenElements.Add(LogButtons); // Show chat button - showChatButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), + chatPanelTabButtons.Add(new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), TextManager.Get("Chat"), style: "GUITabButton") { Selected = true, @@ -455,34 +1255,32 @@ namespace Barotrauma { if (socialHolder != null) { socialHolder.Visible = true; } if (serverLogHolder != null) { serverLogHolder.Visible = false; } - showChatButton.Selected = true; - showLogButton.Selected = false; + chatPanelTabButtons.ForEach(otherBtn => otherBtn.Selected = otherBtn == button); return true; } - }; + }); // Server log button - showLogButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), + chatPanelTabButtons.Add(new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), TextManager.Get("ServerLog"), style: "GUITabButton") { OnClicked = (GUIButton button, object userData) => { if (socialHolder != null) { socialHolder.Visible = false; } - if (!(serverLogHolder?.Visible ?? true)) + if (serverLogHolder is { Visible: false }) { if (GameMain.Client?.ServerSettings?.ServerLog == null) { return false; } serverLogHolder.Visible = true; GameMain.Client.ServerSettings.ServerLog.AssignLogFrame(serverLogReverseButton, serverLogBox, serverLogFilterTicks.Content, serverLogFilter); } - showChatButton.Selected = false; - showLogButton.Selected = true; + chatPanelTabButtons.ForEach(otherBtn => otherBtn.Selected = otherBtn == button); return true; } - }; + }); - GUITextBlock.AutoScaleAndNormalize(showChatButton.TextBlock, showLogButton.TextBlock); + GUITextBlock.AutoScaleAndNormalize(chatPanelTabButtons.Select(btn => btn.TextBlock)); - GUIFrame logHolderBottom = new GUIFrame(new RectTransform(Vector2.One, logHolder.RectTransform), style: null) + GUIFrame logHolderBottom = new GUIFrame(new RectTransform(Vector2.One, logContents.RectTransform), style: null) { CanBeFocused = false }; @@ -586,6 +1384,65 @@ namespace Barotrauma Font = GUIStyle.SmallFont }; + } + + private void CreateBottomPanelContents(GUIComponent bottomBar) + { + //bottom panel ------------------------------------------------------------ + + GUILayoutGroup bottomBarLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) + { + Stretch = true, + IsHorizontal = true, + RelativeSpacing = PanelSpacing + }; + GUILayoutGroup bottomBarMid = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) + { + Stretch = true, + IsHorizontal = true, + RelativeSpacing = PanelSpacing + }; + GUILayoutGroup bottomBarRight = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) + { + Stretch = true, + IsHorizontal = true, + RelativeSpacing = PanelSpacing + }; + + var disconnectButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), bottomBarLeft.RectTransform), TextManager.Get("disconnect")) + { + OnClicked = (bt, userdata) => { GameMain.QuitToMainMenu(save: false, showVerificationPrompt: true); return true; } + }; + disconnectButton.TextBlock.AutoScaleHorizontal = true; + + + // file transfers ------------------------------------------------------------ + FileTransferFrame = new GUIFrame(new RectTransform(Vector2.One, bottomBarLeft.RectTransform), style: "TextFrame"); + var fileTransferContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), FileTransferFrame.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + FileTransferTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), "", font: GUIStyle.SmallFont); + var fileTransferBottom = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + FileTransferProgressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), fileTransferBottom.RectTransform), 0.0f, Color.DarkGreen); + FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "", + font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); + new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), fileTransferBottom.RectTransform), TextManager.Get("cancel"), style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => + { + if (FileTransferFrame.UserData is not FileReceiver.FileTransferIn transfer) { return false; } + GameMain.Client?.CancelFileTransfer(transfer); + GameMain.Client?.FileReceiver.StopTransfer(transfer); + return true; + } + }; + + roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { @@ -601,9 +1458,8 @@ namespace Barotrauma ReadyToStartBox = new GUITickBox(new RectTransform(new Vector2(0.95f, 0.75f), readyToStartContainer.RectTransform, anchor: Anchor.Center), TextManager.Get("ReadyToStartTickBox")); - // Spectate button - spectateButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), - TextManager.Get("SpectateButton")); + joinOnGoingRoundButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), + TextManager.Get("ServerListJoin")); // Start button StartButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), @@ -612,8 +1468,15 @@ namespace Barotrauma OnClicked = (btn, obj) => { if (GameMain.Client == null) { return true; } - GameMain.Client.RequestStartRound(); - CoroutineManager.StartCoroutine(WaitForStartRound(StartButton), "WaitForStartRound"); + if (CampaignSetupFrame.Visible && CampaignSetupUI != null) + { + CampaignSetupUI.StartGameClicked(btn, obj); + } + else + { + GameMain.Client.RequestStartRound(); + CoroutineManager.StartCoroutine(WaitForStartRound(StartButton), "WaitForStartRound"); + } return true; } }; @@ -629,676 +1492,18 @@ namespace Barotrauma { OnSelected = (tickBox) => { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, autoRestart: tickBox.Selected); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; } }; - clientDisabledElements.Add(autoRestartBoxContainer); + clientDisabledElements.Add(autoRestartBox); + AssignComponentToServerSetting(autoRestartBox, nameof(ServerSettings.AutoRestart)); - //-------------------------------------------------------------------------------------------------------------------------------- - //infoframe contents - //-------------------------------------------------------------------------------------------------------------------------------- - - //server info ------------------------------------------------------------------ - - // Server Info Header - GUILayoutGroup lobbyHeader = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), infoFrameContent.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - - ServerName = new GUITextBox(new RectTransform(Vector2.One, lobbyHeader.RectTransform)) - { - MaxTextLength = NetConfig.ServerNameMaxLength, - OverflowClip = true - }; - ServerName.OnDeselected += (textBox, key) => - { - if (GameMain.Client == null) { return; } - if (!textBox.Readonly) - { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Name); - } - }; - clientReadonlyElements.Add(ServerName); - - Favorite = new GUITickBox(new RectTransform(new Vector2(1.0f, 1.0f), lobbyHeader.RectTransform, scaleBasis: ScaleBasis.BothHeight), - "", null, "GUIServerListFavoriteTickBox") - { - Selected = false, - ToolTip = TextManager.Get("addtofavorites"), - OnSelected = (tickbox) => - { - if (GameMain.Client == null) { return true; } - ServerInfo info = GameMain.Client.CreateServerInfoFromSettings(); - if (tickbox.Selected) - { - GameMain.ServerListScreen.AddToFavoriteServers(info); - } - else - { - GameMain.ServerListScreen.RemoveFromFavoriteServers(info); - } - tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites"); - return true; - } - }; - - SettingsButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), lobbyHeader.RectTransform, Anchor.TopRight), - TextManager.Get("ServerSettingsButton")); - clientHiddenElements.Add(SettingsButton); - - lobbyHeader.RectTransform.MinSize = new Point(0, Math.Max(ServerName.Rect.Height, SettingsButton.Rect.Height)); - - GUILayoutGroup lobbyContent = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), infoFrameContent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.025f - }; - - GUILayoutGroup serverInfoHolder = new GUILayoutGroup(new RectTransform(Vector2.One, lobbyContent.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.025f - }; - - var serverBanner = new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.25f), serverInfoHolder.RectTransform), DrawServerBanner) - { - HideElementsOutsideFrame = true - }; - new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.05f), serverBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.04f) }, - "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") - { - CanBeFocused = false - }; - - publicOrPrivate = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), serverBanner.RectTransform, Anchor.BottomRight, Pivot.BottomRight), - "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") - { - CanBeFocused = false - }; - - var serverMessageContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), serverInfoHolder.RectTransform)); - ServerMessage = new GUITextBox(new RectTransform(Vector2.One, serverMessageContainer.Content.RectTransform), - style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft); - var serverMessageHint = new GUITextBlock(new RectTransform(Vector2.One, ServerMessage.RectTransform), - textColor: Color.DarkGray * 0.6f, textAlignment: Alignment.TopLeft, font: GUIStyle.Font, text: TextManager.Get("ClickToWriteServerMessage")); - - void updateServerMessageScrollBasedOnCaret() - { - float caretY = ServerMessage.CaretScreenPos.Y; - float bottomCaretExtent = ServerMessage.Font.LineHeight * 1.5f; - float topCaretExtent = -ServerMessage.Font.LineHeight * 0.5f; - if (caretY + bottomCaretExtent > serverMessageContainer.Rect.Bottom) - { - serverMessageContainer.ScrollBar.BarScroll - = (caretY - ServerMessage.Rect.Top - serverMessageContainer.Rect.Height + bottomCaretExtent) - / (ServerMessage.Rect.Height - serverMessageContainer.Rect.Height); - } - else if (caretY + topCaretExtent < serverMessageContainer.Rect.Top) - { - serverMessageContainer.ScrollBar.BarScroll - = (caretY - ServerMessage.Rect.Top + topCaretExtent) - / (ServerMessage.Rect.Height - serverMessageContainer.Rect.Height); - } - } - - ServerMessage.OnSelected += (textBox, key) => - { - serverMessageHint.Visible = false; - updateServerMessageScrollBasedOnCaret(); - }; - ServerMessage.OnTextChanged += (textBox, text) => - { - serverMessageHint.Visible = !textBox.Selected && !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text); - RefreshServerInfoSize(); - return true; - }; - ServerMessage.RectTransform.SizeChanged += RefreshServerInfoSize; - - void RefreshServerInfoSize() - { - serverMessageHint.Visible = !ServerMessage.Selected && !ServerMessage.Readonly && string.IsNullOrWhiteSpace(ServerMessage.Text); - Vector2 textSize = ServerMessage.Font.MeasureString(ServerMessage.WrappedText); - ServerMessage.RectTransform.NonScaledSize = new Point(ServerMessage.RectTransform.NonScaledSize.X, Math.Max(serverMessageContainer.Content.Rect.Height, (int)textSize.Y + 10)); - serverMessageContainer.UpdateScrollBarSize(); - } - - ServerMessage.OnEnterPressed += (textBox, text) => - { - string str = textBox.Text; - int caretIndex = textBox.CaretIndex; - textBox.Text = $"{str[..caretIndex]}\n{str[caretIndex..]}"; - textBox.CaretIndex = caretIndex + 1; - - return true; - }; - ServerMessage.OnDeselected += (textBox, key) => - { - if (GameMain.Client == null) { return; } - if (!textBox.Readonly) - { - GameMain.Client?.ServerSettings?.ClientAdminWrite(ServerSettings.NetFlags.Message); - } - serverMessageHint.Visible = !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text); - }; - - ServerMessage.OnKeyHit += (sender, key) => updateServerMessageScrollBasedOnCaret(); - - - clientHiddenElements.Add(serverMessageHint); - clientReadonlyElements.Add(ServerMessage); - - //submarine list ------------------------------------------------------------------ - - GUILayoutGroup subHolder = new GUILayoutGroup(new RectTransform(Vector2.One, lobbyContent.RectTransform)) - { - RelativeSpacing = panelSpacing, - Stretch = true - }; - - var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Submarine"), font: GUIStyle.SubHeadingFont); - - SubVisibilityButton - = new GUIButton( - new RectTransform(Vector2.One * 1.2f, subLabel.RectTransform, anchor: Anchor.CenterRight, - scaleBasis: ScaleBasis.BothHeight), - style: "EyeButton") - { - OnClicked = (button, o) => - { - CreateSubmarineVisibilityMenu(); - return false; - } - }; - clientHiddenElements.Add(SubVisibilityButton); - - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subHolder.RectTransform), isHorizontal: true) - { - Stretch = true - }; - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUIStyle.Font); - subSearchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); - filterContainer.RectTransform.MinSize = subSearchBox.RectTransform.MinSize; - subSearchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - subSearchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - subSearchBox.OnTextChanged += (textBox, text) => - { - UpdateSubVisibility(); - return true; - }; - - SubList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)) - { - PlaySoundOnSelect = true, - OnSelected = VotableClicked - }; - - var voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), subLabel.RectTransform, Anchor.TopRight), - TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) - { - UserData = "subvotes", - Visible = false, - CanBeFocused = false - }; - - //respawn shuttle / submarine preview ------------------------------------------------------------------ - - GUILayoutGroup rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), lobbyContent.RectTransform)) - { - RelativeSpacing = panelSpacing, - Stretch = true - }; - - GUILayoutGroup shuttleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), isHorizontal: true) - { - Stretch = true - }; - - shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle")) - { - Selected = true, - OnSelected = (GUITickBox box) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, useRespawnShuttle: box.Selected); - return true; - } - }; - shuttleTickBox.TextBlock.RectTransform.SizeChanged += () => - { - shuttleTickBox.TextBlock.AutoScaleHorizontal = true; - shuttleTickBox.TextBlock.TextScale = 1.0f; - if (shuttleTickBox.TextBlock.TextScale < 0.75f) - { - shuttleTickBox.TextBlock.Wrap = true; - shuttleTickBox.TextBlock.AutoScaleHorizontal = true; - shuttleTickBox.TextBlock.TextScale = 1.0f; - } - }; - ShuttleList = new GUIDropDown(new RectTransform(Vector2.One, shuttleHolder.RectTransform), elementCount: 10) - { - OnSelected = (component, obj) => - { - GameMain.Client?.RequestSelectSub(obj as SubmarineInfo, isShuttle: true); - return true; - } - }; - ShuttleList.ListBox.RectTransform.MinSize = new Point(250, 0); - shuttleHolder.RectTransform.MinSize = new Point(0, ShuttleList.RectTransform.Children.Max(c => c.MinSize.Y)); - - subPreviewContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), rightColumn.RectTransform), style: null); - subPreviewContainer.RectTransform.SizeChanged += () => - { - if (SelectedSub != null) { CreateSubPreview(SelectedSub); } - }; - - //------------------------------------------------------------------------------------------------------------------ - // Gamemode panel - //------------------------------------------------------------------------------------------------------------------ - - GUILayoutGroup gameModeBackground = new GUILayoutGroup(new RectTransform(Vector2.One, gameModeContainer.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - GUILayoutGroup gameModeHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform)) - { - Stretch = true - }; - - var modeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), gameModeHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("GameMode"), font: GUIStyle.SubHeadingFont); - voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), modeLabel.RectTransform, Anchor.TopRight), - TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) - { - UserData = "modevotes", - Visible = false - }; - ModeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform)) - { - PlaySoundOnSelect = true, - OnSelected = VotableClicked - }; - - foreach (GameModePreset mode in GameModePreset.List) - { - if (mode.IsSinglePlayer) { continue; } - - var modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), ModeList.Content.RectTransform), style: null) - { - UserData = mode - }; - - var modeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.9f), modeFrame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.02f, 0.0f) }) - { - AbsoluteSpacing = (int)(5 * GUI.Scale), - Stretch = true - }; - - var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont); - var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true); - //leave some padding for the vote count text - modeDescription.Padding = new Vector4(modeDescription.Padding.X, modeDescription.Padding.Y, GUI.IntScale(30), modeDescription.Padding.W); - modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent; - modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor; - modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f; - modeFrame.OnAddedToGUIUpdateList = (c) => - { - modeTitle.State = modeDescription.State = c.State; - }; - - new GUIImage(new RectTransform(new Vector2(0.2f, 0.8f), modeFrame.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f) }, - style: "GameModeIcon." + mode.Identifier, scaleToFit: true); - - modeFrame.RectTransform.MinSize = new Point(0, (int)(modeContent.Children.Sum(c => c.Rect.Height + modeContent.AbsoluteSpacing) / modeContent.RectTransform.RelativeSize.Y)); - } - - var gameModeSpecificFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform), style: null); - CampaignSetupFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) - { - Visible = false - }; - CampaignFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) - { - Visible = false - }; - GUILayoutGroup campaignContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.5f), CampaignFrame.RectTransform, Anchor.Center)) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), - TextManager.Get("gamemode.multiplayercampaign"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); - ContinueCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), - TextManager.Get("campaigncontinue"), textAlignment: Alignment.Center) - { - OnClicked = (_, __) => - { - CoroutineManager.StartCoroutine(WaitForStartRound(ContinueCampaignButton), "WaitForStartRound"); - GameMain.Client?.RequestStartRound(true); - return true; - } - }; - QuitCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), - TextManager.Get("quitbutton"), textAlignment: Alignment.Center) - { - OnClicked = (_, __) => - { - if (GameMain.Client == null) { return false; } - if (GameMain.Client.GameStarted) - { - GameMain.Client.RequestRoundEnd(save: false); - } - else - { - GameMain.Client.RequestRoundEnd(save: false, quitCampaign: true); - } - return true; - } - }; - - //mission type ------------------------------------------------------------------ - MissionTypeFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null); - - GUILayoutGroup missionHolder = new GUILayoutGroup(new RectTransform(Vector2.One, MissionTypeFrame.RectTransform)) - { - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), missionHolder.RectTransform) { MinSize = new Point(0, 25) }, - TextManager.Get("MissionType"), font: GUIStyle.SubHeadingFont); - missionTypeList = new GUIListBox(new RectTransform(Vector2.One, missionHolder.RectTransform)) - { - OnSelected = (component, obj) => - { - return false; - } - }; - - var missionTypes = (MissionType[])Enum.GetValues(typeof(MissionType)); - missionTypeTickBoxes = new GUITickBox[missionTypes.Length - 2]; - int index = 0; - for (int i = 0; i < missionTypes.Length; i++) - { - var missionType = missionTypes[i]; - if (missionType == MissionType.None || missionType == MissionType.All) { continue; } - - GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), missionTypeList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, style: "ListBoxElement") - { - UserData = missionType, - }; - - if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) - { - missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), string.Empty) - { - UserData = (int)missionType, - Visible = false, - CanBeFocused = false - }; - } - else - { - missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), - TextManager.Get("MissionType." + missionType.ToString())) - { - UserData = (int)missionType, - ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString()), - OnSelected = (tickbox) => - { - int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; - int missionTypeAnd = (int)MissionType.All & (!tickbox.Selected ? (~(int)tickbox.UserData) : (int)MissionType.All); - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); - return true; - } - }; - frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; - } - index++; - } - clientDisabledElements.AddRange(missionTypeTickBoxes); - - //------------------------------------------------------------------ - // settings panel - //------------------------------------------------------------------ - - GUILayoutGroup settingsHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform)) - { - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 25) }, - TextManager.Get("Settings"), font: GUIStyle.SubHeadingFont); - - var settingsFrameTop = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.55f), settingsHolder.RectTransform), style: "InnerFrame"); - var settingsContentTop = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), settingsFrameTop.RectTransform, Anchor.Center)) - { - Stretch = true, - AbsoluteSpacing = GUI.IntScale(10) - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 30) }, - TextManager.Get("TraitorSettings"), font: GUIStyle.SubHeadingFont); - - var settingsFrameBottom = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.35f), settingsHolder.RectTransform), style: "InnerFrame"); - var settingsContentBottom = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.85f), settingsFrameBottom.RectTransform, Anchor.Center)) - { - Stretch = true, - AbsoluteSpacing = GUI.IntScale(10) - }; - - //seed ------------------------------------------------------------------ - - var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), TextManager.Get("LevelSeed")); - SeedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight)); - SeedBox.OnDeselected += (textBox, key) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); - }; - clientDisabledElements.Add(SeedBox); - LevelSeed = ToolBox.RandomSeed(8); - - //level difficulty ------------------------------------------------------------------ - - var difficultyHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), settingsContentTop.RectTransform), style: null) - { - CanBeFocused = true - }; - - var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), difficultyHolder.RectTransform), TextManager.Get("LevelDifficulty")) - { - ToolTip = TextManager.Get("leveldifficultyexplanation") - }; - - levelDifficultyScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.5f), difficultyHolder.RectTransform, Anchor.BottomCenter), style: "GUISlider", barSize: 0.2f) - { - Step = 0.01f, - Range = new Vector2(0.0f, 100.0f), - ToolTip = TextManager.Get("leveldifficultyexplanation"), - OnReleased = (scrollbar, value) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, levelDifficulty: scrollbar.BarScrollValue); - return true; - } - }; - var difficultyName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), "", textAlignment: Alignment.CenterRight) - { - ToolTip = TextManager.Get("leveldifficultyexplanation") - }; - levelDifficultyScrollBar.OnMoved = (scrollbar, value) => - { - if (!EventManagerSettings.Prefabs.Any()) { return true; } - difficultyName.Text = - EventManagerSettings.GetByDifficultyPercentile(value).Name - + $" ({TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue)).ToString())})"; - difficultyName.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); - return true; - }; - - clientDisabledElements.Add(levelDifficultyScrollBar); - - //bot count ------------------------------------------------------------------ - - var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - - new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botCountSettingHolder.RectTransform), TextManager.Get("BotCount"), wrap: true); - var botCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), botCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - botCountButtons = new GUIButton[2]; - botCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botCountContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: -1); - return true; - } - }; - - botCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), botCountContainer.RectTransform), "0", textAlignment: Alignment.Center, style: "GUITextBox"); - botCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botCountContainer.RectTransform), style: "GUIButtonToggleRight") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: 1); - return true; - } - }; - botCountSettingHolder.RectTransform.MinSize = new Point(0, SeedBox.RectTransform.MinSize.Y); - clientDisabledElements.AddRange(botCountButtons); - - var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - - new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botSpawnModeSettingHolder.RectTransform), TextManager.Get("BotSpawnMode"), wrap: true); - var botSpawnModeContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - botSpawnModeButtons = new GUIButton[2]; - botSpawnModeButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botSpawnModeContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: -1); - return true; - } - }; - - botSpawnModeText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), botSpawnModeContainer.RectTransform), "", textAlignment: Alignment.Center, style: "GUITextBox"); - botSpawnModeButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botSpawnModeContainer.RectTransform), style: "GUIButtonToggleRight") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: 1); - return true; - } - }; - - clientDisabledElements.AddRange(botSpawnModeButtons); - - settingsBlocker = new GUIFrame(new RectTransform(Vector2.One, settingsFrameTop.RectTransform), style: "InnerFrame") - { - Color = Color.Black * 0.85f, - IgnoreLayoutGroups = true, - Visible = false - }; - new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.3f), settingsBlocker.RectTransform, Anchor.Center), - TextManager.Get("settings.campaigndisabled"), wrap: true, style: "InnerFrame", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); - - //traitor probability ------------------------------------------------------------------ - - var traitorsProbHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), settingsContentBottom.RectTransform), style: null); - - var traitorProbLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), traitorsProbHolder.RectTransform), TextManager.Get("traitor.probability")); - - traitorProbabilitySlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.5f), traitorsProbHolder.RectTransform, Anchor.BottomCenter), style: "GUISlider", barSize: 0.2f) - { - Step = 0.05f, - Range = new Vector2(0.0f, 1.0f), - ToolTip = TextManager.Get("traitor.probability.tooltip"), - OnReleased = (scrollbar, value) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorProbability: value); - return true; - } - }; - var traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), traitorProbLabel.RectTransform), "", textAlignment: Alignment.CenterRight) - { - ToolTip = TextManager.Get("traitor.probability.tooltip") - }; - traitorProbabilitySlider.OnMoved = (scrollbar, value) => - { - traitorProbabilityText.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue * 100)).ToString()); - traitorProbabilityText.TextColor = - value <= 0.0f ? - GUIStyle.Green : - ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red); - return true; - }; - - traitorElements.Clear(); - traitorElements.Add(traitorProbabilityText); - traitorElements.Add(traitorProbabilitySlider); - clientDisabledElements.Add(traitorProbabilitySlider); - - var traitorDangerHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), settingsContentBottom.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerHolder.RectTransform), TextManager.Get("traitor.dangerlevelsetting"), wrap: true) - { - ToolTip = TextManager.Get("traitor.dangerlevelsetting.tooltip") - }; - - var traitorDangerContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), traitorDangerHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - var traitorDangerButtons = new GUIButton[2]; - traitorDangerButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: -1); - return true; - } - }; - - traitorDangerGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - AbsoluteSpacing = 1 - }; - for (int i = TraitorEventPrefab.MinDangerLevel; i <= TraitorEventPrefab.MaxDangerLevel; i++) - { - var difficultyColor = Mission.GetDifficultyColor(i); - new GUIImage(new RectTransform(new Vector2(0.75f), traitorDangerGroup.RectTransform), "DifficultyIndicator", scaleToFit: true) - { - ToolTip = - RichString.Rich( - $"‖color:{Color.White.ToStringHex()}‖{TextManager.Get($"traitor.dangerlevel.{i}")}‖color:end‖" + '\n' + - TextManager.Get($"traitor.dangerlevel.{i}.description")), - Color = difficultyColor, - DisabledColor = Color.Gray * 0.5f, - }; - } - - traitorDangerButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleRight") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: 1); - return true; - } - }; - - SetTraitorDangerIndicators(GameMain.Client?.ServerSettings.TraitorDangerLevel ?? TraitorEventPrefab.MinDangerLevel); - traitorElements.AddRange(traitorDangerButtons); - clientDisabledElements.AddRange(traitorDangerButtons); - - settingsContentTop.Recalculate(); - settingsContentBottom.Recalculate(); } public void StopWaitingForStartRound() { CoroutineManager.StopCoroutines("WaitForStartRound"); - if (StartButton != null) { StartButton.Enabled = true; @@ -1306,7 +1511,7 @@ namespace Barotrauma GUI.ClearCursorWait(); } - public IEnumerable WaitForStartRound(GUIButton startButton) + public static IEnumerable WaitForStartRound(GUIButton startButton) { GUI.SetCursorWaiting(); LocalizedString headerText = TextManager.Get("RoundStartingPleaseWait"); @@ -1352,13 +1557,14 @@ namespace Barotrauma CharacterAppearanceCustomizationMenu?.Dispose(); JobSelectionFrame = null; - infoFrameContent.Recalculate(); - Character.Controlled = null; GameMain.LightManager.LosEnabled = false; GUI.PreventPauseMenuToggle = false; CampaignCharacterDiscarded = false; + changesPendingText?.Parent?.RemoveChild(changesPendingText); + changesPendingText = null; + chatInput.Select(); chatInput.OnEnterPressed = GameMain.Client.EnterChatMessage; chatInput.OnTextChanged += GameMain.Client.TypingChatMessage; @@ -1372,7 +1578,6 @@ namespace Barotrauma //disable/hide elements the clients are not supposed to use/see clientDisabledElements.ForEach(c => c.Enabled = false); - clientReadonlyElements.ForEach(c => c.Readonly = true); clientHiddenElements.ForEach(c => c.Visible = false); RefreshEnabledElements(); @@ -1380,86 +1585,104 @@ namespace Barotrauma if (GameMain.Client != null) { ChatManager.RegisterKeys(chatInput, GameMain.Client.ChatBox.ChatManager); - spectateButton.Visible = GameMain.Client.GameStarted; + joinOnGoingRoundButton.Visible = GameMain.Client.GameStarted; ReadyToStartBox.Selected = false; GameMain.Client.SetReadyToStart(ReadyToStartBox); } else { - spectateButton.Visible = false; + joinOnGoingRoundButton.Visible = false; } SetSpectate(spectateBox.Selected); if (GameMain.Client != null) { GameMain.Client.Voting.ResetVotes(GameMain.Client.ConnectedClients); - spectateButton.OnClicked = GameMain.Client.SpectateClicked; + joinOnGoingRoundButton.OnClicked = GameMain.Client.JoinOnGoingClicked; ReadyToStartBox.OnSelected = GameMain.Client.SetReadyToStart; } roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); roundControlsHolder.Recalculate(); - base.Select(); - } + AssignComponentsToServerSettings(); - public void SetPublic(bool isPublic) - { - publicOrPrivate.Text = isPublic ? TextManager.Get("PublicLobbyTag") : TextManager.Get("PrivateLobbyTag"); + RefreshPlaystyleIcons(); + + base.Select(); } public void RefreshEnabledElements() { - bool manageSettings = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + if (GameMain.Client == null) { return; } + var client = GameMain.Client; + var settings = client.ServerSettings; + bool manageSettings = HasPermission(ClientPermissions.ManageSettings); + bool campaignSelected = CampaignFrame.Visible || CampaignSetupFrame.Visible; + bool campaignStarted = CampaignFrame.Visible; + bool gameStarted = client != null && client.GameStarted; - ServerName.Readonly = !manageSettings; - ServerMessage.Readonly = !manageSettings; - missionTypeList.Enabled = manageSettings; - foreach (var tickBox in missionTypeTickBoxes) + //disable elements the client doesn't have access to + foreach (var element in clientDisabledElements) { - tickBox.Enabled = manageSettings; + element.Enabled = manageSettings; } - SeedBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && manageSettings; - levelDifficultyScrollBar.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && manageSettings; - levelDifficultyScrollBar.ToolTip = string.Empty; - if (!levelDifficultyScrollBar.Enabled) + traitorElements.ForEach(e => e.Enabled &= settings.TraitorProbability > 0); + SetTraitorDangerIndicators(settings.TraitorDangerLevel); + respawnSettingsElements.ForEach(e => e.Enabled &= settings.AllowRespawn); + + //go through the individual elements that are only enabled in a specific context + if (ShuttleList != null) { - levelDifficultyScrollBar.ToolTip = TextManager.Get("campaigndifficultydisabled"); + ShuttleList.Enabled = ShuttleList.ButtonEnabled = HasPermission(ClientPermissions.SelectSub) && !gameStarted && settings.AllowRespawn; } + if (SubList != null) + { + SubList.Enabled = !campaignStarted && (settings.AllowSubVoting || HasPermission(ClientPermissions.SelectSub)); + } + if (ModeList != null) + { + ModeList.Enabled = !gameStarted && (settings.AllowModeVoting || HasPermission(ClientPermissions.SelectMode)); + } + shuttleTickBox.Enabled &= !gameStarted; - traitorElements.ForEach(e => e.Enabled = manageSettings); - botCountButtons[0].Enabled = botCountButtons[1].Enabled = manageSettings; - botSpawnModeButtons[0].Enabled = botSpawnModeButtons[1].Enabled = manageSettings; + RefreshStartButtonVisibility(); - autoRestartBox.Enabled = manageSettings; + botSettingsElements.ForEach(b => b.Enabled = !campaignStarted && manageSettings); - SettingsButton.Visible = manageSettings; - SettingsButton.OnClicked = GameMain.Client.ServerSettings.ToggleSettingsFrame; - StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !GameMain.Client.GameStarted && !CampaignSetupFrame.Visible && !CampaignFrame.Visible; - ServerName.Readonly = !manageSettings; - ServerMessage.Readonly = !manageSettings; - shuttleTickBox.Enabled = manageSettings && !GameMain.Client.GameStarted; - SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); - ShuttleList.Enabled = ShuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub) && !GameMain.Client.GameStarted; - ModeList.Enabled = !GameMain.Client.GameStarted && (GameMain.Client.ServerSettings.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode)); - LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); - GameMain.Client.UpdateLogButtonPermissions(); + campaignDisabledElements.ForEach(e => e.Enabled = !campaignSelected && manageSettings); + levelDifficultySlider.ToolTip = levelDifficultySlider.Enabled ? string.Empty : TextManager.Get("campaigndifficultydisabled"); + + //hide elements the client shouldn't + foreach (var element in clientHiddenElements) + { + element.Visible = manageSettings; + } + //go through the individual elements that are only visible in a specific context + ReadyToStartBox.Parent.Visible = !gameStarted; + LogButtons.Visible = HasPermission(ClientPermissions.ServerLog); + + client?.UpdateLogButtonPermissions(); roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); roundControlsHolder.Children.ForEach(c => c.RectTransform.RelativeSize = Vector2.One); roundControlsHolder.Recalculate(); - SubVisibilityButton.Visible = manageSettings; - - ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; + SettingsButton.OnClicked = settings.ToggleSettingsFrame; RefreshGameModeContent(); + + static bool HasPermission(ClientPermissions permissions) + { + if (GameMain.Client == null) { return false; } + return GameMain.Client.HasPermission(permissions); + } } public void ShowSpectateButton() { if (GameMain.Client == null) { return; } - spectateButton.Visible = true; - spectateButton.Enabled = true; + joinOnGoingRoundButton.Visible = true; + joinOnGoingRoundButton.Enabled = true; StartButton.Visible = false; } @@ -1483,14 +1706,14 @@ namespace Barotrauma private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing = true) { - UpdatePlayerFrame(characterInfo, allowEditing, playerInfoContainer); + UpdatePlayerFrame(characterInfo, allowEditing, playerInfoContent); } public void CreatePlayerFrame(GUIComponent parent, bool createPendingText = true, bool alwaysAllowEditing = false) { if (GameMain.Client == null) { return; } UpdatePlayerFrame( - Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo ?? GameMain.Client.CharacterInfo, + Character.Controlled?.Info ?? playerInfoContent.UserData as CharacterInfo ?? GameMain.Client.CharacterInfo, allowEditing: alwaysAllowEditing || campaignCharacterInfo == null, parent: parent, createPendingText: createPendingText); @@ -1512,12 +1735,8 @@ namespace Barotrauma bool isGameRunning = GameMain.GameSession?.IsRunning ?? false; - infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, isGameRunning ? 0.97f : 0.92f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) - { - RelativeSpacing = 0.0f, - Stretch = true, - UserData = characterInfo - }; + parent.ClearChildren(); + parent.UserData = characterInfo; bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; changesPendingText?.Parent?.RemoveChild(changesPendingText); @@ -1528,8 +1747,7 @@ namespace Barotrauma CreateChangesPendingText(); } - - CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.RectTransform), !nameChangePending ? characterInfo.Name : GameMain.Client.PendingName, textAlignment: Alignment.Center) + CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), parent.RectTransform), !nameChangePending ? characterInfo.Name : GameMain.Client.PendingName, textAlignment: Alignment.Center) { MaxTextLength = Client.MaxNameLength, OverflowClip = true @@ -1566,11 +1784,11 @@ namespace Barotrauma }; //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.006f), infoContainer.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.006f), parent.RectTransform), style: null); if (allowEditing) { - GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.016f), infoContainer.RectTransform), isHorizontal: true) + GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), parent.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f @@ -1592,7 +1810,7 @@ namespace Barotrauma // Unsubscribe from previous events, not even sure if this matters here but it doesn't hurt so why not if (characterInfoFrame != null) { characterInfoFrame.RectTransform.SizeChanged -= RecalculateSubDescription; } - characterInfoFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), infoContainer.RectTransform), style: null); + characterInfoFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); characterInfoFrame.RectTransform.SizeChanged += RecalculateSubDescription; JobPreferenceContainer = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), @@ -1604,7 +1822,7 @@ namespace Barotrauma PlaySoundOnSelect = true, OnSelected = (child, obj) => { - if (child.IsParentOf(GUI.MouseOn)) return false; + if (child.IsParentOf(GUI.MouseOn)) { return false; } return OpenJobSelection(child, obj); } }; @@ -1622,7 +1840,7 @@ namespace Barotrauma } // The old job variant system used one-based indexing // so let's make sure no one get to pick a variant which doesn't exist - var variant = Math.Min(jobPreference.Variant, prefab.Variants - 1); + int variant = Math.Min(jobPreference.Variant, prefab.Variants - 1); jobPrefab = new JobVariant(prefab, variant); break; } @@ -1644,28 +1862,28 @@ namespace Barotrauma } else { - characterInfo.CreateIcon(new RectTransform(new Vector2(0.6f, 0.16f), infoContainer.RectTransform, Anchor.TopCenter)); + characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.16f), parent.RectTransform, Anchor.TopCenter)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) { HoverColor = Color.Transparent, SelectedColor = Color.Transparent }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont); foreach (Skill skill in characterInfo.Job.GetSkills()) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); - var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), + var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()), textColor, font: GUIStyle.SmallFont); } // Spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), infoContainer.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), parent.RectTransform), style: null); - new GUIButton(new RectTransform(new Vector2(0.8f, 0.1f), infoContainer.RectTransform, Anchor.BottomCenter), TextManager.Get("CreateNew")) + new GUIButton(new RectTransform(new Vector2(0.8f, 0.1f), parent.RectTransform, Anchor.BottomCenter), TextManager.Get("CreateNew")) { IgnoreLayoutGroups = true, OnClicked = (btn, userdata) => @@ -1682,7 +1900,7 @@ namespace Barotrauma TeamPreferenceListBox = null; if (SelectedMode == GameModePreset.PvP) { - TeamPreferenceListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.04f), infoContainer.RectTransform, anchor: Anchor.TopLeft, pivot: Pivot.TopLeft), isHorizontal: true, style: null) + TeamPreferenceListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.04f), parent.RectTransform, anchor: Anchor.TopLeft, pivot: Pivot.TopLeft), isHorizontal: true, style: null) { Enabled = true, KeepSpaceForScrollBar = false, @@ -1690,7 +1908,7 @@ namespace Barotrauma ScrollBarEnabled = false, ScrollBarVisible = false }; - + TeamPreferenceListBox.RectTransform.MinSize = new Point(0, GUI.IntScale(30)); TeamPreferenceListBox.UpdateDimensions(); Color team1Color = new Color(0, 110, 150, 255); @@ -1770,9 +1988,9 @@ namespace Barotrauma private void CreateChangesPendingText() { - if (!createPendingChangesText || changesPendingText != null || infoContainer == null) { return; } + if (!createPendingChangesText || changesPendingText != null || playerInfoContent == null) { return; } - changesPendingText = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.Parent.Parent.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, -0.03f) }, + changesPendingText = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.065f), playerInfoContent.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, -0.03f) }, style: "OuterGlow") { Color = Color.Black, @@ -1834,7 +2052,7 @@ namespace Barotrauma int i = 0; foreach (var child in traitorDangerGroup.Children) { - child.Enabled = i < dangerLevel; + child.Enabled = i < dangerLevel && GameMain.Client?.ServerSettings is { TraitorProbability: > 0 }; i++; } } @@ -1848,16 +2066,16 @@ namespace Barotrauma public void SetSpectate(bool spectate) { if (GameMain.Client == null) { return; } - this.spectateBox.Selected = spectate; + spectateBox.Selected = spectate; if (spectate) { - playerInfoContainer.ClearChildren(); + playerInfoContent.ClearChildren(); GameMain.Client.CharacterInfo?.Remove(); GameMain.Client.CharacterInfo = null; GameMain.Client.Character?.Remove(); GameMain.Client.Character = null; - new GUITextBlock(new RectTransform(Vector2.One, playerInfoContainer.RectTransform, Anchor.Center), + new GUITextBlock(new RectTransform(Vector2.One, playerInfoContent.RectTransform, Anchor.Center), TextManager.Get("PlayingAsSpectator"), textAlignment: Alignment.Center); } @@ -1877,10 +2095,6 @@ namespace Barotrauma // Hide spectate tickbox if spectating is not allowed spectateBox.Visible = allowSpectating; - if (infoContainer != null) - { - infoContainer.RectTransform.RelativeSize = new Vector2(infoContainer.RectTransform.RelativeSize.X, spectateBox.Visible ? 0.92f : 0.97f); - } } public void SetAutoRestart(bool enabled, float timer = 0.0f) @@ -1929,14 +2143,15 @@ namespace Barotrauma }; int buttonSize = (int)(frame.Rect.Height * 0.8f); - var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft) /*{ AbsoluteOffset = new Point(buttonSize + 5, 0) }*/, + var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft), ToolBox.LimitString(sub.DisplayName.Value, GUIStyle.Font, subList.Rect.Width - 65), textAlignment: Alignment.CenterLeft) { CanBeFocused = false }; - var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.StringRepresentation == sub.MD5Hash?.StringRepresentation); - if (matchingSub == null) matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); + var matchingSub = + SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.StringRepresentation == sub.MD5Hash?.StringRepresentation) ?? + SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); if (matchingSub == null) { @@ -2437,12 +2652,19 @@ namespace Barotrauma RelativeSpacing = 0.03f }; - var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, hasManagePermissions ? 0.1f : 0.25f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), paddedPlayerFrame.RectTransform), isHorizontal: false); + + var headerTextContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), headerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerContainer.RectTransform), + var headerVolumeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), headerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerTextContainer.RectTransform), text: selectedClient.Name, font: GUIStyle.LargeFont); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, (int)(nameText.Rect.Width * 0.95f)); @@ -2615,7 +2837,7 @@ namespace Barotrauma rankDropDown.SelectItem(null); DebugConsole.Command selectedCommand = tickBox.UserData as DebugConsole.Command; - if (!(PlayerFrame.UserData is Client client)) { return false; } + if (PlayerFrame.UserData is not Client client) { return false; } if (!tickBox.Selected) { @@ -2647,18 +2869,18 @@ namespace Barotrauma banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), TextManager.Get("clientpermission.unban")) { - UserData = selectedClient + UserData = selectedClient, + OnClicked = (bt, userdata) => { GameMain.Client?.UnbanPlayer(selectedClient.Name); return true; } }; - 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 + UserData = selectedClient, + OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; } }; - banButton.OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; }; } banButton.OnClicked += ClosePlayerFrame; } @@ -2682,13 +2904,31 @@ namespace Barotrauma var kickButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), TextManager.Get("Kick")) { - UserData = selectedClient + UserData = selectedClient, + OnClicked = (bt, userdata) => { KickPlayer(selectedClient); return true; } }; - kickButton.OnClicked = (bt, userdata) => { KickPlayer(selectedClient); return true; }; kickButton.OnClicked += ClosePlayerFrame; } - new GUITickBox(new RectTransform(new Vector2(0.175f, 1.0f), headerContainer.RectTransform, Anchor.TopRight), + var volumeLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), headerVolumeContainer.RectTransform), isHorizontal: false); + var volumeTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), volumeTextLayout.RectTransform), TextManager.Get("VoiceChatVolume")); + var volumePercentageText = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), volumeTextLayout.RectTransform), ToolBox.GetFormattedPercentage(selectedClient.VoiceVolume), textAlignment: Alignment.Right); + new GUIScrollBar(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), barSize: 0.1f, style: "GUISlider") + { + Range = new Vector2(0f, 1f), + BarScroll = selectedClient.VoiceVolume / Client.MaxVoiceChatBoost, + OnMoved = (_, barScroll) => + { + float newVolume = barScroll * Client.MaxVoiceChatBoost; + + selectedClient.VoiceVolume = newVolume; + volumePercentageText.Text = ToolBox.GetFormattedPercentage(newVolume); + return true; + } + }; + + new GUITickBox(new RectTransform(new Vector2(0.175f, 1.0f), headerVolumeContainer.RectTransform, Anchor.TopRight), TextManager.Get("Mute")) { Selected = selectedClient.MutedLocally, @@ -2704,7 +2944,7 @@ namespace Barotrauma if (selectedClient.AccountId.TryUnwrap(out var accountId)) { - var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), headerContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) }, + var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), headerTextContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) }, accountId.ViewProfileLabel()) { UserData = selectedClient @@ -2761,13 +3001,13 @@ namespace Barotrauma return true; } - public void KickPlayer(Client client) + public static void KickPlayer(Client client) { if (GameMain.NetworkMember == null || client == null) { return; } GameMain.Client.CreateKickReasonPrompt(client.Name, false); } - public void BanPlayer(Client client) + public static void BanPlayer(Client client) { if (GameMain.NetworkMember == null || client == null) { return; } GameMain.Client.CreateKickReasonPrompt(client.Name, ban: true); @@ -2837,7 +3077,7 @@ namespace Barotrauma if (GUI.MouseOn?.UserData is JobVariant jobPrefab && GUI.MouseOn.Style?.Name == "JobVariantButton") { - if (!(jobVariantTooltip?.UserData is JobVariant prevVisibleVariant) || prevVisibleVariant.Prefab != jobPrefab.Prefab || prevVisibleVariant.Variant != jobPrefab.Variant) + if (jobVariantTooltip?.UserData is not JobVariant prevVisibleVariant || prevVisibleVariant.Prefab != jobPrefab.Prefab || prevVisibleVariant.Variant != jobPrefab.Variant) { CreateJobVariantTooltip(jobPrefab.Prefab, jobPrefab.Variant, GUI.MouseOn.Parent); } @@ -2890,6 +3130,8 @@ namespace Barotrauma } private PlayStyle? prevPlayStyle = null; + private bool? prevIsPublic = null; + private void DrawServerBanner(SpriteBatch spriteBatch, GUICustomComponent component) { if (GameMain.NetworkMember?.ServerSettings == null) { return; } @@ -2901,21 +3143,24 @@ namespace Barotrauma .GetSprite(GUIComponent.ComponentState.None); if (sprite is null) { return; } - float scale = component.Rect.Width / sprite.size.X; - sprite.Draw(spriteBatch, component.Center, scale: scale); + GUI.DrawBackgroundSprite(spriteBatch, sprite, Color.White, drawArea: component.Rect); if (!prevPlayStyle.HasValue || playStyle != prevPlayStyle.Value) { - var nameText = component.GetChild(); - nameText.Text = TextManager.Get($"ServerTag.{playStyle}"); - nameText.Color = sprite.SourceElement.GetAttributeColor("BannerColor") ?? Color.White; - nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); + playstyleText.Text = TextManager.Get($"ServerTag.{playStyle}"); + playstyleText.Color = sprite.SourceElement.GetAttributeColor("BannerColor") ?? Color.White; + playstyleText.RectTransform.NonScaledSize = (playstyleText.Font.MeasureString(playstyleText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); prevPlayStyle = playStyle; - - component.ToolTip = TextManager.Get($"ServerTagDescription.{playStyle}"); + (playstyleText.Parent as GUILayoutGroup)?.Recalculate(); + playstyleText.ToolTip = TextManager.Get($"ServerTagDescription.{playStyle}"); + } + if (!prevIsPublic.HasValue || GameMain.NetworkMember.ServerSettings.IsPublic != prevIsPublic.Value) + { + publicOrPrivateText.Text = GameMain.NetworkMember.ServerSettings.IsPublic ? TextManager.Get("PublicLobbyTag") : TextManager.Get("PrivateLobbyTag"); + publicOrPrivateText.RectTransform.NonScaledSize = (publicOrPrivateText.Font.MeasureString(publicOrPrivateText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); + (publicOrPrivateText.Parent as GUILayoutGroup)?.Recalculate(); + prevIsPublic = GameMain.NetworkMember.ServerSettings.IsPublic; } - - publicOrPrivate.RectTransform.NonScaledSize = (publicOrPrivate.Font.MeasureString(publicOrPrivate.Text) + new Vector2(25, 8) * GUI.Scale).ToPoint(); } private static void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobPrefab, int itemsPerRow) @@ -3097,8 +3342,7 @@ namespace Barotrauma bool moveToNext = obj != null; var jobPrefab = (obj as JobVariant)?.Prefab; - - var prevObj = child.UserData; + object prevObj = child.UserData; var existingChild = JobList.Content.FindChild(d => (d.UserData is JobVariant prefab) && (prefab.Prefab == jobPrefab)); if (existingChild != null && obj != null) @@ -3392,7 +3636,6 @@ namespace Barotrauma }); autoRestartBox.Parent.Visible = true; - settingsBlocker.Visible = false; if (SelectedMode == GameModePreset.Mission || SelectedMode == GameModePreset.PvP) { MissionTypeFrame.Visible = true; @@ -3406,9 +3649,7 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.Map != null) { //campaign running - settingsBlocker.Visible = true; CampaignFrame.Visible = QuitCampaignButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound); - ContinueCampaignButton.Enabled = !GameMain.Client.GameStarted && CampaignFrame.Visible; CampaignSetupFrame.Visible = false; } else @@ -3457,12 +3698,27 @@ namespace Barotrauma } ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; + RefreshStartButtonVisibility(); + } - StartButton.Visible = - GameMain.Client.HasPermission(ClientPermissions.ManageRound) && - !GameMain.Client.GameStarted && - !CampaignSetupFrame.Visible && - !CampaignFrame.Visible; + public void RefreshStartButtonVisibility() + { + if (CampaignSetupUI != null && CampaignSetupFrame is { Visible: true }) + { + //setting up a campaign -> start button only visible if we're in the "new game" tab (load game menu not visible) + StartButton.Visible = + !GameMain.Client.GameStarted && + !CampaignSetupUI.LoadGameMenuVisible && + (GameMain.Client.HasPermission(ClientPermissions.ManageRound) || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)); + } + else + { + //if a campaign is currently running, we must show the start button to allow continuing + bool campaignActive = GameMain.GameSession?.GameMode is CampaignMode; + StartButton.Visible = + (SelectedMode != GameModePreset.MultiPlayerCampaign || campaignActive) && + !GameMain.Client.GameStarted && GameMain.Client.HasPermission(ClientPermissions.ManageRound); + } } public void ToggleCampaignMode(bool enabled) @@ -3472,16 +3728,12 @@ namespace Barotrauma //remove campaign character from the panel if (campaignCharacterInfo != null) { + campaignCharacterInfo = null; UpdatePlayerFrame(null); SetSpectate(spectateBox.Selected); } - campaignCharacterInfo = null; CampaignCharacterDiscarded = false; } - else - { - CampaignFrame.Visible = CampaignSetupFrame.Visible = false; - } RefreshEnabledElements(); if (enabled && SelectedMode != GameModePreset.MultiPlayerCampaign) { @@ -3498,7 +3750,7 @@ namespace Barotrauma subPreviewContainer.ClearChildren(); foreach (GUIComponent child in SubList.Content.Children) { - if (!(child.UserData is SubmarineInfo sub)) { continue; } + if (child.UserData is not SubmarineInfo sub) { continue; } //just check the name, even though the campaign sub may not be the exact same version //we're selecting the sub just for show, the selection is not actually used for anything if (sub.Name == name) @@ -3676,12 +3928,24 @@ namespace Barotrauma private static bool StringsEqual(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase); - + public static bool operator ==(FailedSubInfo a, FailedSubInfo b) => StringsEqual(a.Name, b.Name) && StringsEqual(a.Hash, b.Hash); public static bool operator !=(FailedSubInfo a, FailedSubInfo b) => !(a == b); + + public override int GetHashCode() + { + return HashCode.Combine(Name, Hash); + } + + public override bool Equals(object obj) + { + return obj is FailedSubInfo info && + Name == info.Name && + Hash == info.Hash; + } } public FailedSubInfo? FailedSelectedSub; @@ -3708,7 +3972,7 @@ namespace Barotrauma //matching sub found and already selected, all good if (sub != null) { - if (subList == this.SubList) + if (subList == SubList) { CreateSubPreview(sub); } @@ -3924,7 +4188,7 @@ namespace Barotrauma to.DraggedElement = draggedElement; - to.BarScroll = to.BarScroll * (oldCount / newCount); + to.BarScroll *= (oldCount / newCount); } } @@ -4095,7 +4359,7 @@ namespace Barotrauma { foreach (GUIComponent child in SubList.Content.Children) { - if (!(child.UserData is SubmarineInfo sub)) { continue; } + if (child.UserData is not SubmarineInfo sub) { continue; } child.Visible = (!GameMain.Client.ServerSettings.HiddenSubs.Contains(sub.Name) || (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.Name.Equals(sub.Name, StringComparison.OrdinalIgnoreCase))) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index a435e37a7..472a0292d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -306,22 +306,6 @@ namespace Barotrauma OverflowClip = true }; - if (string.IsNullOrEmpty(ClientNameBox.Text)) - { - TaskPool.Add("GetDefaultUserName", - GetDefaultUserName(), - t => - { - if (!t.TryGetResult(out string name)) { return; } - if (ClientNameBox.Text.IsNullOrEmpty()) { ClientNameBox.Text = name; } - }); - } - ClientNameBox.OnTextChanged += (textbox, text) => - { - MultiplayerPreferences.Instance.PlayerName = text; - return true; - }; - var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - sidebarWidth - infoHolder.RelativeSpacing, 0.5f), infoHolder.RectTransform), isHorizontal: true); tabs[TabEnum.All] = new Tab(TabEnum.All, this, tabButtonHolder, ""); @@ -874,11 +858,44 @@ namespace Barotrauma return 0; } } - + public override void Select() { base.Select(); + + if (string.IsNullOrEmpty(ClientNameBox.Text)) + { + TaskPool.Add("GetDefaultUserName", + GetDefaultUserName(), + t => + { + if (!t.TryGetResult(out string name)) { return; } + if (ClientNameBox.Text.IsNullOrEmpty()) + { + ClientNameBox.Text = name; + string nameWithoutInvisibleSymbols = string.Empty; + foreach (char c in ClientNameBox.Text) + { + Vector2 size = ClientNameBox.Font.MeasureChar(c); + if (size.X > 0 && size.Y > 0) + { + nameWithoutInvisibleSymbols += c; + } + } + if (nameWithoutInvisibleSymbols != ClientNameBox.Text) + { + MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text = nameWithoutInvisibleSymbols; + new GUIMessageBox(TextManager.Get("Warning"), TextManager.GetWithVariable("NameContainsInvisibleSymbols", "[name]", nameWithoutInvisibleSymbols)); + } + } + }); + } + ClientNameBox.OnTextChanged += (textbox, text) => + { + MultiplayerPreferences.Instance.PlayerName = text; + return true; + }; if (EosInterface.IdQueries.IsLoggedIntoEosConnect) { if (SteamManager.IsInitialized) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index f32050a63..ae8b70f03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -50,6 +50,8 @@ namespace Barotrauma private bool editBackgroundColor; private Color backgroundColor = new Color(0.051f, 0.149f, 0.271f, 1.0f); + private bool ControlDown => PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl); + private readonly Camera cam; public override Camera Cam { @@ -366,7 +368,8 @@ namespace Barotrauma "DecorativeSprite", "BarrelSprite", "RailSprite", - "SchematicSprite" + "SchematicSprite", + "WeldedSprite" }; foreach (string spriteElementName in spriteElementNames) @@ -471,10 +474,10 @@ namespace Barotrauma public override void Update(double deltaTime) { base.Update(deltaTime); - Widget.EnableMultiSelect = PlayerInput.KeyDown(Keys.LeftControl); + Widget.EnableMultiSelect = ControlDown; spriteList.SelectMultiple = Widget.EnableMultiSelect; // Select rects with the mouse - if (Widget.selectedWidgets.None() || Widget.EnableMultiSelect) + if (Widget.SelectedWidgets.None() || Widget.EnableMultiSelect) { if (SelectedTexture != null && GUI.MouseOn == null) { @@ -578,7 +581,7 @@ namespace Barotrauma foreach (var sprite in selectedSprites) { var newRect = sprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + if (ControlDown) { newRect.Width--; } @@ -593,7 +596,7 @@ namespace Barotrauma foreach (var sprite in selectedSprites) { var newRect = sprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + if (ControlDown) { newRect.Width++; } @@ -608,7 +611,7 @@ namespace Barotrauma foreach (var sprite in selectedSprites) { var newRect = sprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + if (ControlDown) { newRect.Height++; } @@ -623,7 +626,7 @@ namespace Barotrauma foreach (var sprite in selectedSprites) { var newRect = sprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + if (ControlDown) { newRect.Height--; } @@ -637,6 +640,7 @@ namespace Barotrauma } } + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { graphics.Clear(backgroundColor); @@ -691,22 +695,22 @@ namespace Barotrauma Vector2 GetTopLeft() => sprite.SourceRect.Location.ToVector2(); Vector2 GetTopRight() => new Vector2(GetTopLeft().X + sprite.SourceRect.Width, GetTopLeft().Y); Vector2 GetBottomRight() => new Vector2(GetTopRight().X, GetTopRight().Y + sprite.SourceRect.Height); - var originWidget = GetWidget($"{id}_origin", sprite, widgetSize, Widget.Shape.Cross, initMethod: w => + var originWidget = GetWidget($"{id}_origin", sprite, widgetSize, WidgetShape.Cross, initMethod: w => { - w.tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal()); + w.Tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal()); w.MouseHeld += dTime => { w.DrawPos = PlayerInput.MousePosition.Clamp(textureRect.Location.ToVector2() + GetTopLeft() * zoom, textureRect.Location.ToVector2() + GetBottomRight() * zoom); sprite.Origin = (w.DrawPos - textureRect.Location.ToVector2() - sprite.SourceRect.Location.ToVector2() * zoom) / zoom; - w.tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal()); + w.Tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal()); }; - w.refresh = () => + w.Refresh = () => w.DrawPos = (textureRect.Location.ToVector2() + (sprite.Origin + sprite.SourceRect.Location.ToVector2()) * zoom) .Clamp(textureRect.Location.ToVector2() + GetTopLeft() * zoom, textureRect.Location.ToVector2() + GetBottomRight() * zoom); }); - var positionWidget = GetWidget($"{id}_position", sprite, widgetSize, Widget.Shape.Rectangle, initMethod: w => + var positionWidget = GetWidget($"{id}_position", sprite, widgetSize, WidgetShape.Rectangle, initMethod: w => { - w.tooltip = positionLabel + sprite.SourceRect.Location; + w.Tooltip = positionLabel + sprite.SourceRect.Location; w.MouseHeld += dTime => { w.DrawPos = (drawGrid && snapToGrid) ? @@ -719,13 +723,13 @@ namespace Barotrauma // TODO: cache the sprite name? textBox.Text = GetSpriteName(sprite) + " " + sprite.SourceRect; } - w.tooltip = positionLabel + sprite.SourceRect.Location; + w.Tooltip = positionLabel + sprite.SourceRect.Location; }; - w.refresh = () => w.DrawPos = textureRect.Location.ToVector2() + sprite.SourceRect.Location.ToVector2() * zoom; + w.Refresh = () => w.DrawPos = textureRect.Location.ToVector2() + sprite.SourceRect.Location.ToVector2() * zoom; }); - var sizeWidget = GetWidget($"{id}_size", sprite, widgetSize, Widget.Shape.Rectangle, initMethod: w => + var sizeWidget = GetWidget($"{id}_size", sprite, widgetSize, WidgetShape.Rectangle, initMethod: w => { - w.tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString()); + w.Tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString()); w.MouseHeld += dTime => { w.DrawPos = (drawGrid && snapToGrid) ? @@ -740,9 +744,9 @@ namespace Barotrauma // TODO: cache the sprite name? textBox.Text = GetSpriteName(sprite) + " " + sprite.SourceRect; } - w.tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString()); + w.Tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString()); }; - w.refresh = () => w.DrawPos = textureRect.Location.ToVector2() + new Vector2(sprite.SourceRect.Right, sprite.SourceRect.Bottom) * zoom; + w.Refresh = () => w.DrawPos = textureRect.Location.ToVector2() + new Vector2(sprite.SourceRect.Right, sprite.SourceRect.Bottom) * zoom; }); originWidget.MouseDown += () => GUI.KeyboardDispatcher.Subscriber = null; positionWidget.MouseDown += () => GUI.KeyboardDispatcher.Subscriber = null; @@ -1027,31 +1031,31 @@ namespace Barotrauma #region Widgets private Dictionary widgets = new Dictionary(); - private Widget GetWidget(string id, Sprite sprite, int size = 5, Widget.Shape shape = Widget.Shape.Rectangle, Action initMethod = null) + private Widget GetWidget(string id, Sprite sprite, int size = 5, WidgetShape shape = WidgetShape.Rectangle, Action initMethod = null) { if (!widgets.TryGetValue(id, out Widget widget)) { int selectedSize = (int)Math.Round(size * 1.5f); widget = new Widget(id, size, shape) { - data = sprite, - color = Color.Yellow, - secondaryColor = Color.Gray, - tooltipOffset = new Vector2(selectedSize / 2 + 5, -10) + Data = sprite, + Color = Color.Yellow, + SecondaryColor = Color.Gray, + TooltipOffset = new Vector2(selectedSize / 2 + 5, -10) }; widget.PreDraw += (sp, dTime) => { if (!widget.IsControlled) { - widget.refresh(); + widget.Refresh(); } }; widget.PreUpdate += dTime => widget.Enabled = selectedSprites.Contains(sprite); widget.PostUpdate += dTime => { - widget.inputAreaMargin = widget.IsControlled ? 1000 : 0; - widget.size = widget.IsSelected ? selectedSize : size; - widget.isFilled = widget.IsControlled; + widget.InputAreaMargin = widget.IsControlled ? 1000 : 0; + widget.Size = widget.IsSelected ? selectedSize : size; + widget.IsFilled = widget.IsControlled; }; widgets.Add(id, widget); initMethod?.Invoke(widget); @@ -1062,7 +1066,7 @@ namespace Barotrauma private void ResetWidgets() { widgets.Clear(); - Widget.selectedWidgets.Clear(); + Widget.SelectedWidgets.Clear(); } #endregion } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index cabd7e4fd..32a0a41e4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; using Barotrauma.Steam; @@ -81,7 +81,10 @@ namespace Barotrauma ItemCount, LightCount, ShadowCastingLightCount, - WaterInHulls + WaterInHulls, + LowOxygenOutputWarning, + TooLargeForEndGame, + NotEnoughContainers } public static Vector2 MouseDragStart = Vector2.Zero; @@ -143,6 +146,7 @@ namespace Barotrauma private GUIButton visibilityButton; private GUIFrame layerPanel; private GUIListBox layerList; + private List layerSpecificButtons = new List(); private GUIFrame undoBufferPanel; private GUIFrame undoBufferDisclaimer; @@ -552,6 +556,21 @@ namespace Barotrauma } }; + spacing = new GUIFrame(new RectTransform(new Vector2(0.02f, 1.0f), paddedTopPanel.RectTransform), style: null); + + var selectedLayerText = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), paddedTopPanel.RectTransform), + string.Empty, textAlignment: Alignment.Center); + selectedLayerText.TextGetter = () => + { + string selectedLayer = layerList.SelectedData as string; + if (selectedLayer != prevSelectedLayer) + { + prevSelectedLayer = selectedLayer; + return selectedLayer.IsNullOrEmpty() ? string.Empty : TextManager.GetWithVariable("editor.layer.editinglayer", "[layer]", selectedLayer); + } + return selectedLayerText.Text; + }; + TopPanel.RectTransform.MinSize = new Point(0, (int)(paddedTopPanel.RectTransform.Children.Max(c => c.MinSize.Y) / paddedTopPanel.RectTransform.RelativeSize.Y)); paddedTopPanel.Recalculate(); @@ -581,25 +600,27 @@ namespace Barotrauma { ScrollBarVisible = true, AutoHideScrollBar = false, - OnSelected = (component, o) => + OnSelected = (component, userdata) => { - if (GUI.MouseOn is GUITickBox) { return false; } // lol - if (!(o is string layer)) { return false; } - - MapEntity.SelectedList.Clear(); - foreach (MapEntity entity in MapEntity.MapEntityList.Where(me => !me.Removed && me.Layer == layer)) + //toggling selection is not how listboxes normally work, need to do that manually here + SoundPlayer.PlayUISound(GUISoundType.Select); + if (layerList.SelectedData == userdata) { - if (entity.IsSelected) { continue; } - - MapEntity.SelectedList.Add(entity); + layerSpecificButtons.ForEach(btn => btn.Enabled = false); + layerList.Deselect(); + return false; + } + else + { + layerSpecificButtons.ForEach(btn => btn.Enabled = true); + return true; } - return true; } }; GUILayoutGroup layerButtonGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), layerGroup.RectTransform)); - GUILayoutGroup layerButtonTopGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), isHorizontal: true); + GUILayoutGroup layerButtonBottomGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), isHorizontal: true); GUIButton layerAddButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), layerButtonTopGroup.RectTransform), text: TextManager.Get("editor.layer.newlayer"), style: "GUIButtonFreeScale") { @@ -612,6 +633,7 @@ namespace Barotrauma GUIButton layerDeleteButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), layerButtonTopGroup.RectTransform), text: TextManager.Get("editor.layer.deletelayer"), style: "GUIButtonFreeScale") { + Enabled = false, OnClicked = (button, o) => { if (layerList.SelectedData is string layer) @@ -620,10 +642,12 @@ namespace Barotrauma } return true; } - }; + }; + layerSpecificButtons.Add(layerDeleteButton); - GUIButton layerRenameButton = new GUIButton(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), text: TextManager.Get("editor.layer.renamelayer"), style: "GUIButtonFreeScale") + GUIButton layerRenameButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), layerButtonBottomGroup.RectTransform), text: TextManager.Get("editor.layer.renamelayer"), style: "GUIButtonFreeScale") { + Enabled = false, OnClicked = (button, o) => { if (layerList.SelectedData is string layer) @@ -636,9 +660,27 @@ namespace Barotrauma return true; } }; + layerSpecificButtons.Add(layerRenameButton); - GUITextBlock.AutoScaleAndNormalize(layerAddButton.TextBlock, layerDeleteButton.TextBlock, layerRenameButton.TextBlock); + GUIButton selectLayerButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), layerButtonBottomGroup.RectTransform), text: TextManager.Get("editor.layer.selectlayer"), style: "GUIButtonFreeScale") + { + Enabled = false, + OnClicked = (button, o) => + { + if (layerList.SelectedData is string layer) + { + foreach (MapEntity entity in MapEntity.MapEntityList.Where(me => !me.Removed && me.Layer == layer)) + { + if (entity.IsSelected) { continue; } + MapEntity.SelectedList.Add(entity); + } + } + return true; + } + }; + layerSpecificButtons.Add(selectLayerButton); + GUITextBlock.AutoScaleAndNormalize(layerAddButton.TextBlock, layerDeleteButton.TextBlock, layerRenameButton.TextBlock, selectLayerButton.TextBlock); Vector2 subPanelSize = new Vector2(0.925f, 0.9f); @@ -1213,17 +1255,13 @@ namespace Barotrauma frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); LocalizedString name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; - frame.ToolTip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{name}‖color:end‖"; - if (!ep.Description.IsNullOrEmpty()) - { - frame.ToolTip += '\n' + ep.Description; - } + frame.ToolTip = ep.CreateTooltipText(); - if (ep.ContentPackage != GameMain.VanillaContent && ep.ContentPackage != null) + if (ep.IsModded) { frame.Color = Color.Magenta; - frame.ToolTip = $"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(Color.MediumPurple)}‖{ep.ContentPackage?.Name}‖color:end‖"; } + if (ep.HideInMenus || ep.HideInEditors) { frame.Color = Color.Red; @@ -1392,14 +1430,17 @@ namespace Barotrauma Level.Loaded?.GenerationParams?.AmbientLightColor ?? new Color(3, 3, 3, 3); - UpdateEntityList(); - isAutoSaving = false; + if (!wasSelectedBefore) { OpenEntityMenu(null); wasSelectedBefore = true; } + else + { + OpenEntityMenu(selectedCategory); + } if (backedUpSubInfo != null) { @@ -2035,11 +2076,9 @@ namespace Barotrauma saveFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.6f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 500) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.65f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 500) }); var paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; - //var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveSubDialogHeader"), font: GUIStyle.LargeFont); - var columnArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), paddedSaveFrame.RectTransform), isHorizontal: true) { RelativeSpacing = 0.02f, Stretch = true }; var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.55f, 1.0f), columnArea.RectTransform)) { RelativeSpacing = 0.01f, Stretch = true }; var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.42f, 1.0f), columnArea.RectTransform)) { RelativeSpacing = 0.02f, Stretch = true }; @@ -2123,6 +2162,41 @@ namespace Barotrauma subTypeDropdown.AddItem(TextManager.Get(textTag), subType); } + if (Layers.Any()) + { + var layerVisibilityGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.01f), leftColumn.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), TextManager.Get("editor.layer.visiblebydefault"), textAlignment: Alignment.CenterLeft); + var layerVisibilityDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), + text: LocalizedString.Join(", ", Layers.Where(l => !Submarine.MainSub?.Info?.LayersHiddenByDefault?.Contains(l.ToIdentifier()) ?? false).Select(lt => TextManager.Capitalize(lt.Key)) ?? ((LocalizedString)"None").ToEnumerable()), selectMultiple: true); + foreach (string layerName in Layers.Keys) + { + layerVisibilityDropDown.AddItem(TextManager.Capitalize(layerName), layerName); + if (MainSub?.Info == null) { continue; } + if (!MainSub.Info.LayersHiddenByDefault.Contains(layerName.ToIdentifier())) + { + layerVisibilityDropDown.SelectItem(layerName); + } + } + layerVisibilityDropDown.OnSelected += (_, __) => + { + if (MainSub.Info == null) { return false; } + MainSub.Info.LayersHiddenByDefault.Clear(); + foreach (string layerName in Layers.Keys) + { + //selected as visible = not hidden + if (layerVisibilityDropDown.SelectedDataMultiple.Any(o => o as string == layerName)) + { + continue; + } + MainSub.Info.LayersHiddenByDefault.Add(layerName.ToIdentifier()); + } + + layerVisibilityDropDown.Text = ToolBox.LimitString(layerVisibilityDropDown.Text.Value, layerVisibilityDropDown.Font, layerVisibilityDropDown.Rect.Width); + return true; + }; + layerVisibilityGroup.RectTransform.MinSize = layerVisibilityDropDown.RectTransform.MinSize = new Point(0, layerVisibilityDropDown.RectTransform.Children.Max(c => c.MinSize.Y)); + } + //--------------------------------------- var subTypeDependentSettingFrame = new GUIFrame(new RectTransform((1.0f, 0.5f), leftColumn.RectTransform), style: "InnerFrame"); @@ -2154,7 +2228,7 @@ namespace Barotrauma var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.ModuleFlags.Select(s => TextManager.Capitalize(s.Value)) ?? ((LocalizedString)"None").ToEnumerable()), selectMultiple: true); - foreach (Identifier flag in availableFlags) + foreach (Identifier flag in availableFlags.OrderBy(f => f.Value, StringComparer.InvariantCultureIgnoreCase)) { moduleTypeDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); if (MainSub?.Info?.OutpostModuleInfo == null) { continue; } @@ -2189,7 +2263,7 @@ namespace Barotrauma { allowAttachDropDown.SelectItem("any".ToIdentifier()); } - foreach (Identifier flag in availableFlags) + foreach (Identifier flag in availableFlags.OrderBy(f => f.Value, StringComparer.InvariantCultureIgnoreCase)) { if (flag == "any" || flag == "none") { continue; } allowAttachDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); @@ -2215,12 +2289,13 @@ namespace Barotrauma var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); - HashSet availableLocationTypes = new HashSet { "any".ToIdentifier() }; + HashSet availableLocationTypes = new HashSet(); foreach (LocationType locationType in LocationType.Prefabs) { availableLocationTypes.Add(locationType.Identifier); } var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt.Value)) ?? ((LocalizedString)"any").ToEnumerable()), selectMultiple: true); - foreach (Identifier locationType in availableLocationTypes) + locationTypeDropDown.AddItem(TextManager.Capitalize("any"), "any".ToIdentifier()); + foreach (Identifier locationType in availableLocationTypes.OrderBy(f => f.Value, StringComparer.InvariantCultureIgnoreCase)) { locationTypeDropDown.AddItem(TextManager.Capitalize(locationType.Value), locationType); if (MainSub?.Info?.OutpostModuleInfo == null) { continue; } @@ -2239,7 +2314,6 @@ namespace Barotrauma }; locationTypeGroup.RectTransform.MinSize = new Point(0, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - // gap positions --------------------- var gapPositionGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -2472,7 +2546,7 @@ namespace Barotrauma int basePrice = (GameMain.DebugDraw ? 0 : MainSub?.CalculateBasePrice()) ?? 1000; - new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), NumberType.Int, hidePlusMinusButtons: true) + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), NumberType.Int, buttonVisibility: GUINumberInput.ButtonVisibility.ForceHidden) { IntValue = Math.Max(MainSub?.Info?.Price ?? basePrice, basePrice), MinValueInt = basePrice, @@ -3148,6 +3222,7 @@ namespace Barotrauma } UpdateEntityList(); + OpenEntityMenu(selectedCategory); } saveFrame = null; @@ -3208,14 +3283,18 @@ namespace Barotrauma SubmarineInfo.RefreshSavedSubs(); SetMode(Mode.Default); - loadFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) + loadFrame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); + + new GUIButton(new RectTransform(Vector2.One, loadFrame.RectTransform, Anchor.Center), style: null) { - OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) loadFrame = null; return true; }, + OnClicked = (_, _) => + { + loadFrame = null; + return true; + } }; - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, loadFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.75f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.53f, 0.75f), loadFrame.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.Smallest) { MinSize = new Point(350, 500) }); var paddedLoadFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.01f }; @@ -3905,16 +3984,15 @@ namespace Barotrauma } else { - List availableLayerOptions = new List + + List availableLayers = new List { new ContextMenuOption("editor.layer.nolayer", true, onSelected: () => { MoveToLayer(null, targets); }) }; + availableLayers.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); - availableLayerOptions.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); - - ContextMenuOption[] layerOptions = - { - new ContextMenuOption("editor.layer.movetolayer", isEnabled: hasTargets, availableLayerOptions.ToArray()), + List availableLayerOptions = new List + { new ContextMenuOption("editor.layer.movetolayer", isEnabled: hasTargets, availableLayers.ToArray()), new ContextMenuOption("editor.layer.createlayer", isEnabled: hasTargets, onSelected: () => { CreateNewLayer(null, targets); }), new ContextMenuOption("editor.layer.selectall", isEnabled: hasTargets, onSelected: () => { @@ -3924,19 +4002,11 @@ namespace Barotrauma MapEntity.SelectedList.Add(match); } }), - new ContextMenuOption("editor.layer.openlayermenu", isEnabled: true, onSelected: () => - { - previouslyUsedPanel.Visible = false; - undoBufferPanel.Visible = false; - showEntitiesPanel.Visible = false; - layerPanel.Visible = !layerPanel.Visible; - layerPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(visibilityButton.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); - }) }; + availableLayerOptions.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); GUIContextMenu.CreateContextMenu( new ContextMenuOption("label.openlabel", isEnabled: target != null, onSelected: () => OpenItem(target)), - new ContextMenuOption("editor.layer", isEnabled: hasTargets, layerOptions), 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))), @@ -3948,6 +4018,10 @@ namespace Barotrauma if (!me.Removed) { me.Remove(); } } }), + new ContextMenuOption(string.Empty, isEnabled: false, onSelected: () => { /*do nothing*/ }), + new ContextMenuOption("editor.layer.movetoactivelayer", isEnabled: !(layerList?.SelectedData as string).IsNullOrEmpty(), onSelected: () => { MoveToLayer(layerList.SelectedData as string, targets); }), + new ContextMenuOption("editor.layer.removefromlayer", isEnabled: targets.Any(t => t.Layer != string.Empty), onSelected: () => { targets.ForEach(t => t.Layer = string.Empty); }), + new ContextMenuOption("editor.layeroptions", isEnabled: hasTargets, availableLayerOptions.ToArray()), new ContextMenuOption(TextManager.GetWithVariable("editortip.shiftforextraoptions", "[button]", PlayerInput.SecondaryMouseLabel) + '\n' + TextManager.Get("editortip.altforruler"), isEnabled: false, onSelected: null)); } } @@ -3958,6 +4032,10 @@ namespace Barotrauma foreach (MapEntity entity in content) { + if (MapEntity.SelectedList.Contains(entity)) + { + MapEntity.ResetEditingHUD(); + } entity.Layer = layer; } } @@ -4003,7 +4081,7 @@ namespace Barotrauma UpdateLayerPanel(); } - private void ReconstructLayers() + public void ReconstructLayers() { ClearLayers(); foreach (MapEntity entity in MapEntity.MapEntityList) @@ -5055,6 +5133,29 @@ namespace Barotrauma } GameMain.SubEditorScreen.UpdateUndoHistoryPanel(); + + if (command is AddOrDeleteCommand addOrDelete) + { + GameMain.SubEditorScreen.EntityAddedOrDeleted(addOrDelete.Receivers); + } + } + + private string prevSelectedLayer; + private void EntityAddedOrDeleted(IEnumerable entities) + { + if (layerList?.SelectedData is string selectedLayer) + { + //add the created entities to the selected layer + foreach (var entity in entities) + { + if (!entity.Removed) + { + entity.Layer = selectedLayer; + } + } + var layerElement = layerList.Content.FindChild(selectedLayer); + layerElement?.Flash(GUIStyle.Green); + } } private void UpdateLayerPanel() @@ -5064,10 +5165,15 @@ namespace Barotrauma layerList.Content.ClearChildren(); layerList.Deselect(); + layerSpecificButtons.ForEach(btn => btn.Enabled = false); GUILayoutGroup buttonHeaders = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), layerList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft); - new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headervisible"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; - new GUIButton(new RectTransform(new Vector2(0.15f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headerlink"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; + new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headervisible"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; + new GUIButton(new RectTransform(new Vector2(0.15f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headerlink"), style: "GUIButtonSmallFreeScale") + { + ForceUpperCase = ForceUpperCase.Yes, + ToolTip = TextManager.Get("editor.layer.headerlink.tooltip") + }; new GUIButton(new RectTransform(new Vector2(0.6f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; foreach (var (layer, (visibility, linkage)) in Layers) @@ -5090,7 +5196,11 @@ namespace Barotrauma UpdateLayerPanel(); return false; } - + //hiding a layer automatically deselects it (can't edit a hidden layer) + if (!box.Selected && layerList.SelectedData as string == layer) + { + layerList.Deselect(); + } Layers[layer] = new LayerData(box.Selected ? LayerVisibility.Visible : LayerVisibility.Invisible, data.Linkage); return true; } @@ -5134,7 +5244,7 @@ namespace Barotrauma var btn = child as GUIButton; string originalBtnText = btn.Text.Value; btn.Text = ToolBox.LimitString(btn.Text, btn.Font, btn.Rect.Width); - if (originalBtnText != btn.Text) + if (originalBtnText != btn.Text && btn.ToolTip.IsNullOrEmpty()) { btn.ToolTip = originalBtnText; } @@ -5334,51 +5444,58 @@ namespace Barotrauma } } - if (PlayerInput.KeyHit(InputType.Use) && mode == Mode.Default) + if (mode == Mode.Default) { - if (dummyCharacter != null) + if (PlayerInput.KeyHit(InputType.Use)) { - if (dummyCharacter.SelectedItem == null) + if (dummyCharacter != null) { - foreach (var entity in MapEntity.HighlightedEntities) + if (dummyCharacter.SelectedItem == null) { - if (entity is Item item && item.Components.Any(ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) + foreach (var entity in MapEntity.HighlightedEntities) { - var container = item.GetComponents().ToList(); - if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false)) + if (entity is Item item && item.Components.Any(ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) { - OpenItem(item); - break; + var container = item.GetComponents().ToList(); + if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false)) + { + OpenItem(item); + break; + } } } } - } - else - { - CloseItem(); + else + { + CloseItem(); + } } } - } - // Focus to selection - if (PlayerInput.KeyHit(Keys.F) && mode == Mode.Default) - { - // content warning: contains coordinate system workarounds - var selected = MapEntity.SelectedList; - if (selected.Count > 0) + // Focus to selection + if (PlayerInput.KeyHit(Keys.F)) { - var dRect = selected.First().Rect; - var rect = new Rectangle(dRect.Left, dRect.Top, dRect.Width, dRect.Height * -1); - if (selected.Count > 1) + // content warning: contains coordinate system workarounds + var selected = MapEntity.SelectedList; + if (selected.Count > 0) { - // Create one big rect out of our selection - selected.Skip(1).ForEach(me => + var dRect = selected.First().Rect; + var rect = new Rectangle(dRect.Left, dRect.Top, dRect.Width, dRect.Height * -1); + if (selected.Count > 1) { - var wRect = me.Rect; - rect = Rectangle.Union(rect, new Rectangle(wRect.Left, wRect.Top, wRect.Width, wRect.Height * -1)); - }); + // Create one big rect out of our selection + selected.Skip(1).ForEach(me => + { + var wRect = me.Rect; + rect = Rectangle.Union(rect, new Rectangle(wRect.Left, wRect.Top, wRect.Width, wRect.Height * -1)); + }); + } + camTargetFocus = rect.Center.ToVector2(); } - camTargetFocus = rect.Center.ToVector2(); + } + if (PlayerInput.KeyHit(Keys.Tab)) + { + entityFilterBox.Select(); } } @@ -5392,11 +5509,6 @@ namespace Barotrauma toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); } - if (PlayerInput.KeyHit(Keys.Tab)) - { - entityFilterBox.Select(); - } - if (PlayerInput.IsCtrlDown() && MapEntity.StartMovingPos == Vector2.Zero) { cam.MoveCamera((float) deltaTime, allowMove: false, allowZoom: GUI.MouseOn == null); @@ -5478,8 +5590,9 @@ namespace Barotrauma foreach (LightComponent lightComponent in item.GetComponents()) { lightComponent.Light.Color = - item.body == null || item.body.Enabled || - (item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems) ? + (item.body == null || item.body.Enabled || item.ParentInventory is ItemInventory { Container.HideItems: true }) && + /*the light is only visible when worn -> can't be visible in the editor*/ + lightComponent.Parent is not Wearable ? lightComponent.LightColor : Color.Transparent; lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 4d9def9a6..69298eef4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -2,10 +2,12 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Barotrauma.Items.Components; using Barotrauma.Extensions; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Barotrauma { @@ -24,6 +26,31 @@ namespace Barotrauma public static DateTime NextCommandPush; public static Tuple CommandBuffer; + private bool isReadonly; + public bool Readonly + { + get => isReadonly; + set + { + foreach (var component in Fields.SelectMany(f => f.Value)) + { + switch (component) + { + case GUINumberInput numInput: + numInput.Readonly = value; + break; + case GUITextBox textBox: + textBox.Readonly = value; + break; + default: + component.Enabled = !value; + break; + } + } + isReadonly = value; + } + } + private Action refresh; public int ContentHeight @@ -478,6 +505,7 @@ namespace Barotrauma GUITickBox propertyTickBox = new GUITickBox(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), displayName) { Font = GUIStyle.SmallFont, + Enabled = !Readonly, Selected = value, ToolTip = toolTip, OnSelected = (tickBox) => @@ -528,7 +556,8 @@ namespace Barotrauma var numberInput = new GUINumberInput(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), NumberType.Int) { ToolTip = toolTip, - Font = GUIStyle.SmallFont + Font = GUIStyle.SmallFont, + Readonly = Readonly }; numberInput.MinValueInt = editableAttribute.MinValueInt; numberInput.MaxValueInt = editableAttribute.MaxValueInt; @@ -572,7 +601,8 @@ namespace Barotrauma numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; numberInput.ValueStep = editableAttribute.ValueStep; - numberInput.ForceShowPlusMinusButtons = editableAttribute.ForceShowPlusMinusButtons; + numberInput.PlusMinusButtonVisibility = editableAttribute + .ForceShowPlusMinusButtons ? GUINumberInput.ButtonVisibility.ForceVisible : default; numberInput.FloatValue = value; numberInput.OnValueChanged += numInput => @@ -690,25 +720,31 @@ namespace Barotrauma public GUIComponent CreateStringField(ISerializableEntity entity, SerializableProperty property, string value, LocalizedString displayName, LocalizedString toolTip) { - var frame = new GUILayoutGroup(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), isHorizontal: true, childAnchor: Anchor.CenterLeft) + bool isItemTagBox = IsItemTagBox(entity, property.Name, out Item it); + var mainFrame = new GUILayoutGroup(new RectTransform(new Point(Rect.Width, isItemTagBox ? elementHeight * 2 : elementHeight), layoutGroup.RectTransform, isFixedSize: true)); + + var frame = new GUILayoutGroup(new RectTransform(isItemTagBox ? new Vector2(1f, 0.5f) : Vector2.One, mainFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont, textAlignment: Alignment.Left) { ToolTip = toolTip }; + Identifier translationTextTag = property.GetAttribute()?.TranslationTextTag ?? Identifier.Empty; - float browseButtonWidth = 0.1f; + const float browseButtonWidth = 0.1f; var editableAttribute = property.GetAttribute(); float textBoxWidth = inputFieldWidth; - if (!translationTextTag.IsEmpty) { textBoxWidth -= browseButtonWidth; } + if (!translationTextTag.IsEmpty || isItemTagBox) { textBoxWidth -= browseButtonWidth; } GUITextBox propertyBox = new GUITextBox(new RectTransform(new Vector2(textBoxWidth, 1), frame.RectTransform)) { Enabled = editableAttribute != null && !editableAttribute.ReadOnly, + Readonly = Readonly, ToolTip = toolTip, Font = GUIStyle.SmallFont, - Text = value, + Text = StripPrefabTags(value), OverflowClip = true }; @@ -725,7 +761,9 @@ namespace Barotrauma propertyBox.OnEnterPressed += (box, text) => OnApply(box); refresh += () => { - if (!propertyBox.Selected) { propertyBox.Text = property.GetValue(entity).ToString(); } + if (propertyBox.Selected) { return; } + + propertyBox.Text = StripPrefabTags(property.GetValue(entity).ToString()); }; bool OnApply(GUITextBox textBox) @@ -743,7 +781,7 @@ namespace Barotrauma if (SetPropertyValue(property, entity, textBox.Text)) { TrySendNetworkUpdate(entity, property); - textBox.Text = property.GetValue(entity).ToString(); + textBox.Text = StripPrefabTags(property.GetValue(entity).ToString()); textBox.Flash(GUIStyle.Green, flashDuration: 1f); } //restore the entities that were selected before applying @@ -778,9 +816,67 @@ namespace Barotrauma }; propertyBox.Text = value; } + + if (isItemTagBox) + { + // create prefab tag box + var prefabFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), mainFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), prefabFrame.RectTransform), TextManager.Get("predefinedtags.name"), font: GUIStyle.SmallFont, textAlignment: Alignment.Left) + { + ToolTip = TextManager.Get("predefinedtags.description") + }; + + new GUITextBox(new RectTransform(new Vector2(inputFieldWidth, 1), prefabFrame.RectTransform), createPenIcon: false) + { + Readonly = true, + Font = GUIStyle.SmallFont, + Text = GetPrefabTags(it), + OverflowClip = true, + ToolTip = TextManager.Get("predefinedtags.description") + }; + + // add container tag popup button to the modifiable tag box + new GUIButton(new RectTransform(new Vector2(browseButtonWidth, 1), frame.RectTransform, Anchor.TopRight), "...") + { + OnClicked = (_, _) => { it.CreateContainerTagPicker(propertyBox); return true; } + }; + } + frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { propertyBox }); } return frame; + + static bool IsItemTagBox(ISerializableEntity entity, string propertyName, [NotNullWhen(true)] out Item it) + { + if (entity is Item item && propertyName.Equals(nameof(Item.Tags), StringComparison.OrdinalIgnoreCase)) + { + it = item; + return true; + } + it = null; + return false; + } + + string StripPrefabTags(string text) + { + if (!isItemTagBox) { return text; } + + string prefabTags = GetPrefabTags(it); + if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(prefabTags)) { return text; } + + text = text.Remove(prefabTags); + if (text.StartsWith(",")) + { + text = text.Remove(0, 1); + } + return text; + } + + static string GetPrefabTags(Item it) => string.Join(',', it.Prefab.Tags); } public GUIComponent CreatePointField(ISerializableEntity entity, SerializableProperty property, Point value, LocalizedString displayName, LocalizedString toolTip) @@ -886,7 +982,8 @@ namespace Barotrauma numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; numberInput.ValueStep = editableAttribute.ValueStep; - numberInput.ForceShowPlusMinusButtons = editableAttribute.ForceShowPlusMinusButtons; + numberInput.PlusMinusButtonVisibility = editableAttribute + .ForceShowPlusMinusButtons ? GUINumberInput.ButtonVisibility.ForceVisible : default; numberInput.FloatValue = i == 0 ? value.X : value.Y; @@ -1275,7 +1372,11 @@ namespace Barotrauma // Set the label to be (i + 1) so it's easier to understand for non-programmers string componentLabel = (i + 1).ToString(); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), elementLayoutGroup.RectTransform) { MaxSize = new Point(25, elementLayoutGroup.Rect.Height) }, componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); - GUITextBox textBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), elementLayoutGroup.RectTransform), text: value[i]) { Font = GUIStyle.SmallFont }; + GUITextBox textBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), elementLayoutGroup.RectTransform), text: value[i]) + { + Font = GUIStyle.SmallFont, + Readonly = Readonly + }; int comp = i; textBox.OnEnterPressed += (textBox, text) => OnApply(textBox); textBox.OnDeselected += (textBox, keys) => OnApply(textBox); @@ -1387,7 +1488,7 @@ namespace Barotrauma private bool SetPropertyValue(SerializableProperty property, object entity, object value) { - if (LockEditing || IsEntityRemoved(entity)) { return false; } + if (LockEditing || IsEntityRemoved(entity) || Readonly) { return false; } object oldData = property.GetValue(entity); // some properties have null as the default string value diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index f8c7eacf5..0e5e54d1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -270,6 +270,7 @@ namespace Barotrauma Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, v => unsavedConfig.Graphics.VSync = v); Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, v => unsavedConfig.Graphics.CompressTextures = v); + Spacer(right); Label(right, TextManager.Get("LOSEffect"), GUIStyle.SubHeadingFont); DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, v => unsavedConfig.Graphics.LosMode = v); @@ -683,22 +684,22 @@ namespace Barotrauma private void CreateGameplayTab() { GUIFrame content = CreateNewContentFrame(Tab.Gameplay); - - var (left, right) = CreateSidebars(content); + + var (leftColumn, rightColumn) = CreateSidebars(content, split: true); var languages = TextManager.AvailableLanguages .OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier()) .ToArray(); - Label(left, TextManager.Get("Language"), GUIStyle.SubHeadingFont); - Dropdown(left, v => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, v => unsavedConfig.Language = v); - Spacer(left); - - Tickbox(left, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, v => unsavedConfig.PauseOnFocusLost = v); - Spacer(left); - - Tickbox(left, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, v => unsavedConfig.DisableInGameHints = v); + Label(leftColumn, TextManager.Get("Language"), GUIStyle.SubHeadingFont); + Dropdown(leftColumn, v => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, v => unsavedConfig.Language = v); + Spacer(leftColumn); + + Tickbox(leftColumn, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, v => unsavedConfig.PauseOnFocusLost = v); + Spacer(leftColumn); + + Tickbox(leftColumn, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, v => unsavedConfig.DisableInGameHints = v); var resetInGameHintsButton = - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), left.RectTransform), + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), leftColumn.RectTransform), TextManager.Get("ResetInGameHints"), style: "GUIButtonSmall") { OnClicked = (button, o) => @@ -716,22 +717,41 @@ namespace Barotrauma return false; } }; - Spacer(left); - Label(left, TextManager.Get("ShowEnemyHealthBars"), GUIStyle.SubHeadingFont); - DropdownEnum(left, v => TextManager.Get($"ShowEnemyHealthBars.{v}"), null, unsavedConfig.ShowEnemyHealthBars, v => unsavedConfig.ShowEnemyHealthBars = v); - Spacer(left); + Spacer(leftColumn); - Label(left, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); - Slider(left, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, v => unsavedConfig.Graphics.HUDScale = v); - Label(left, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); - Slider(left, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); - Label(left, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); - Slider(left, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); + Tickbox(leftColumn, TextManager.Get("ChatSpeechBubbles"), TextManager.Get("ChatSpeechBubbles.Tooltip"), unsavedConfig.ChatSpeechBubbles, v => unsavedConfig.ChatSpeechBubbles = v); + Label(leftColumn, TextManager.Get("ShowEnemyHealthBars"), GUIStyle.SubHeadingFont); + DropdownEnum(leftColumn, v => TextManager.Get($"ShowEnemyHealthBars.{v}"), null, unsavedConfig.ShowEnemyHealthBars, v => unsavedConfig.ShowEnemyHealthBars = v); + Spacer(leftColumn); + Label(leftColumn, TextManager.Get("InteractionLabels"), GUIStyle.SubHeadingFont); + DropdownEnum(leftColumn, v => TextManager.Get($"InteractionLabels.{v}"), null, unsavedConfig.InteractionLabelDisplayMode, v => unsavedConfig.InteractionLabelDisplayMode = v); + + Label(rightColumn, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); + Slider(rightColumn, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, v => unsavedConfig.Graphics.HUDScale = v); + Label(rightColumn, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); + Slider(rightColumn, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); + Label(rightColumn, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); + Slider(rightColumn, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); + Spacer(rightColumn); + var resetSpamListFilter = + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform), + TextManager.Get("clearserverlistfilters"), style: "GUIButtonSmall") + { + OnClicked = static (_, _) => + { + GUI.AskForConfirmation( + header: TextManager.Get("clearserverlistfilters"), + body: TextManager.Get("clearserverlistfiltersconfirmation"), + onConfirm: SpamServerFilters.ClearLocalSpamFilter); + return true; + } + }; + Spacer(rightColumn); #if !OSX - Spacer(right); - var statisticsTickBox = new GUITickBox(NewItemRectT(right), TextManager.Get("statisticsconsenttickbox")) + Spacer(rightColumn); + var statisticsTickBox = new GUITickBox(NewItemRectT(rightColumn), TextManager.Get("statisticsconsenttickbox")) { OnSelected = tickBox => { @@ -780,7 +800,7 @@ namespace Barotrauma if (SteamManager.IsInitialized) { bool shouldCrossplayBeEnabled = unsavedConfig.CrossplayChoice is Eos.EosSteamPrimaryLogin.CrossplayChoice.Enabled; - var crossplayTickBox = Tickbox(right, TextManager.Get("EosAllowCrossplay"), TextManager.Get("EosAllowCrossplayTooltip"), shouldCrossplayBeEnabled, v => + var crossplayTickBox = Tickbox(rightColumn, TextManager.Get("EosAllowCrossplay"), TextManager.Get("EosAllowCrossplayTooltip"), shouldCrossplayBeEnabled, v => { unsavedConfig.CrossplayChoice = v ? Eos.EosSteamPrimaryLogin.CrossplayChoice.Enabled @@ -792,21 +812,6 @@ namespace Barotrauma crossplayTickBox.ToolTip = TextManager.Get("CantAccessEOSSettingsInMP"); } } - - Spacer(right); - var resetSpamListFilter = - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), right.RectTransform), - TextManager.Get("clearserverlistfilters"), style: "GUIButtonSmall") - { - OnClicked = static (_, _) => - { - GUI.AskForConfirmation( - header: TextManager.Get("clearserverlistfilters"), - body: TextManager.Get("clearserverlistfiltersconfirmation"), - onConfirm: SpamServerFilters.ClearLocalSpamFilter); - return true; - } - }; } private void CreateModsTab(out WorkshopMenu workshopMenu) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index d050e09c4..f5145ff10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -31,6 +31,7 @@ namespace Barotrauma.Sounds return; } + Loading = true; TaskPool.Add( $"LoadSamples {filename}", LoadSamples(reader), @@ -46,6 +47,7 @@ namespace Barotrauma.Sounds playbackAmplitude = result.PlaybackAmplitude; Owner.KillChannels(this); // prevents INVALID_OPERATION error buffers?.Dispose(); buffers = null; + Loading = false; }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index 8c815b9c0..87c38b8de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -24,6 +24,8 @@ namespace Barotrauma.Sounds public readonly bool StreamsReliably; + public bool Loading { get; protected set; } + private readonly SoundManager.SourcePoolIndex sourcePoolIndex = SoundManager.SourcePoolIndex.Default; public virtual SoundManager.SourcePoolIndex SourcePoolIndex { @@ -84,18 +86,34 @@ namespace Barotrauma.Sounds return Owner.IsPlaying(this); } + public bool LogWarningIfStillLoading() + { + if (Loading) + { + if (Level.Loaded is not { Generating: true }) + { + DebugConsole.AddWarning($"Attempted to play the sound {this} while it was still loading."); + } + return true; + } + return false; + } + public virtual SoundChannel Play(float gain, float range, Vector2 position, bool muffle = false) { + LogWarningIfStillLoading(); return new SoundChannel(this, gain, new Vector3(position.X, position.Y, 0.0f), 1.0f, range * 0.4f, range, "default", muffle); } public virtual SoundChannel Play(float gain, float range, float freqMult, Vector2 position, bool muffle = false) { + LogWarningIfStillLoading(); return new SoundChannel(this, gain, new Vector3(position.X, position.Y, 0.0f), freqMult, range * 0.4f, range, "default", muffle); } public virtual SoundChannel Play(Vector3? position, float gain, float freqMult = 1.0f, bool muffle = false) { + LogWarningIfStillLoading(); return new SoundChannel(this, gain, position, freqMult, BaseNear, BaseFar, "default", muffle); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 08e9811ce..d90705fdb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -262,6 +262,9 @@ namespace Barotrauma.Sounds } } + public const float MinFrequencyMultiplier = 0.25f; + public const float MaxFrequencyMultiplier = 4.0f; + public float frequencyMultiplier; public float FrequencyMultiplier { @@ -271,11 +274,11 @@ namespace Barotrauma.Sounds } set { - if (value < 0.25f || value > 4.0f) + if (value is < MinFrequencyMultiplier or > MaxFrequencyMultiplier) { DebugConsole.ThrowError($"Frequency multiplier out of range: {value}" + Environment.StackTrace.CleanupStackTrace()); } - frequencyMultiplier = Math.Clamp(value, 0.25f, 4.0f); + frequencyMultiplier = Math.Clamp(value, MinFrequencyMultiplier, MaxFrequencyMultiplier); if (ALSourceIndex < 0) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index c9a06e685..1c55f6070 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Sounds listenerPosition = value; Al.Listener3f(Al.Position,value.X,value.Y,value.Z); int alError = Al.GetError(); - if (alError != Al.NoError) + if (alError != Al.NoError && !GameMain.IsExiting) { throw new Exception("Failed to set listener position: " + Al.GetErrorString(alError)); } @@ -68,7 +68,7 @@ namespace Barotrauma.Sounds listenerOrientation[0] = value.X; listenerOrientation[1] = value.Y; listenerOrientation[2] = value.Z; Al.Listenerfv(Al.Orientation, listenerOrientation); int alError = Al.GetError(); - if (alError != Al.NoError) + if (alError != Al.NoError && !GameMain.IsExiting) { throw new Exception("Failed to set listener target vector: " + Al.GetErrorString(alError)); } @@ -83,7 +83,7 @@ namespace Barotrauma.Sounds listenerOrientation[3] = value.X; listenerOrientation[4] = value.Y; listenerOrientation[5] = value.Z; Al.Listenerfv(Al.Orientation, listenerOrientation); int alError = Al.GetError(); - if (alError != Al.NoError) + if (alError != Al.NoError && !GameMain.IsExiting) { throw new Exception("Failed to set listener up vector: " + Al.GetErrorString(alError)); } @@ -101,7 +101,7 @@ namespace Barotrauma.Sounds listenerGain = value; Al.Listenerf(Al.Gain, listenerGain); int alError = Al.GetError(); - if (alError != Al.NoError) + if (alError != Al.NoError && !GameMain.IsExiting) { throw new Exception("Failed to set listener gain: " + Al.GetErrorString(alError)); } @@ -860,14 +860,14 @@ namespace Barotrauma.Sounds ReleaseResources(false); - if (!Alc.MakeContextCurrent(IntPtr.Zero)) + if (!Alc.MakeContextCurrent(IntPtr.Zero) && !GameMain.IsExiting) { throw new Exception("Failed to detach the current ALC context! (error code: " + Alc.GetError(alcDevice).ToString() + ")"); } Alc.DestroyContext(alcContext); - if (!Alc.CloseDevice(alcDevice)) + if (!Alc.CloseDevice(alcDevice) && !GameMain.IsExiting) { throw new Exception("Failed to close ALC device!"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 3be4a5571..eaee24b70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -632,7 +632,8 @@ namespace Barotrauma } IEnumerable suitableIntensityMusic = Enumerable.Empty(); - if (targetMusic[mainTrackIndex] is { MuteIntensityTracks: false } mainTrack && Screen.Selected == GameMain.GameScreen) + BackgroundMusic mainTrack = targetMusic[mainTrackIndex]; + if (mainTrack is not { MuteIntensityTracks: true } && Screen.Selected == GameMain.GameScreen) { float intensity = currentIntensity; if (mainTrack?.ForceIntensityTrack != null) @@ -770,11 +771,16 @@ namespace Barotrauma private static IEnumerable GetSuitableMusicClips(Identifier musicType, float currentIntensity) { - return musicClips.Where(music => - music != null && - music.Type == musicType && + return musicClips.Where(music => IsSuitableMusicClip(music, musicType, currentIntensity)); + } + + private static bool IsSuitableMusicClip(BackgroundMusic music, Identifier musicType, float currentIntensity) + { + return + music != null && + music.Type == musicType && currentIntensity >= music.IntensityRange.X && - currentIntensity <= music.IntensityRange.Y); + currentIntensity <= music.IntensityRange.Y; } private static Identifier GetCurrentMusicType() @@ -858,35 +864,42 @@ namespace Barotrauma if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) { return "flooded".ToIdentifier(); } } - - float enemyDistThreshold = 5000.0f; - if (targetSubmarine != null) + float intensity = (GameMain.GameSession?.EventManager?.MusicIntensity ?? 0) * 100.0f; + bool anyMonsterMusicAvailable = + musicClips.Any(m => IsSuitableMusicClip(m, "monster".ToIdentifier(), intensity) || IsSuitableMusicClip(m, "monsterambience".ToIdentifier(), intensity)); + + if (anyMonsterMusicAvailable) { - enemyDistThreshold = Math.Max(enemyDistThreshold, Math.Max(targetSubmarine.Borders.Width, targetSubmarine.Borders.Height) * 2.0f); - } - - foreach (Character character in Character.CharacterList) - { - if (character.IsDead || !character.Enabled) continue; - if (!(character.AIController is EnemyAIController enemyAI) || !enemyAI.Enabled || (!enemyAI.AttackHumans && !enemyAI.AttackRooms)) { continue; } - + float enemyDistThreshold = 5000.0f; if (targetSubmarine != null) { - if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < enemyDistThreshold * enemyDistThreshold) - { - return "monster".ToIdentifier(); - } + enemyDistThreshold = Math.Max(enemyDistThreshold, Math.Max(targetSubmarine.Borders.Width, targetSubmarine.Borders.Height) * 2.0f); } - else if (Character.Controlled != null) + foreach (Character character in Character.CharacterList) { - if (Vector2.DistanceSquared(character.WorldPosition, Character.Controlled.WorldPosition) < enemyDistThreshold * enemyDistThreshold) + if (character.IsDead || !character.Enabled) { continue; } + if (character.AIController is not EnemyAIController { Enabled: true } enemyAI) { continue; } + if (!enemyAI.AttackHumans && !enemyAI.AttackRooms) { continue; } + + if (targetSubmarine != null) { - return "monster".ToIdentifier(); + if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < enemyDistThreshold * enemyDistThreshold) + { + return "monster".ToIdentifier(); + } + } + else if (Character.Controlled != null) + { + if (Vector2.DistanceSquared(character.WorldPosition, Character.Controlled.WorldPosition) < enemyDistThreshold * enemyDistThreshold) + { + return "monster".ToIdentifier(); + } } } } + if (GameMain.GameSession != null) { if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub != null && Submarine.MainSub.AtEndExit) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index 8e84334b9..8bc574c86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -34,6 +34,8 @@ namespace Barotrauma.Sounds public bool UseRadioFilter; public bool UseMuffleFilter; + public bool UsingRadio; + public float Near { get; private set; } public float Far { get; private set; } @@ -55,7 +57,7 @@ namespace Barotrauma.Sounds { if (soundChannel == null) { return; } gain = value; - soundChannel.Gain = value * GameSettings.CurrentConfig.Audio.VoiceChatVolume; + soundChannel.Gain = value * GameSettings.CurrentConfig.Audio.VoiceChatVolume * client.VoiceVolume; } } @@ -64,8 +66,11 @@ namespace Barotrauma.Sounds get { return soundChannel?.CurrentAmplitude ?? 0.0f; } } - public VoipSound(string name, SoundManager owner, VoipQueue q) : base(owner, $"VoIP ({name})", true, true, getFullPath: false) + private Client client; + + public VoipSound(Client targetClient, SoundManager owner, VoipQueue q) : base(owner, $"VoIP ({targetClient.Name})", true, true, getFullPath: false) { + client = targetClient; decoder = VoipConfig.CreateDecoder(); ALFormat = Al.FormatMono16; @@ -99,7 +104,7 @@ namespace Barotrauma.Sounds public void ApplyFilters(short[] buffer, int readSamples) { - float finalGain = gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume; + float finalGain = gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume * client.VoiceVolume; for (int i = 0; i < readSamples; i++) { float fVal = ShortToFloat(buffer[i]); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index c1c8efe3f..bbd74e417 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -30,6 +30,11 @@ namespace Barotrauma Noise } + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] + public float BlinkFrequency { get; private set; } + + private float blinkTimer = 0.0f; + [Serialize("0,0", IsPropertySaveable.Yes), Editable] public Vector2 Offset { get; private set; } @@ -242,7 +247,16 @@ namespace Barotrauma } } if (!spriteState.IsActive) { continue; } - + if (decorativeSprite.BlinkFrequency > 0.0f) + { + decorativeSprite.blinkTimer += deltaTime * decorativeSprite.BlinkFrequency; + decorativeSprite.blinkTimer %= 1.0f; + if (decorativeSprite.blinkTimer > 0.5f) + { + spriteState.IsActive = false; + continue; + } + } //check if the sprite should be animated bool animate = true; foreach (PropertyConditional conditional in decorativeSprite.AnimationConditionals) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 94f4d5547..c253984c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -240,7 +240,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Sprite \"{file}\" not found!"); + DebugConsole.ThrowError($"Sprite \"{file}\" not found!", contentPackage: contentPackage); DebugConsole.Log(Environment.StackTrace.CleanupStackTrace()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 3eb48d01a..e74ee34bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -29,6 +29,8 @@ namespace Barotrauma /// private bool forcePlaySounds; + private CoroutineHandle playSoundAfterLoadedCoroutine; + partial void InitProjSpecific(ContentXElement element, string parentDebugName) { particleEmitters = new List(); @@ -150,9 +152,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hullGuess: hull, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier()); - ignoreMuffling = sound.IgnoreMuffling; - if (soundChannel != null) { soundChannel.Looping = loopSound; } + PlaySoundOrDelayIfNotLoaded(sound); } } else @@ -177,9 +177,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling, freqMult: selectedSound.GetRandomFrequencyMultiplier()); - ignoreMuffling = selectedSound.IgnoreMuffling; - if (soundChannel != null) { soundChannel.Looping = loopSound; } + PlaySoundOrDelayIfNotLoaded(selectedSound); } } else @@ -193,6 +191,46 @@ namespace Barotrauma soundEmitter = entity; loopStartTime = Timing.TotalTime; } + + void PlaySoundOrDelayIfNotLoaded(RoundSound selectedSound) + { + if (playSoundAfterLoadedCoroutine != null) { return; } + if (selectedSound.Sound.Loading) + { + playSoundAfterLoadedCoroutine = CoroutineManager.StartCoroutine(PlaySoundAfterLoaded(selectedSound)); + } + else + { + PlaySound(selectedSound); + } + } + + IEnumerable PlaySoundAfterLoaded(RoundSound selectedSound) + { + float maxWaitTimer = 1.0f; + while (selectedSound.Sound.Loading && maxWaitTimer > 0.0f) + { + maxWaitTimer -= CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + if (!selectedSound.Sound.Loading) + { + PlaySound(selectedSound); + } + yield return CoroutineStatus.Success; + } + + void PlaySound(RoundSound selectedSound) + { + //if the sound loops, we must make sure the existing channel + System.Diagnostics.Debug.Assert( + soundChannel == null || !soundChannel.IsPlaying || soundChannel.FadingOutAndDisposing || !soundChannel.Looping, + "A StatusEffect attempted to play a sound, but an looping sound is already playing. The looping sound should be stopped before playing a new one, or it will keep looping indefinitely."); + + soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling, freqMult: selectedSound.GetRandomFrequencyMultiplier()); + ignoreMuffling = selectedSound.IgnoreMuffling; + if (soundChannel != null) { soundChannel.Looping = loopSound; } + } } static partial void UpdateAllProjSpecific(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index 8c9a12c07..ca2bd8eb5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -143,7 +143,10 @@ namespace Barotrauma.Steam { OnClicked = (b, _) => { - SelectTab(tab); + if (tab != CurrentTab) + { + SelectTab(tab); + } return false; } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index fe3bc6a7b..b2f6731d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -386,7 +386,7 @@ namespace Barotrauma.Steam private IEnumerable MessageBoxCoroutine(Func> subcoroutine) { - var messageBox = new GUIMessageBox("", TextManager.Get("ellipsis"), buttons: new [] { TextManager.Get("Cancel") }); + var messageBox = new GUIMessageBox("", TextManager.Get("ellipsis").Fallback("..."), buttons: new [] { TextManager.Get("Cancel") }); messageBox.Buttons[0].OnClicked = (button, o) => { messageBox.Close(); @@ -494,7 +494,8 @@ namespace Barotrauma.Steam stagingReady = true; stagingException = t.Exception?.GetInnermost(); }); - currentStepText.Text = TextManager.Get("PublishPopupStaging"); + TrySetText("PublishPopupStaging"); + while (!stagingReady) { yield return new WaitForSeconds(0.5f); } if (stagingException != null) @@ -519,7 +520,7 @@ namespace Barotrauma.Steam } resultException = t.Exception?.GetInnermost(); }); - currentStepText.Text = TextManager.Get("PublishPopupSubmit"); + TrySetText("PublishPopupSubmit"); while (!result.HasValue && resultException is null) { yield return new WaitForSeconds(0.5f); } if (result is { Success: true }) @@ -567,7 +568,7 @@ namespace Barotrauma.Steam }); while (!installed) { - currentStepText.Text = TextManager.Get("PublishPopupInstall"); + TrySetText("PublishPopupInstall"); yield return new WaitForSeconds(0.5f); } @@ -601,6 +602,14 @@ namespace Barotrauma.Steam SteamManager.Workshop.DeletePublishStagingCopy(); messageBox.Close(); + + void TrySetText(string textTag) + { + if (currentStepText?.Text != null) + { + currentStepText.Text = TextManager.Get(textTag); + } + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs index 50a3f5f90..b47221c6d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs @@ -112,7 +112,7 @@ namespace Barotrauma internal class AddOrDeleteCommand : Command { private readonly Dictionary PreviousInventories = new Dictionary(); - private readonly List Receivers; + public readonly List Receivers; private readonly List CloneList; private readonly bool WasDeleted; private readonly List ContainedItemsCommand = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index 047dce5cd..c17369cd4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Text; using Barotrauma.Networking; using Color = Microsoft.Xna.Framework.Color; @@ -422,6 +423,38 @@ namespace Barotrauma return str; } + /// + /// Removes lines on a multi-line string until it fits within the specified height and adds "..." to the end if the string is too long. + /// Doesn't really do anything if the string is only one line, should mostly be used with . + /// + public static string LimitStringHeight(string str, ScalableFont font, int maxHeight) + { + if (maxHeight <= 0 || string.IsNullOrWhiteSpace(str)) { return string.Empty; } + + float currHeight = font.MeasureString("...").Y; + var lines = str.Split('\n'); + + var sb = new StringBuilder(); + foreach (string line in lines) + { + var (lineX, lineY) = font.MeasureString(line); + currHeight += lineY; + if (currHeight > maxHeight) + { + var modifiedLine = line; + while (font.MeasureString($"{modifiedLine}...").X > lineX) + { + modifiedLine = modifiedLine[..^1]; + } + sb.AppendLine($"{modifiedLine}..."); + return sb.ToString(); + } + sb.AppendLine(line); + } + + return str; + } + public static Color GradientLerp(float t, params Color[] gradient) { if (!MathUtils.IsValid(t)) { return Color.Purple; } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index f685198c3..dd2bdf7fc 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index c71af10d6..61c9542cc 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 0608b7d9d..ea9773d05 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index a39539d07..c27e3e704 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index dd5b22619..1052b103a 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 4002d6ebe..50fa97fdb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -16,7 +16,7 @@ namespace Barotrauma public void ApplyDeathEffects() { - RespawnManager.ReduceCharacterSkills(this); + RespawnManager.ReduceCharacterSkillsOnDeath(this); RemoveSavedStatValuesOnDeath(); CauseOfDeath = null; } @@ -68,8 +68,7 @@ namespace Barotrauma msg.WriteColorR8G8B8(Head.SkinColor); msg.WriteColorR8G8B8(Head.HairColor); msg.WriteColorR8G8B8(Head.FacialHairColor); - - msg.WriteString(ragdollFileName); + msg.WriteIdentifier(HumanPrefabIds.NpcIdentifier); msg.WriteIdentifier(MinReputationToHire.factionId); if (!MinReputationToHire.factionId.IsEmpty) @@ -80,11 +79,13 @@ namespace Barotrauma { msg.WriteUInt32(Job.Prefab.UintIdentifier); msg.WriteByte((byte)Job.Variant); - var skills = Job.Prefab.Skills.OrderBy(s => s.Identifier); + + var skills = Job.GetSkills().OrderBy(s => s.Identifier); msg.WriteByte((byte)skills.Count()); - foreach (SkillPrefab skillPrefab in skills) + foreach (var skill in skills) { - msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier)?.Level ?? 0.0f); + msg.WriteIdentifier(skill.Identifier); + msg.WriteSingle(skill.Level); } } else diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index d61c718ca..4e1ef65eb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -75,7 +75,7 @@ namespace Barotrauma NetConfig.HighPrioCharacterPositionUpdateInterval, priority); - if (IsDead) + if (IsDead && !AnimController.IsDraggedWithRope) { interval = Math.Max(interval * 2, 0.1f); } @@ -590,6 +590,31 @@ namespace Barotrauma } } break; + case LatchedOntoTargetEventData latchedOntoTargetEventData: + msg.WriteBoolean(latchedOntoTargetEventData.IsLatched); + if (latchedOntoTargetEventData.IsLatched) + { + msg.WriteSingle(SimPosition.X); + msg.WriteSingle(SimPosition.Y); + msg.WriteSingle(latchedOntoTargetEventData.AttachSurfaceNormal.X); + msg.WriteSingle(latchedOntoTargetEventData.AttachSurfaceNormal.Y); + msg.WriteSingle(latchedOntoTargetEventData.AttachPos.X); + msg.WriteSingle(latchedOntoTargetEventData.AttachPos.Y); + msg.WriteInt32(latchedOntoTargetEventData.TargetLevelWallIndex); + if (latchedOntoTargetEventData.TargetStructureID != NullEntityID) + { + msg.WriteUInt16(latchedOntoTargetEventData.TargetStructureID); + } + else if (latchedOntoTargetEventData.TargetCharacterID != NullEntityID) + { + msg.WriteUInt16(latchedOntoTargetEventData.TargetCharacterID); + } + else + { + msg.WriteUInt16(NullEntityID); + } + } + break; default: throw new Exception($"Malformed character event: did not expect {eventData.GetType().Name}"); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 21758979c..d1c337634 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1185,7 +1185,9 @@ namespace Barotrauma if (GameMain.Server == null) { return; } GameMain.Server.ServerSettings.SetPassword(args.Length > 0 ? args[0] : ""); NewMessage(client.Name + " " + (GameMain.Server.ServerSettings.HasPassword ? " changed the server password to \"" + args[0] + "\"." : " removed password protection from the server.")); - GameMain.Server.SendConsoleMessage(GameMain.Server.ServerSettings.HasPassword ? "Changed the server password." : "Removed password protection from the server.", client); + GameMain.Server.SendChatMessage( + TextManager.GetWithVariable(GameMain.Server.ServerSettings.HasPassword ? "PasswordChangedByClient" : "PasswordRemovedByClient", "[clientname]", client.Name).Value, + ChatMessageType.Server); }); commands.Add(new Command("setmaxplayers|maxplayers", "setmaxplayers [max players]: Sets the maximum player count of the server that's being hosted.", (string[] args) => @@ -1278,12 +1280,11 @@ namespace Barotrauma commands.Add(new Command("servername", "servername [name]: Change the name of the server.", (string[] args) => { GameMain.Server.ServerName = string.Join(" ", args); - GameMain.NetLobbyScreen.ChangeServerName(string.Join(" ", args)); })); commands.Add(new Command("servermsg", "servermsg [message]: Change the message displayed in the server lobby.", (string[] args) => { - GameMain.NetLobbyScreen.ChangeServerMessage(string.Join(" ", args)); + GameMain.Server.ServerSettings.ServerMessageText = string.Join(" ", args); })); commands.Add(new Command("seed|levelseed", "seed/levelseed: Changes the level seed for the next round.", (string[] args) => diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 9653e372e..cf7fdf067 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -48,6 +48,7 @@ namespace Barotrauma spawnInfo[target].OriginalInventoryID, spawnInfo[target].OriginalItemContainerIndex, spawnInfo[target].OriginalSlotIndex); + msg.WriteUInt16(target.ParentTarget?.Item?.ID ?? Entity.NullEntityID); } msg.WriteByte((byte)spawnInfo[target].ExecutedEffectIndices.Count); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 0081b16f2..abeeeb1df 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -470,6 +470,11 @@ namespace Barotrauma return characterData.Find(cd => cd.MatchesClient(client)); } + public CharacterCampaignData GetCharacterData(CharacterInfo characterInfo) + { + return characterData.Find(cd => cd.CharacterInfo == characterInfo); + } + public CharacterCampaignData SetClientCharacterData(Client client) { characterData.RemoveAll(cd => cd.MatchesClient(client)); @@ -970,7 +975,7 @@ namespace Barotrauma { int desiredQuantity = purchasedItem.Quantity; if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchasedList) && - alreadyPurchasedList.FirstOrDefault(p => p.ItemPrefab == purchasedItem.ItemPrefab) is { } alreadyPurchased) + alreadyPurchasedList.FirstOrDefault(p => p.ItemPrefab == purchasedItem.ItemPrefab && p.DeliverImmediately == purchasedItem.DeliverImmediately) is { } alreadyPurchased) { desiredQuantity -= alreadyPurchased.Quantity; } @@ -1198,14 +1203,13 @@ namespace Barotrauma if (fireCharacter) { firedIdentifier = msg.ReadInt32(); } Location location = map?.CurrentLocation; - List hiredCharacters = new List(); CharacterInfo firedCharacter = null; if (location != null && AllowedToManageCampaign(sender, ClientPermissions.ManageHires)) { if (fireCharacter) { - firedCharacter = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); + firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); if (firedCharacter != null && (firedCharacter.Character?.IsBot ?? true)) { CrewManager.FireCharacter(firedCharacter); @@ -1221,7 +1225,7 @@ namespace Barotrauma CharacterInfo characterInfo = null; if (existingCrewMember && CrewManager != null) { - characterInfo = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); + characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); } else if(!existingCrewMember && location.HireManager != null) { @@ -1251,10 +1255,7 @@ namespace Barotrauma { foreach (CharacterInfo hireInfo in location.HireManager.PendingHires) { - if (TryHireCharacter(location, hireInfo, sender.Character, sender)) - { - hiredCharacters.Add(hireInfo); - } + TryHireCharacter(location, hireInfo, client: sender); } } @@ -1271,7 +1272,7 @@ namespace Barotrauma } pendingHireInfos.Add(match); - if (pendingHireInfos.Count + CrewManager.CharacterInfos.Count() >= CrewManager.MaxCrewSize) + if (pendingHireInfos.Count + CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize) { break; } @@ -1281,7 +1282,7 @@ namespace Barotrauma location.HireManager.AvailableCharacters.ForEachMod(info => { - if(!location.HireManager.PendingHires.Contains(info)) + if (!location.HireManager.PendingHires.Contains(info)) { location.HireManager.RenameCharacter(info, info.OriginalName); } @@ -1292,11 +1293,11 @@ namespace Barotrauma // bounce back if (renameCharacter && existingCrewMember) { - SendCrewState(hiredCharacters, (renamedIdentifier, newName), firedCharacter); + SendCrewState((renamedIdentifier, newName), firedCharacter); } else { - SendCrewState(hiredCharacters, default, firedCharacter); + SendCrewState(firedCharacter: firedCharacter); } } @@ -1310,7 +1311,7 @@ namespace Barotrauma /// the client and the server when there's only one person on the server but when a second person joins both of /// their available hires are different from the server. /// - public void SendCrewState(List hiredCharacters, (int id, string newName) renamedCrewMember, CharacterInfo firedCharacter) + public void SendCrewState((int id, string newName) renamedCrewMember = default, CharacterInfo firedCharacter = null) { List availableHires = new List(); List pendingHires = new List(); @@ -1332,21 +1333,19 @@ namespace Barotrauma hire.ServerWrite(msg); msg.WriteInt32(hire.Salary); } - + msg.WriteUInt16((ushort)pendingHires.Count); foreach (CharacterInfo pendingHire in pendingHires) { msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName()); } - msg.WriteUInt16((ushort)(hiredCharacters?.Count ?? 0)); - if(hiredCharacters != null) + var hiredCharacters = CrewManager.GetCharacterInfos().Where(ci => ci.IsNewHire); + msg.WriteUInt16((ushort)hiredCharacters.Count()); + foreach (CharacterInfo info in hiredCharacters) { - foreach (CharacterInfo info in hiredCharacters) - { - info.ServerWrite(msg); - msg.WriteInt32(info.Salary); - } + info.ServerWrite(msg); + msg.WriteInt32(info.Salary); } bool validRenaming = renamedCrewMember.id > -1 && !string.IsNullOrEmpty(renamedCrewMember.newName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs index 6f980eab7..cc154a863 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs @@ -27,6 +27,9 @@ namespace Barotrauma.Items.Components //opening a partially stuck door makes it less stuck if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } + ActionType actionType = open ? ActionType.OnOpen : ActionType.OnClose; + item.ApplyStatusEffects(actionType, deltaTime: 1.0f); + if (sendNetworkMessage) { item.CreateServerEvent(this, new EventData(forcedOpen)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs index 11342ebed..a322888cb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs @@ -11,20 +11,20 @@ namespace Barotrauma.Items.Components if (!Snapped) { msg.WriteUInt16(target?.ID ?? Entity.NullEntityID); - if (source is Entity entity && !entity.Removed) + switch (source) { - msg.WriteUInt16(entity?.ID ?? Entity.NullEntityID); - msg.WriteByte((byte)0); - } - else if (source is Limb limb && limb.character != null && !limb.character.Removed) - { - msg.WriteUInt16(limb.character?.ID ?? Entity.NullEntityID); - msg.WriteByte((byte)limb.character.AnimController.Limbs.IndexOf(limb)); - } - else - { - msg.WriteUInt16(Entity.NullEntityID); - msg.WriteByte((byte)0); + case Entity { Removed: false } entity: + msg.WriteUInt16(entity.ID); + msg.WriteByte((byte)0); + break; + case Limb { character.Removed: false } limb: + msg.WriteUInt16(limb.character.ID); + msg.WriteByte((byte)limb.character.AnimController.Limbs.IndexOf(limb)); + break; + default: + msg.WriteUInt16(Entity.NullEntityID); + msg.WriteByte((byte)0); + break; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs index b0b24e75c..25d808849 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -100,7 +100,7 @@ namespace Barotrauma.Items.Components return (msg, deliveryMethod); } - public void CreateServerEvent(INetSerializableStruct data) + public void CreateServerEvent(INetSerializableStruct data) => item.CreateServerEvent(this, new CircuitBoxEventData(data)); public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null) @@ -120,7 +120,7 @@ namespace Barotrauma.Items.Components case CircuitBoxOpcode.AddComponent: { var data = INetSerializableStruct.Read(msg); - if (!item.CanClientAccess(c)) { break; } + if (!CanAccessAndUnlocked(c)) { break; } var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.PrefabIdentifier); if (prefab is null) @@ -158,14 +158,14 @@ namespace Barotrauma.Items.Components var data = INetSerializableStruct.Read(msg); if (!item.CanClientAccess(c)) { break; } - MoveNodesInternal(data.TargetIDs, data.IOs, data.MoveAmount); + MoveNodesInternal(data.TargetIDs, data.IOs, data.LabelIDs, data.MoveAmount); CreateServerEvent(data); break; } case CircuitBoxOpcode.DeleteComponent: { var data = INetSerializableStruct.Read(msg); - if (!data.TargetIDs.Any() || !item.CanClientAccess(c)) { break; } + if (!data.TargetIDs.Any() || !CanAccessAndUnlocked(c)) { break; } CreateRefundItemsForUsedResources(data.TargetIDs, c.Character); GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogComponentName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring); @@ -180,6 +180,7 @@ namespace Barotrauma.Items.Components SelectComponentsInternal(data.TargetIDs, c.CharacterID, data.Overwrite); SelectInputOutputInternal(data.IOs, c.CharacterID, data.Overwrite); + SelectLabelsInternal(data.LabelIDs, c.CharacterID, data.Overwrite); BroadcastSelectionStatus(); break; } @@ -195,7 +196,7 @@ namespace Barotrauma.Items.Components case CircuitBoxOpcode.AddWire: { var data = INetSerializableStruct.Read(msg); - if (!item.CanClientAccess(c)) { break; } + if (!CanAccessAndUnlocked(c)) { break; } var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.SelectedWirePrefabIdentifier); if (prefab is null) @@ -229,13 +230,56 @@ namespace Barotrauma.Items.Components case CircuitBoxOpcode.RemoveWire: { var data = INetSerializableStruct.Read(msg); - if (!data.TargetIDs.Any() || !item.CanClientAccess(c)) { break; } + if (!data.TargetIDs.Any() || !CanAccessAndUnlocked(c)) { break; } GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogWireName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring); RemoveWireInternal(data.TargetIDs); CreateServerEvent(data); break; } + case CircuitBoxOpcode.RenameLabel: + { + var data = INetSerializableStruct.Read(msg); + if (!CanAccessAndUnlocked(c)) { break; } + + RenameLabelInternal(data.LabelId, data.Color, data.NewHeader, data.NewBody); + CreateServerEvent(data); + break; + } + case CircuitBoxOpcode.AddLabel: + { + var data = INetSerializableStruct.Read(msg); + if (!CanAccessAndUnlocked(c)) { break; } + + ushort id = ICircuitBoxIdentifiable.FindFreeID(Labels); + if (id is ICircuitBoxIdentifiable.NullComponentID) + { + ThrowError("Unable to add label because there are no available IDs left.", c); + return; + } + + AddLabelInternal(id, data.Color, data.Position, data.Header, data.Body); + CreateServerEvent(new CircuitBoxServerAddLabelEvent(id, data.Position, new Vector2(256), data.Color, data.Header, data.Body)); + break; + } + case CircuitBoxOpcode.RemoveLabel: + { + var data = INetSerializableStruct.Read(msg); + if (!CanAccessAndUnlocked(c)) { break; } + + RemoveLabelInternal(data.TargetIDs); + CreateServerEvent(data); + break; + } + case CircuitBoxOpcode.ResizeLabel: + { + var data = INetSerializableStruct.Read(msg); + if (!CanAccessAndUnlocked(c)) { break; } + + ResizeLabelInternal(data.ID, data.Position, data.Size); + CreateServerEvent(data with { Size = Vector2.Max(data.Size, CircuitBoxLabelNode.MinSize) }); + break; + } default: throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); } @@ -253,6 +297,8 @@ namespace Barotrauma.Items.Components return wire.BackingWire.TryUnwrap(out var backingWire) ? backingWire.Name : "a wire"; } + + bool CanAccessAndUnlocked(Client client) => item.CanClientAccess(client) && !Locked; } /// @@ -280,6 +326,7 @@ namespace Barotrauma.Items.Components CircuitBoxInitializeStateFromServerEvent data = new( Components: Components.Select(EventFromComponent).ToImmutableArray(), Wires: Wires.Select(EventFromWire).ToImmutableArray(), + Labels: Labels.Select(EventFromLabel).ToImmutableArray(), InputPos: inputPos, OutputPos: outputPos); @@ -297,6 +344,9 @@ namespace Barotrauma.Items.Components var request = new CircuitBoxClientAddWireEvent(wire.Color, from, to, wire.UsedItemPrefab.UintIdentifier); return new CircuitBoxServerCreateWireEvent(request, wire.ID, backingWire); } + + static CircuitBoxServerAddLabelEvent EventFromLabel(CircuitBoxLabelNode label) + => new(label.ID, label.Position, label.Size, label.Color, label.HeaderText, label.BodyText); } // we don't care about updating the view on server @@ -314,8 +364,9 @@ namespace Barotrauma.Items.Components var nodes = Components.Select(static c => new CircuitBoxIdSelectionPair(c.ID, c.IsSelected ? Option.Some(c.SelectedBy) : Option.None)).ToImmutableArray(); var wires = Wires.Select(static w => new CircuitBoxIdSelectionPair(w.ID, w.IsSelected ? Option.Some(w.SelectedBy) : Option.None)).ToImmutableArray(); var ios = InputOutputNodes.Select(static n => new CircuitBoxTypeSelectionPair(n.NodeType, n.IsSelected ? Option.Some(n.SelectedBy) : Option.None)).ToImmutableArray(); + var labels = Labels.Select(static n => new CircuitBoxIdSelectionPair(n.ID, n.IsSelected ? Option.Some(n.SelectedBy) : Option.None)).ToImmutableArray(); - CreateServerEvent(new CircuitBoxServerUpdateSelection(nodes, wires, ios)); + CreateServerEvent(new CircuitBoxServerUpdateSelection(nodes, wires, ios, labels)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 0ce5b7ab5..3d42626e3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -13,8 +13,6 @@ namespace Barotrauma public void ServerEventRead(IReadMessage msg, Client c) { - List prevItems = new List(AllItems.Distinct()); - if (!receivedItemIds.TryGetValue(c, out List[] receivedItemIdsFromClient)) { receivedItemIdsFromClient = new List[capacity]; @@ -60,8 +58,21 @@ namespace Barotrauma return; } - List prevItemInventories = new List() { this }; + //we need to check which of the items the client can access at this point, before we start shuffling things around + //otherwise if you're e.g. holding an item to access a cabinet, and picking up an item from the cabinet unequips the item you're holding, + //you would fail to pick up the item because it gets unequipped before checking whether you can access the cabinet. + Dictionary canAccessItem = new Dictionary(); + for (int i = 0; i < capacity; i++) + { + foreach (ushort id in receivedItemIdsFromClient[i]) + { + if (Entity.FindEntityByID(id) is not Item item) { continue; } + canAccessItem[item] = item.CanClientAccess(c); + } + } + List prevItems = new List(AllItems.Distinct()); + List prevItemInventories = new List() { this }; for (int i = 0; i < capacity; i++) { foreach (Item item in slots[i].Items.ToList()) @@ -119,7 +130,7 @@ namespace Barotrauma var holdable = item.GetComponent(); if (holdable != null && !holdable.CanBeDeattached()) { continue; } - if (!prevItems.Contains(item) && !item.CanClientAccess(c) && + if (!prevItems.Contains(item) && !canAccessItem[item] && (c.Character == null || item.PreviousParentInventory == null || !c.Character.CanAccessInventory(item.PreviousParentInventory))) { #if DEBUG || UNSTABLE diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs index 2f2a1e174..c5d89f288 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs @@ -1,10 +1,22 @@ -using System; -using Barotrauma.Networking; +using Barotrauma.Networking; +using System; namespace Barotrauma { partial class Submarine { + public readonly struct SetLayerEnabledEventData : NetEntityEvent.IData + { + public readonly Identifier Layer; + public readonly bool Enabled; + + public SetLayerEnabledEventData(Identifier layer, bool enabled) + { + Layer = layer; + Enabled = enabled; + } + } + public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c) { subBody.Body.ServerWrite(tempBuffer); @@ -12,7 +24,15 @@ namespace Barotrauma public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - throw new Exception($"Error while writing a network event for the submarine \"{Info.Name} ({ID})\". Submarines are not even supposed to send events!"); + if (extraData is SetLayerEnabledEventData setLayerEnabledEventData) + { + msg.WriteIdentifier(setLayerEnabledEventData.Layer); + msg.WriteBoolean(setLayerEnabledEventData.Enabled); + } + else + { + throw new Exception($"Error while writing a network event for the submarine \"{Info.Name} ({ID})\". Unrecognized event data: {extraData?.GetType().Name ?? "null"}"); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 5ffb59bc7..73e844d25 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -349,7 +349,7 @@ namespace Barotrauma.Networking { UInt32 id = incMsg.ReadUInt32(); BannedPlayer? bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id); - if (bannedPlayer != null) + if (bannedPlayer != null && c.HasPermission(ClientPermissions.Unban)) { GameServer.Log(GameServer.ClientLogName(c) + " unbanned " + bannedPlayer.Name + " (" + bannedPlayer.AddressOrAccountId + ")", ServerLog.MessageType.ConsoleUsage); RemoveBan(bannedPlayer); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index af254550f..85e4b7fb8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -282,15 +282,19 @@ namespace Barotrauma.Networking SendConsoleMessage("Granted all permissions to " + newClient.Name + ".", newClient); } - SendChatMessage($"ServerMessage.JoinedServer~[client]={ClientLogName(newClient)}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Joined); + SendChatMessage($"ServerMessage.JoinedServer~[client]={ClientLogName(newClient)}", ChatMessageType.Server, changeType: PlayerConnectionChangeType.Joined); ServerSettings.ServerDetailsChanged = true; if (previousPlayer != null && previousPlayer.Name != newClient.Name) { string prevNameSanitized = previousPlayer.Name.Replace("‖", ""); - SendChatMessage($"ServerMessage.PreviousClientName~[client]={ClientLogName(newClient)}~[previousname]={prevNameSanitized}", ChatMessageType.Server, null); + SendChatMessage($"ServerMessage.PreviousClientName~[client]={ClientLogName(newClient)}~[previousname]={prevNameSanitized}", ChatMessageType.Server); previousPlayer.Name = newClient.Name; } + if (!ServerSettings.ServerMessageText.IsNullOrEmpty()) + { + SendDirectChatMessage((TextManager.Get("servermotd") + '\n' + ServerSettings.ServerMessageText).Value, newClient, ChatMessageType.Server); + } var savedPermissions = ServerSettings.ClientPermissions.Find(scp => scp.AddressOrAccountId.TryGet(out AccountId accountId) @@ -434,12 +438,12 @@ namespace Barotrauma.Networking endRoundDelay = 5.0f; endRoundTimer += deltaTime; } - else if (subAtLevelEnd && !(GameMain.GameSession?.GameMode is CampaignMode)) + else if (subAtLevelEnd && GameMain.GameSession?.GameMode is not CampaignMode) { endRoundDelay = 5.0f; endRoundTimer += deltaTime; } - else if (isCrewDead && RespawnManager == null) + else if (isCrewDead && (RespawnManager == null || !RespawnManager.CanRespawnAgain)) { #if !DEBUG if (endRoundTimer <= 0.0f) @@ -1136,6 +1140,10 @@ namespace Barotrauma.Networking //check if midround syncing is needed due to missed unique events if (!midroundSyncingDone) { entityEventManager.InitClientMidRoundSync(c); } MissionAction.NotifyMissionsUnlockedThisRound(c); + if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.SendCrewState(); + } c.InGame = true; } } @@ -2322,7 +2330,7 @@ namespace Barotrauma.Networking if (campaign != null) { campaign.CargoManager.CreatePurchasedItems(); - campaign.SendCrewState(null, default, null); + campaign.SendCrewState(); } Level.Loaded?.SpawnNPCs(); @@ -2792,8 +2800,6 @@ namespace Barotrauma.Networking logMsg = message.TextWithSender; } Log(logMsg, ServerLog.MessageType.Chat); - - base.AddChatMessage(message); } private bool ReadClientNameChange(Client c, IReadMessage inc) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index cf6595515..fc721f6f8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -78,6 +78,11 @@ namespace Barotrauma.Networking public bool IsProcessed; + /// + /// Does the client need to be controlling a character for the server to consider the event valid? + /// + public bool RequireCharacter = true; + public BufferedEvent(Client sender, Character senderCharacter, UInt16 characterStateID, IClientSerializable targetEntity, ReadWriteMessage data) { this.Sender = sender; @@ -157,15 +162,19 @@ namespace Barotrauma.Networking { if (bufferedEvent.Character == null || bufferedEvent.Character.IsDead) { - bufferedEvent.IsProcessed = true; - continue; + if (bufferedEvent.RequireCharacter) + { + bufferedEvent.IsProcessed = true; + continue; + } } //delay reading the events until the inputs for the corresponding frame have been processed //UNLESS the character is unconscious, in which case we'll read the messages immediately (because further inputs will be ignored) //atm the "give in" command is the only thing unconscious characters can do, other types of events are ignored - if (!bufferedEvent.Character.IsIncapacitated && + if (bufferedEvent.Character != null && + !bufferedEvent.Character.IsIncapacitated && NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID)) { DebugConsole.Log($"Delaying reading entity event sent by a client until the character state has been processed. Event's character state: {bufferedEvent.CharacterStateID}, last processed character state: {bufferedEvent.Character.LastProcessedID}"); @@ -503,7 +512,12 @@ namespace Barotrauma.Networking byte[] temp = msg.ReadBytes(msgLength - 2); buffer.WriteBytes(temp, 0, msgLength - 2); buffer.BitPosition = 0; - BufferEvent(new BufferedEvent(sender, sender.Character, characterStateID, entity, buffer)); + BufferEvent( + new BufferedEvent(sender, sender.Character, characterStateID, entity, buffer) + { + //hull updates can be sent without a character to allow editing water and fire in spectator mode + RequireCharacter = entity is not Hull + }); sender.LastSentEntityEventID++; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index fd8d39273..c9329cf3e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -272,7 +272,8 @@ namespace Barotrauma.Networking ServerName = GameMain.Server.ServerName, ContentPackages = contentPackages .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow)) - .ToImmutableArray() + .ToImmutableArray(), + AllowModDownloads = serverSettings.AllowModDownloads }; break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 0d32a93e8..2cae1d3ef 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -323,8 +323,8 @@ namespace Barotrauma.Networking var clients = GetClientsToRespawn().ToList(); foreach (Client c in clients) { - //get rid of the existing character - c.Character?.DespawnNow(); + // Get rid of the existing character + if (c.Character is Character character) { character.DespawnNow(); } c.WaitForNextRoundRespawn = null; @@ -369,12 +369,9 @@ namespace Barotrauma.Networking { divingSuitPrefab = ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuitdeep")); } - if (divingSuitPrefab == null) - { - divingSuitPrefab = + divingSuitPrefab ??= ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuit")) ?? ItemPrefab.Find(null, "divingsuit".ToIdentifier()); - } ItemPrefab oxyPrefab = ItemPrefab.Find(null, "oxygentank".ToIdentifier()); ItemPrefab scooterPrefab = ItemPrefab.Find(null, "underwaterscooter".ToIdentifier()); ItemPrefab batteryPrefab = ItemPrefab.Find(null, "batterycell".ToIdentifier()); @@ -387,6 +384,7 @@ namespace Barotrauma.Networking characterInfos[i].ClearCurrentOrders(); + CharacterCampaignData characterCampaignData = null; bool forceSpawnInMainSub = false; if (!bot) { @@ -398,16 +396,16 @@ namespace Barotrauma.Networking clients[i].PendingName = null; } - var matchingData = campaign?.GetClientCharacterData(clients[i]); - if (matchingData != null) + characterCampaignData = campaign?.GetClientCharacterData(clients[i]); + if (characterCampaignData != null) { - if (!matchingData.HasSpawned) + if (!characterCampaignData.HasSpawned) { forceSpawnInMainSub = true; } else { - ReduceCharacterSkills(characterInfos[i]); + ReduceCharacterSkillsOnDeath(characterInfos[i]); characterInfos[i].RemoveSavedStatValuesOnDeath(); characterInfos[i].CauseOfDeath = null; } @@ -415,6 +413,7 @@ namespace Barotrauma.Networking } var character = Character.Create(characterInfos[i], (forceSpawnInMainSub ? mainSubSpawnPoints[i] : shuttleSpawnPoints[i]).WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot); + characterCampaignData?.ApplyWalletData(character); character.TeamID = CharacterTeamType.Team1; character.LoadTalents(); @@ -505,11 +504,10 @@ namespace Barotrauma.Networking } var characterData = campaign?.GetClientCharacterData(clients[i]); + // NOTE: This was where Reaper's tax got applied if (characterData != null && Level.Loaded?.Type != LevelData.LevelType.Outpost && characterData.HasSpawned) { - //we need to reapply the previous respawn penalty affliction or successive deaths won't make it stack - characterData.ApplyHealthData(character, (AfflictionPrefab ap) => ap == GetRespawnPenaltyAfflictionPrefab()); - GiveRespawnPenaltyAffliction(character); + ReduceCharacterSkillsOnDeath(characterInfos[i], applyExtraSkillLoss: true); } if (characterData == null || characterData.HasSpawned) { @@ -541,14 +539,37 @@ namespace Barotrauma.Networking } } - public static void ReduceCharacterSkills(CharacterInfo characterInfo) + /// + /// Reduce any skill gains the character may have made over the job's default + /// skill levels by percentages defined in server settings. There are two + /// reductions, a base one that always applies, and an extra loss that only + /// applies when the player chooses to respawn ASAP rather than wait. + /// + public static void ReduceCharacterSkillsOnDeath(CharacterInfo characterInfo, bool applyExtraSkillLoss = false) { if (characterInfo?.Job == null) { return; } + + float resistanceMultiplier; + float skillLossPercentage; + if (applyExtraSkillLoss) + { + DebugConsole.Log($"Calculating extra skill loss on respawn for {characterInfo.Name}:"); + resistanceMultiplier = characterInfo.LastResistanceMultiplierSkillLossRespawn; + skillLossPercentage = SkillLossPercentageOnImmediateRespawn; + } + else + { + DebugConsole.Log($"Calculating base skill loss on death for {characterInfo.Name}:"); + resistanceMultiplier = characterInfo.LastResistanceMultiplierSkillLossDeath; + skillLossPercentage = SkillLossPercentageOnDeath; + } + skillLossPercentage *= resistanceMultiplier; + foreach (Skill skill in characterInfo.Job.GetSkills()) { var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; } - skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillLossPercentageOnDeath / 100.0f); + skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, skillLossPercentage / 100.0f); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index b75d7efc8..b474f5548 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -95,15 +95,6 @@ namespace Barotrauma.Networking { NetFlags requiredFlags = GetRequiredFlags(c); outMsg.WriteByte((byte)requiredFlags); - if (requiredFlags.HasFlag(NetFlags.Name)) - { - outMsg.WriteString(ServerName); - } - - if (requiredFlags.HasFlag(NetFlags.Message)) - { - outMsg.WriteString(ServerMessageText); - } outMsg.WriteByte((byte)PlayStyle); outMsg.WriteByte((byte)MaxPlayers); outMsg.WriteBoolean(HasPassword); @@ -122,8 +113,7 @@ namespace Barotrauma.Networking WriteHiddenSubs(outMsg); } - if (c.HasPermission(Networking.ClientPermissions.ManageSettings) - && NetIdUtils.IdMoreRecent( + if (NetIdUtils.IdMoreRecent( newID: LastUpdateIdForFlag[NetFlags.Properties], oldID: c.LastRecvServerSettingsUpdate)) { @@ -147,20 +137,6 @@ namespace Barotrauma.Networking bool changed = false; - if (flags.HasFlag(NetFlags.Name)) - { - string serverName = incMsg.ReadString(); - if (ServerName != serverName) { changed = true; } - ServerName = serverName; - } - - if (flags.HasFlag(NetFlags.Message)) - { - string serverMessageText = incMsg.ReadString(); - if (ServerMessageText != serverMessageText) { changed = true; } - ServerMessageText = serverMessageText; - } - if (flags.HasFlag(NetFlags.Properties)) { bool propertiesChanged = ReadExtraCargo(incMsg); @@ -217,42 +193,9 @@ namespace Barotrauma.Networking int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; GameMain.NetLobbyScreen.MissionType = (MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits); - bool changedTraitorProbability = incMsg.ReadBoolean(); - float traitorProbability = incMsg.ReadSingle(); - if (changedTraitorProbability) - { - TraitorProbability = traitorProbability; - } //the byte indicates the direction we're changing the value, subtract one to get negative values from a byte TraitorDangerLevel = TraitorDangerLevel + incMsg.ReadByte() - 1; - int botCount = BotCount + incMsg.ReadByte() - 1; - if (botCount < 0) { botCount = MaxBotCount; } - if (botCount > MaxBotCount) { botCount = 0; } - BotCount = botCount; - - int botSpawnMode = (int)BotSpawnMode + incMsg.ReadByte() - 1; - if (botSpawnMode < 0) { botSpawnMode = 1; } - if (botSpawnMode > 1) { botSpawnMode = 0; } - BotSpawnMode = (BotSpawnMode)botSpawnMode; - - float levelDifficulty = incMsg.ReadSingle(); - if (levelDifficulty >= 0.0f) { SelectedLevelDifficulty = levelDifficulty; } - - bool changedUseRespawnShuttle = incMsg.ReadBoolean(); - bool useRespawnShuttle = incMsg.ReadBoolean(); - if (changedUseRespawnShuttle) - { - UseRespawnShuttle = useRespawnShuttle; - } - - bool changedAutoRestart = incMsg.ReadBoolean(); - bool autoRestart = incMsg.ReadBoolean(); - if (changedAutoRestart) - { - AutoRestart = autoRestart; - } - changed |= true; UpdateFlag(NetFlags.Misc); } @@ -292,8 +235,6 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("enableupnp", EnableUPnP); doc.Root.SetAttributeValue("autorestart", autoRestart); - doc.Root.SetAttributeValue("LevelDifficulty", ((int)selectedLevelDifficulty).ToString()); - doc.Root.SetAttributeValue("ServerMessage", ServerMessageText); doc.Root.SetAttributeValue("HiddenSubs", string.Join(",", HiddenSubs)); @@ -304,6 +245,8 @@ namespace Barotrauma.Networking SerializableProperty.SerializeProperties(this, doc.Root, true); doc.Root.Add(CampaignSettings.Save()); + doc.Root.SetAttributeValue("DisabledMonsters", string.Join(",", MonsterEnabled.Where(kvp => !kvp.Value).Select(kvp => kvp.Key.Value))); + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, @@ -351,9 +294,6 @@ namespace Barotrauma.Networking AllowSubVoting = SubSelectionMode == SelectionMode.Vote; AllowModeVoting = ModeSelectionMode == SelectionMode.Vote; - selectedLevelDifficulty = doc.Root.GetAttributeFloat("LevelDifficulty", 20.0f); - GameMain.NetLobbyScreen.SetLevelDifficulty(selectedLevelDifficulty); - GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability); HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty())); @@ -448,6 +388,14 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetBotCount(BotCount); MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); + var disabledMonsters = doc.Root.GetAttributeIdentifierArray("DisabledMonsters", Array.Empty()); + foreach (var disabledMonster in disabledMonsters) + { + if (MonsterEnabled.ContainsKey(disabledMonster)) + { + MonsterEnabled[disabledMonster] = false; + } + } foreach (XElement element in doc.Root.Elements()) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs index e77071639..7a3e9a43c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs @@ -112,13 +112,13 @@ namespace Barotrauma.Networking if (recipientSpectating) { if (recipient.SpectatePos == null) { return true; } - distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / ChatMessage.SpeakRange, 0.0f, 1.0f); + distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / ChatMessage.SpeakRangeVOIP, 0.0f, 1.0f); return distanceFactor < 1.0f; } else { //otherwise do a distance check - float garbleAmount = ChatMessage.GetGarbleAmount(recipient.Character, sender.Character, ChatMessage.SpeakRange); + float garbleAmount = ChatMessage.GetGarbleAmount(recipient.Character, sender.Character, ChatMessage.SpeakRangeVOIP); distanceFactor = garbleAmount; return garbleAmount < 1.0f; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index ff3f08bbe..554930461 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -106,24 +106,6 @@ namespace Barotrauma } } - public void ChangeServerName(string n) - { - GameMain.Server.ServerSettings.ServerName = n; lastUpdateID++; - } - - public void ChangeServerMessage(string m) - { - GameMain.Server.ServerSettings.ServerMessageText = m; lastUpdateID++; - } - - public List JobPreferences - { - get - { - return null; - } - } - public NetLobbyScreen() { LevelSeed = ToolBox.RandomSeed(8); @@ -170,7 +152,7 @@ namespace Barotrauma } set { - if (levelSeed == value) return; + if (levelSeed == value) { return; } lastUpdateID++; levelSeed = value; @@ -200,6 +182,12 @@ namespace Barotrauma { GameMain.GameSession = null; } + if (GameMain.Server.ServerSettings.SelectedSubmarine.IsNullOrEmpty()) + { + //if no sub is selected in the settings, + //select the random sub we selected in the constructor + GameMain.Server.ServerSettings.SelectedSubmarine = SelectedSub?.Name; + } } public void RandomizeSettings() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index 004e422df..c9c691212 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -334,7 +334,7 @@ namespace Barotrauma private void CreateTraitorEvent(EventManager eventManager, TraitorEventPrefab selectedPrefab, Client traitor) { - if (selectedPrefab.TryCreateInstance(out var newEvent)) + if (selectedPrefab.TryCreateInstance(eventManager.RandomSeed, out var newEvent)) { var secondaryTraitors = SelectSecondaryTraitors(newEvent, traitor); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index b02ff43fb..2e610f36a 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml index a9be7381b..9ab82603d 100644 --- a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -2,37 +2,81 @@ - - + + + + + MaxMissionCount="2" + WorldHostility="low" + CrewVitalityMultiplier="1.1" + NonCrewVitalityMultiplier="1.0" + OxygenMultiplier="1.2" + FuelMultiplier="1.2" + MissionRewardMultiplier="1.0" + ShopPriceMultiplier="0.9" + ShipyardPriceMultiplier="0.9" + RepairFailMultiplier="1.0" + PatdownProbability="low" + ShowHuskWarning="true"/> + WorldHostility="medium" + CrewVitalityMultiplier="1.0" + NonCrewVitalityMultiplier="1.0" + OxygenMultiplier="1.0" + FuelMultiplier="1.0" + MissionRewardMultiplier="1.0" + ShopPriceMultiplier="1.0" + ShipyardPriceMultiplier="1.0" + RepairFailMultiplier="1.0" + PatdownProbability="medium" + ShowHuskWarning="true"/> + MaxMissionCount="2" + WorldHostility="high" + CrewVitalityMultiplier="1.0" + NonCrewVitalityMultiplier="1.0" + OxygenMultiplier="0.7" + FuelMultiplier="0.9" + MissionRewardMultiplier="1.0" + ShopPriceMultiplier="1.5" + ShipyardPriceMultiplier="1.5" + RepairFailMultiplier="2.0" + PatdownProbability="high" + ShowHuskWarning="false"/> + \ 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 eb469f60a..aa47e8d51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -76,9 +76,9 @@ namespace Barotrauma get { return Character.AnimController.Collider.LinearVelocity; } } - public virtual bool CanEnterSubmarine + public virtual CanEnterSubmarine CanEnterSubmarine { - get { return true; } + get { return Character.AnimController.CanEnterSubmarine; } } public virtual bool CanFlip @@ -327,8 +327,17 @@ namespace Barotrauma { if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags)) { - // Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items of the same type and if can't fit them all in the inventory. - return false; + bool switchingToBetterSuit = + targetTags != null && + targetTags.FirstOrDefault() == Tags.HeavyDivingGear && + AIObjectiveFindDivingGear.IsSuitablePressureProtection(item, Tags.HeavyDivingGear, Character) && + !AIObjectiveFindDivingGear.IsSuitablePressureProtection(otherItem, Tags.HeavyDivingGear, Character); + // Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items + // of the same type and if can't fit them all in the inventory. + if (!switchingToBetterSuit) + { + return false; + } } //if everything else fails, simply drop the existing item otherItem.Drop(Character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index d4188f275..0c94adf1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -176,12 +176,13 @@ namespace Barotrauma } } - public override bool CanEnterSubmarine + public override CanEnterSubmarine CanEnterSubmarine { get { - //can't enter a submarine when attached to something - return Character.AnimController.CanEnterSubmarine && (LatchOntoAI == null || !LatchOntoAI.IsAttachedToSub); + //can't enter a submarine when attached to one + if (LatchOntoAI is { IsAttachedToSub: true }) { return CanEnterSubmarine.False; } + return Character.AnimController.CanEnterSubmarine; } } @@ -535,12 +536,27 @@ namespace Barotrauma FadeMemories(updateMemoriesInverval); updateMemoriesTimer = updateMemoriesInverval; } - if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null && - SelectedAiTarget.Entity is Character target && (target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target))) + if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null) { - // Keep fleeing if being chased - State = AIState.Flee; + Character target = SelectedAiTarget.Entity as Character; + if (target == null && SelectedAiTarget.Entity is Item targetItem) + { + target = GetOwner(targetItem); + } + bool shouldFlee = false; + if (target != null) + { + // Keep fleeing if being chased or if we see a human target (that don't have enemy ai). + shouldFlee = target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target); + } + // If we should not flee, just idle. Don't allow any other AI state when below the health threshold. + State = shouldFlee ? AIState.Flee : AIState.Idle; wallTarget = null; + if (State != AIState.Flee) + { + SelectedAiTarget = null; + _lastAiTarget = null; + } } else { @@ -614,7 +630,7 @@ namespace Barotrauma steeringManager = outsideSteering; } } - + bool useSteeringLengthAsMovementSpeed = State == AIState.Idle && Character.AnimController.InWater; bool run = false; switch (State) @@ -870,11 +886,11 @@ namespace Barotrauma } // Ensure that the creature keeps inside the level SteerInsideLevel(deltaTime); - float defaultSpeed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun); - //calculate a normalized Steering value at this point: we multiply it with the actual, desired speed in ApplyMovementLimits - steeringManager.Update(1.0f); - float speed = useSteeringLengthAsMovementSpeed ? Steering.Length() : defaultSpeed; - Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, speed); + float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun); + // Doesn't work if less than 1, when we use steering length as movement speed. + steeringManager.Update(Math.Max(speed, 1.0f)); + float movementSpeed = useSteeringLengthAsMovementSpeed ? Steering.Length() : speed; + Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, movementSpeed); if (Character.CurrentHull != null && Character.AnimController.InWater) { // Limit the swimming speed inside the sub. @@ -1087,6 +1103,22 @@ namespace Barotrauma // How long the monster tries to reach out for the target when it's close to it before ignoring it. private const float reachTimeOut = 10; + private bool IsSameTarget(AITarget target, AITarget otherTarget) + { + if (target?.Entity == otherTarget?.Entity) { return true; } + if (IsItemInCharacterInventory(target, otherTarget) || IsItemInCharacterInventory(otherTarget, target)) { return true; } + return false; + + bool IsItemInCharacterInventory(AITarget potentialItem, AITarget potentialCharacter) + { + if (potentialItem?.Entity is Item item && potentialCharacter?.Entity is Character character) + { + return item.ParentInventory?.Owner == character; + } + return false; + } + } + private void UpdateAttack(float deltaTime) { if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) @@ -1132,7 +1164,7 @@ namespace Barotrauma attackSimPos = Character.GetRelativeSimPosition(SelectedAiTarget.Entity); } - if (Character.AnimController.CanEnterSubmarine) + if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True) { if (TrySteerThroughGaps(deltaTime)) { @@ -1177,13 +1209,21 @@ namespace Barotrauma if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0)) { var currentAttackLimb = AttackLimb ?? _previousAttackLimb; - if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay) + if (currentAttackLimb.attack.CoolDownTimer >= + currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay) { return; } - AIBehaviorAfterAttack activeBehavior = currentAttackLimb.attack.AfterAttack; + currentAttackLimb.attack.AfterAttackTimer += deltaTime; + AIBehaviorAfterAttack activeBehavior = + currentAttackLimb.attack.AfterAttackSecondaryDelay > 0 && currentAttackLimb.attack.AfterAttackTimer > currentAttackLimb.attack.AfterAttackSecondaryDelay ? + currentAttackLimb.attack.AfterAttackSecondary : + currentAttackLimb.attack.AfterAttack; switch (activeBehavior) { + case AIBehaviorAfterAttack.Eat: + UpdateEating(deltaTime); + return; case AIBehaviorAfterAttack.Pursue: case AIBehaviorAfterAttack.PursueIfCanAttack: if (currentAttackLimb.attack.SecondaryCoolDown <= 0) @@ -1205,7 +1245,7 @@ namespace Barotrauma if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. - if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) + if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget)) { canAttack = false; if (activeBehavior == AIBehaviorAfterAttack.PursueIfCanAttack) @@ -1266,27 +1306,24 @@ namespace Barotrauma if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. - if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) + if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget)) { UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } + // If the secondary cooldown is defined and expired, check if we can switch the attack + var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); + if (newLimb != null) + { + // Attack with the new limb + AttackLimb = newLimb; + } else { - // If the secondary cooldown is defined and expired, check if we can switch the attack - var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); - if (newLimb != null) - { - // Attack with the new limb - AttackLimb = newLimb; - } - else - { - // No new limb was found. - UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); - return; - } - } + // No new limb was found. + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + return; + } } else { @@ -1308,27 +1345,24 @@ namespace Barotrauma if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. - if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) + if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget)) { UpdateIdle(deltaTime, followLastTarget: false); return; } + // If the secondary cooldown is defined and expired, check if we can switch the attack + var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); + if (newLimb != null) + { + // Attack with the new limb + AttackLimb = newLimb; + } else { - // If the secondary cooldown is defined and expired, check if we can switch the attack - var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); - if (newLimb != null) - { - // Attack with the new limb - AttackLimb = newLimb; - } - else - { - // No new limb was found. - UpdateIdle(deltaTime, followLastTarget: false); - return; - } - } + // No new limb was found. + UpdateIdle(deltaTime, followLastTarget: false); + return; + } } else { @@ -1341,6 +1375,9 @@ namespace Barotrauma case AIBehaviorAfterAttack.FollowThrough: UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; + case AIBehaviorAfterAttack.FollowThroughWithoutObstacleAvoidance: + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true, avoidObstacles: false); + return; case AIBehaviorAfterAttack.FallBack: case AIBehaviorAfterAttack.Reverse: default: @@ -1724,7 +1761,7 @@ namespace Barotrauma circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor); aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); DisableAttacksIfLimbNotRanged(); - if (targetSub != null && targetSub.Borders.Width < 1000 && AttackLimb?.attack is { Ranged: false }) + if (targetSub is { Borders.Width: < 1000 } && AttackLimb?.attack is { Ranged: false }) { breakCircling = true; CirclePhase = CirclePhase.CloseIn; @@ -1982,7 +2019,7 @@ namespace Barotrauma Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity; if (AttackLimb?.attack is Attack { Ranged: true } attack) { - AimRangedAttack(attack, targetEntity); + AimRangedAttack(attack, attackTargetLimb as ISpatialEntity ?? targetEntity); } if (canAttack) { @@ -2005,9 +2042,10 @@ namespace Barotrauma } } - public void AimRangedAttack(Attack attack, Entity targetEntity) + public void AimRangedAttack(Attack attack, ISpatialEntity targetEntity) { - if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; } + if (attack is not { Ranged: true }) { return; } + if (targetEntity is Entity { Removed: true }) { return; } Character.SetInput(InputType.Aim, false, true); if (attack.AimRotationTorque <= 0) { return; } Limb limb = GetLimbToRotate(attack); @@ -2115,7 +2153,10 @@ namespace Barotrauma bool wasLatched = IsLatchedOnSub; Character.AnimController.ReleaseStuckLimbs(); - LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); + if (attackResult.Damage > 0) + { + LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); + } if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } if (attackResult.Damage >= AIParams.DamageThreshold) { @@ -2283,7 +2324,7 @@ namespace Barotrauma Limb referenceLimb = GetLimbToRotate(ActiveAttack); if (referenceLimb != null) { - Vector2 toTarget = spatialTarget.WorldPosition - referenceLimb.WorldPosition; + Vector2 toTarget = attackWorldPos - referenceLimb.WorldPosition; float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir); float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget)); @@ -2456,7 +2497,7 @@ namespace Barotrauma } private Vector2? attackVector = null; - private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false) + private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false, bool avoidObstacles = true) { if (attackVector == null) { @@ -2468,7 +2509,7 @@ namespace Barotrauma dir = Vector2.UnitY; } steeringManager.SteeringManual(deltaTime, dir); - if (Character.AnimController.InWater && !Reverse) + if (Character.AnimController.InWater && !Reverse && avoidObstacles) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); } @@ -2779,10 +2820,16 @@ namespace Barotrauma // Ignore inner walls when outside (walltargets still work) continue; } - if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s)) + bool attemptToGetInside = + Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True || + //characters that are aggressive boarders can partially enter the sub can attempt to push through holes + (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial && IsAggressiveBoarder); + + if (!attemptToGetInside && IsWallDisabled(s)) { continue; } + // Prefer weaker walls (200 is the default for normal hull walls) valueModifier = 200f / s.MaxHealth; for (int i = 0; i < s.Sections.Length; i++) @@ -2790,7 +2837,7 @@ namespace Barotrauma var section = s.Sections[i]; if (section.gap == null) { continue; } bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; - if (Character.AnimController.CanEnterSubmarine) + if (attemptToGetInside) { if (!isCharacterInside) { @@ -2873,9 +2920,11 @@ namespace Barotrauma { if (!canAttackDoors) { continue; } } - else if (!Character.AnimController.CanEnterSubmarine) + else if (Character.AnimController.CanEnterSubmarine != CanEnterSubmarine.True) { // Ignore broken and open doors, if cannot enter submarine + // Also ignore them if the monster can only partially enter the sub: + // these monsters tend to be too large to get through doors anyway. continue; } if (IsAggressiveBoarder) @@ -2911,6 +2960,13 @@ namespace Barotrauma if (targetParams.IgnoreInside && Character.CurrentHull != null) { continue; } if (targetParams.IgnoreOutside && Character.CurrentHull == null) { continue; } if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; } + if (targetParams.IgnoreTargetInside && aiTarget.Entity.Submarine != null) { continue; } + if (targetParams.IgnoreTargetOutside && aiTarget.Entity.Submarine == null) { continue; } + if (aiTarget.Entity is ISerializableEntity se) + { + if (targetParams.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { continue; } + } + if (targetParams.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; } if (targetParams.IgnoreIfNotInSameSub) { if (aiTarget.Entity.Submarine != Character.Submarine) { continue; } @@ -2981,6 +3037,16 @@ namespace Barotrauma { dist *= 0.9f; } + if (targetParams.PerceptionDistanceMultiplier > 0.0f) + { + dist /= targetParams.PerceptionDistanceMultiplier; + } + + if (targetParams.MaxPerceptionDistance > 0.0f && + dist * dist > targetParams.MaxPerceptionDistance * targetParams.MaxPerceptionDistance) + { + continue; + } if (!CanPerceive(aiTarget, dist, checkVisibility: SelectedAiTarget != aiTarget)) { @@ -3196,7 +3262,7 @@ namespace Barotrauma { if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub) { - if (!(SelectedAiTarget?.Entity is Structure wall)) + if (SelectedAiTarget?.Entity is not Structure wall) { wall = wallTarget?.Structure; } @@ -3251,9 +3317,10 @@ namespace Barotrauma if (HasValidPath(requireNonDirty: true)) { return; } wallHits.Clear(); Structure wall = null; - Vector2 rayStart = AttackLimb != null ? AttackLimb.SimPosition : SimPosition; + Vector2 refPos = AttackLimb != null ? AttackLimb.SimPosition : SimPosition; if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target)) { + Vector2 rayStart = refPos; Vector2 rayEnd = SelectedAiTarget.SimPosition; if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { @@ -3267,6 +3334,7 @@ namespace Barotrauma } if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading)) { + Vector2 rayStart = refPos; Vector2 rayEnd = rayStart + VectorExtensions.Forward(Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5); if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { @@ -3282,6 +3350,7 @@ namespace Barotrauma } if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering)) { + Vector2 rayStart = refPos; Vector2 rayEnd = rayStart + Steering * 5; if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { @@ -3297,18 +3366,25 @@ namespace Barotrauma } if (wallHits.Any()) { + Vector2 targetdiff = ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition - (AttackLimb != null ? AttackLimb.WorldPosition : WorldPosition)); + float targetDistance = targetdiff.LengthSquared(); Body closestBody = null; float closestDistance = 0; int sectionIndex = -1; Vector2 sectionPos = Vector2.Zero; foreach ((Body body, int index, Vector2 sectionPosition) in wallHits) { - float distance = Vector2.DistanceSquared(SimPosition, sectionPosition); + Structure structure = body.UserData as Structure; + float distance = Vector2.DistanceSquared( + refPos, + Submarine.GetRelativeSimPosition(ConvertUnits.ToSimUnits(sectionPosition), Character.Submarine, structure.Submarine)); + //if the wall is further than the target (e.g. at the other side of the sub?), we shouldn't be targeting it + if (distance > targetDistance) { continue; } if (closestBody == null || closestDistance == 0 || distance < closestDistance) { closestBody = body; closestDistance = distance; - wall = closestBody.UserData as Structure; + wall = structure; sectionPos = sectionPosition; sectionIndex = index; } @@ -3326,14 +3402,18 @@ namespace Barotrauma sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X; } LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal); - if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall)) + if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True || + !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall)) { - if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine) + if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True) { bool isTargetingDoor = SelectedAiTarget.Entity is Item i && i.GetComponent() != null; // Blocked by a wall that shouldn't be targeted. The main intention here is to prevent monsters from entering the the tail and the nose pieces. if (!isTargetingDoor) { + //TODO: this might cause problems: many wall pieces (like smaller shuttle pieces + //and small decorative wall structures are currently marked as having no AI target, + //which can mean a monster very frequently ignores targets inside because they're blocked by those structures IgnoreTarget(SelectedAiTarget); ResetAITarget(); } @@ -3353,7 +3433,9 @@ namespace Barotrauma void DoRayCast(Vector2 rayStart, Vector2 rayEnd) { - Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine); + Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, + ignoreSensors: CanEnterSubmarine != CanEnterSubmarine.False, + ignoreDisabledWalls: CanEnterSubmarine != CanEnterSubmarine.False); if (hitTarget != null && IsValid(hitTarget, out wall)) { int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition)); @@ -3371,7 +3453,8 @@ namespace Barotrauma { if (wall.SectionBodyDisabled(i)) { - if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount)) + if (Character.AnimController.CanEnterSubmarine != CanEnterSubmarine.False && + CanPassThroughHole(wall, i, requiredHoleCount)) { sectionIndex = i; break; @@ -3394,14 +3477,14 @@ namespace Barotrauma { wall = null; if (Submarine.LastPickedFraction == 1.0f) { return false; } - if (!(hit.UserData is Structure w)) { return false; } + if (hit.UserData is not Structure w) { return false; } if (w.Submarine == null) { return false; } if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; } if (Character.Submarine == null) { if (w.Prefab.Tags.Contains("inner")) { - if (!Character.AnimController.CanEnterSubmarine) { return false; } + if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.False) { return false; } } else if (!AIParams.TargetOuterWalls) { @@ -3731,7 +3814,7 @@ namespace Barotrauma } reachTimer = 0; sinTime = 0; - if (breakCircling && strikeTimer <= 0) + if (breakCircling && strikeTimer <= 0 && CirclePhase != CirclePhase.CloseIn) { CirclePhase = CirclePhase.Start; } @@ -3757,7 +3840,7 @@ namespace Barotrauma blockCheckTimer = 0; reachTimer = 0; sinTime = 0; - if (breakCircling && strikeTimer <= 0) + if (breakCircling && strikeTimer <= 0 && CirclePhase != CirclePhase.CloseIn) { CirclePhase = CirclePhase.Start; } @@ -3765,7 +3848,12 @@ namespace Barotrauma private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f); - private float GetPerceivingRange(AITarget target) => Math.Max(target.SightRange * Sight, target.SoundRange * Hearing); + private float GetPerceivingRange(AITarget target) + { + float maxSightOrSoundRange = Math.Max(target.SightRange * Sight, target.SoundRange * Hearing); + if (AIParams.MaxPerceptionDistance >= 0 && maxSightOrSoundRange > AIParams.MaxPerceptionDistance) { return AIParams.MaxPerceptionDistance; } + return maxSightOrSoundRange; + } private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1, bool checkVisibility = false) { @@ -3783,6 +3871,7 @@ namespace Barotrauma } if (dist > 0) { + if (AIParams.MaxPerceptionDistance >= 0 && dist > AIParams.MaxPerceptionDistance) { return false; } insideSightRange = IsInRange(dist, target.SightRange, Sight); if (!checkVisibility && insideSightRange) { return true; } insideSoundRange = IsInRange(dist, target.SoundRange, Hearing); @@ -3793,6 +3882,7 @@ namespace Barotrauma { distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition); } + if (AIParams.MaxPerceptionDistance >= 0 && distSquared > AIParams.MaxPerceptionDistance * AIParams.MaxPerceptionDistance) { return false; } insideSightRange = IsInRangeSqr(distSquared, target.SightRange, Sight); if (!checkVisibility && insideSightRange) { return true; } insideSoundRange = IsInRangeSqr(distSquared, target.SoundRange, Hearing); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index a8dfedc43..706a8fd0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -72,6 +72,11 @@ namespace Barotrauma /// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity. /// public float ReportRange { get; set; } = float.PositiveInfinity; + + /// + /// How far the character can seek new weapons from. + /// + public float FindWeaponsRange { get; set; } = float.PositiveInfinity; private float _aimSpeed = 1; public float AimSpeed @@ -150,9 +155,13 @@ namespace Barotrauma } public override bool IsMentallyUnstable => - MentalStateManager == null ? false : - MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Normal && - MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Confused; + MentalStateManager is + { + CurrentMentalType: + MentalStateManager.MentalType.Afraid or + MentalStateManager.MentalType.Desperate or + MentalStateManager.MentalType.Berserk + }; public ShipCommandManager ShipCommandManager { get; private set; } @@ -817,7 +826,7 @@ namespace Barotrauma private readonly HashSet itemsToRelocate = new HashSet(); - private void HandleRelocation(Item item) + public void HandleRelocation(Item item) { if (item.SpawnedInCurrentOutpost) { return; } if (item.Submarine == null) { return; } @@ -837,7 +846,10 @@ namespace Barotrauma // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. campaign.BeforeLevelLoading += Relocate; } - + campaign.ItemsRelocatedToMainSub = true; +#if CLIENT + HintManager.OnItemMarkedForRelocation(); +#endif void Relocate() { if (item == null || item.Removed) { return; } @@ -1566,7 +1578,7 @@ namespace Barotrauma { HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman", AbortCondition = abortCondition, - allowHoldFire = allowHoldFire, + AllowHoldFire = allowHoldFire, }; if (onAbort != null) { @@ -1583,12 +1595,16 @@ namespace Barotrauma public void SetOrder(Order order, bool speak = true) { objectiveManager.SetOrder(order, speak); +#if CLIENT + HintManager.OnSetOrder(Character, order); +#endif } - public void SetForcedOrder(Order order) + public AIObjective SetForcedOrder(Order order) { var objective = ObjectiveManager.CreateObjective(order); ObjectiveManager.SetForcedOrder(objective); + return objective; } public void ClearForcedOrder() @@ -1677,14 +1693,17 @@ namespace Barotrauma return false; } - public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank); + public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => + HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank); /// - /// Check whether the character has a diving suit in usable condition plus some oxygen. + /// Check whether the character has a diving suit in usable condition, suitable pressure protection for the depth, plus some oxygen. /// - public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) + public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true, bool requireSuitablePressureProtection = true) => HasItem(character, Tags.HeavyDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true, - predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes)); + predicate: (Item item) => + character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes) && + (!requireSuitablePressureProtection || AIObjectiveFindDivingGear.IsSuitablePressureProtection(item, Tags.HeavyDivingGear, character))); /// /// Check whether the character has a diving mask in usable condition plus some oxygen. @@ -1838,7 +1857,7 @@ 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 not HumanAIController otherHumanAI || + otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI || otherCharacter.IsEscorted || Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f) { continue; @@ -2064,7 +2083,7 @@ namespace Barotrauma visibleHulls = VisibleHulls; } bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); - bool ignoreOxygen = HasDivingGear(character); + bool ignoreOxygen = HasDivingGear(character); bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.IsCurrentObjective(); float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) @@ -2197,11 +2216,29 @@ namespace Barotrauma public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) { + if (other.IsHusk) + { + // Disguised as husk + return me.IsDisguisedAsHusk; + } + else + { + if (other.IsPrisoner && me.IsPrisoner) + { + // Both prisoners + return true; + } + if (other.IsHostileEscortee && me.IsHostileEscortee) + { + // Both hostile escortees + return true; + } + } bool sameTeam = me.TeamID == other.TeamID; bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other); if (!teamGood) { - return other.IsHusk && me.IsDisguisedAsHusk; + return false; } if (other.IsPet) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index ca5aa891e..963912fc7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; using System.Linq; +using Voronoi2; namespace Barotrauma { @@ -32,14 +33,21 @@ namespace Barotrauma private Vector2 _attachPos; + /// + /// The character won't latch onto anything when the cooldown is active (activates after the character deattaches for whatever reason). + /// private float attachCooldown; - private Limb attachLimb; + private readonly Limb attachLimb; private Vector2 localAttachPos; - private float attachLimbRotation; + private readonly float attachLimbRotation; private float jointDir; + private float latchedDuration; + + private readonly bool freezeWhenLatched; + public List AttachJoints { get; } = new List(); public Vector2? AttachPos @@ -54,18 +62,19 @@ namespace Barotrauma public LatchOntoAI(XElement element, EnemyAIController enemyAI) { - AttachToWalls = element.GetAttributeBool("attachtowalls", false); - AttachToSub = element.GetAttributeBool("attachtosub", false); - AttachToCharacters = element.GetAttributeBool("attachtocharacters", false); - minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 5.0f); - maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 8.0f)); - maxAttachDuration = element.GetAttributeFloat("maxattachduration", -1.0f); - coolDown = element.GetAttributeFloat("cooldown", 2f); - damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f); - detachStun = element.GetAttributeFloat("detachstun", 0.0f); - localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero)); - attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f)); - weld = element.GetAttributeBool("weld", true); + AttachToWalls = element.GetAttributeBool(nameof(AttachToWalls), false); + AttachToSub = element.GetAttributeBool(nameof(AttachToSub), false); + AttachToCharacters = element.GetAttributeBool(nameof(AttachToCharacters), false); + minDeattachSpeed = element.GetAttributeFloat(nameof(minDeattachSpeed), 5.0f); + maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat(nameof(maxDeattachSpeed), 8.0f)); + maxAttachDuration = element.GetAttributeFloat(nameof(maxAttachDuration), -1.0f); + coolDown = element.GetAttributeFloat(nameof(coolDown), 2f); + damageOnDetach = element.GetAttributeFloat(nameof(damageOnDetach), 0.0f); + detachStun = element.GetAttributeFloat(nameof(detachStun), 0.0f); + localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2(nameof(localAttachPos), Vector2.Zero)); + attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat(nameof(attachLimbRotation), 0.0f)); + weld = element.GetAttributeBool(nameof(weld), true); + freezeWhenLatched = element.GetAttributeBool(nameof(freezeWhenLatched), false); string limbString = element.GetAttributeString("attachlimb", null); attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase)); @@ -108,7 +117,23 @@ namespace Barotrauma targetBody = target.AnimController.Collider.FarseerBody; attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition); } - + + public void SetAttachTarget(VoronoiCell levelWall) + { + if (!AttachToWalls) { return; } + Reset(); + foreach (Voronoi2.GraphEdge edge in levelWall.Edges) + { + if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, levelWall.Center, out Vector2 intersection)) + { + attachSurfaceNormal = edge.GetNormal(levelWall); + targetBody = levelWall.Body; + _attachPos = ConvertUnits.ToSimUnits(intersection); + return; + } + } + } + public void Update(EnemyAIController enemyAI, float deltaTime) { if (TargetCharacter != null && character.Submarine != TargetCharacter.Submarine || @@ -119,6 +144,17 @@ namespace Barotrauma } if (IsAttached) { + latchedDuration += deltaTime; + if (freezeWhenLatched && targetBody is { BodyType: BodyType.Static } && + /*brief delay to let the ragdoll "settle"*/ + latchedDuration > 5.0f) + { + foreach (var limb in character.AnimController.Limbs) + { + limb.body.LinearVelocity = Vector2.Zero; + limb.body.AngularVelocity = 0.0f; + } + } if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir)) { var attachJoint = AttachJoints[0]; @@ -241,7 +277,7 @@ namespace Barotrauma { DeattachFromBody(reset: false); } - else + else if (attachCooldown <= 0.0f) { float squaredDistance = Vector2.DistanceSquared(character.SimPosition, _attachPos); float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.Radius, character.AnimController.Collider.Width), character.AnimController.Collider.Height) * 1.2f; @@ -259,6 +295,10 @@ namespace Barotrauma enemyAI.SteeringManager.SteeringSeek(_attachPos); } } + else if (IsAttached) + { + enemyAI.SteeringManager.Reset(); + } break; case AIState.Attack: case AIState.Aggressive: @@ -281,11 +321,11 @@ namespace Barotrauma if (IsAttached && targetBody != null && deattachCheckTimer <= 0.0f) { + attachCooldown = coolDown; bool deattach = false; if (maxAttachDuration > 0) { deattach = true; - attachCooldown = coolDown; } if (!deattach && TargetWall != null && TargetSubmarine != null) { @@ -294,7 +334,6 @@ namespace Barotrauma if (enemyAI.CanPassThroughHole(TargetWall, targetSection)) { deattach = true; - attachCooldown = coolDown; } if (!deattach) { @@ -327,7 +366,7 @@ namespace Barotrauma } } - private void AttachToBody(Vector2 attachPos) + public void AttachToBody(Vector2 attachPos, Vector2? forceAttachSurfaceNormal = null, Vector2? forceColliderSimPosition = null) { if (attachLimb == null) { return; } if (targetBody == null) { return; } @@ -343,6 +382,12 @@ namespace Barotrauma jointDir = attachLimb.Dir; + if (forceAttachSurfaceNormal.HasValue) { attachSurfaceNormal = forceAttachSurfaceNormal.Value; } + if (forceColliderSimPosition.HasValue) + { + character.TeleportTo(ConvertUnits.ToDisplayUnits(forceColliderSimPosition.Value)); + } + Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale; if (jointDir < 0.0f) { @@ -350,6 +395,9 @@ namespace Barotrauma } float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir; + //make sure the angle "has the same number of revolutions" as the reference limb + //(e.g. we don't want to rotate the legs to 0 if the torso is at 360, because that'd blow up the hip joints) + angle = attachLimb.body.WrapAngleToSameNumberOfRevolutions(angle); attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle); var limbJoint = new WeldJoint(attachLimb.body.FarseerBody, targetBody, @@ -392,10 +440,26 @@ namespace Barotrauma { deattachCheckTimer = maxAttachDuration; } + +#if SERVER + if (TargetCharacter != null) + { + GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, TargetCharacter, attachSurfaceNormal, attachPos)); + } + else if (TargetWall != null) + { + GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, TargetWall, attachSurfaceNormal, attachPos)); + } + else if (targetBody.UserData is Voronoi2.VoronoiCell cell) + { + GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, cell, attachSurfaceNormal, attachPos)); + } +#endif } public void DeattachFromBody(bool reset, float cooldown = 0) { + bool wasAttached = IsAttached; foreach (Joint joint in AttachJoints) { GameMain.World.Remove(joint); @@ -410,6 +474,12 @@ namespace Barotrauma { Reset(); } +#if SERVER + if (wasAttached) + { + GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData()); + } +#endif } private void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index c9dd03d88..d099b1d21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -294,6 +294,41 @@ namespace Barotrauma return Priority; } + /// + /// Get a normalized value representing how close the target position is. + /// The value is a rough estimation, where vertical movement is assumed to be more costly than horizontal. + /// + /// Position of the target + /// How much more costly vertical movement is than horizontal + /// Maximum distance, after which the factor will reach it's minimum value (= anything beyond this point is "as far as it can be"). + /// The factor at the maximum distance and beyond (= how "viable" very far-away targets should be considered). + /// The factor at the minimum distance (= how viable a target that's 0 units a way is considered). + public static float GetDistanceFactor(Vector2 selfPos, Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f) + { + float yDist = Math.Abs(selfPos.Y - targetWorldPos.Y); + yDist = yDist > 100 ? yDist * verticalDistanceMultiplier : 0; + float distance = Math.Abs(selfPos.X - targetWorldPos.X) + yDist; + float distanceFactor = MathHelper.Lerp(factorAtMinDistance, factorAtMaxDistance, MathUtils.InverseLerp(0, maxDistance, distance)); + return + factorAtMinDistance > factorAtMaxDistance ? + MathHelper.Clamp(distanceFactor, factorAtMaxDistance, factorAtMinDistance) : + MathHelper.Clamp(distanceFactor, factorAtMinDistance, factorAtMaxDistance); + } + + /// + /// Get a normalized value representing how close the target position is. + /// The value is a rough estimation, where vertical movement is assumed to be more costly than horizontal. + /// + /// Position of the target + /// How much more costly vertical movement is than horizontal + /// Maximum distance, after which the factor will reach it's minimum value (= anything beyond this point is "as far as it can be"). + /// The factor at the maximum distance and beyond (= how "viable" very far-away targets should be considered). + /// The factor at the minimum distance (= how viable a target that's 0 units a way is considered). + protected float GetDistanceFactor(Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f) + { + return GetDistanceFactor(character.WorldPosition, targetWorldPos, factorAtMaxDistance, verticalDistanceMultiplier, maxDistance, factorAtMinDistance); + } + private void UpdateDevotion(float deltaTime) { var currentObjective = objectiveManager.CurrentObjective; @@ -463,7 +498,7 @@ namespace Barotrauma { hasBeenChecked = true; CheckSubObjectives(); - if (subObjectives.None() || ConcurrentObjectives && subObjectives.All(so => so is AIObjectiveGoTo)) + if (subObjectives.None() || ConcurrentObjectives) { if (Check()) { @@ -509,7 +544,7 @@ namespace Barotrauma public virtual void SpeakAfterOrderReceived() { } - protected static bool CanEquip(Character character, Item item, bool allowWearing) + protected static bool CanPutInInventory(Character character, Item item, bool allowWearing) { if (item == null) { return false; } bool canEquip = false; @@ -550,6 +585,6 @@ namespace Barotrauma return canEquip && character.Inventory.CanBePut(item); } - protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing); + protected bool CanEquip(Item item, bool allowWearing) => CanPutInInventory(character, item, allowWearing); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index 45d99f33a..df9c74e24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -21,6 +21,11 @@ namespace Barotrauma private AIObjectiveDecontainItem decontainObjective; private int itemIndex = 0; + /// + /// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup) + /// + public override bool ConcurrentObjectives => true; + public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { @@ -39,10 +44,8 @@ namespace Barotrauma float distanceFactor = 0.9f; if (!IsPriority && item.CurrentHull != character.CurrentHull) { - float yDist = Math.Abs(character.WorldPosition.Y - item.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(character.WorldPosition.X - item.WorldPosition.X) + yDist; - distanceFactor = MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist)); + distanceFactor = GetDistanceFactor(item.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000, + factorAtMinDistance: 0.9f, factorAtMaxDistance: 0); } bool isSelected = character.HasItem(item); float selectedBonus = isSelected ? 100 - MaxDevotion : 0; @@ -116,7 +119,7 @@ namespace Barotrauma protected override bool CheckObjectiveSpecific() { - if (item.IgnoreByAI(character)) + if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item)) { Abandon = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 72277a406..8ac1bc914 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -56,8 +56,15 @@ namespace Barotrauma // The validity changes when a character picks the item up. if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); } if (target.CurrentHull.FireSources.Count > 0) { return false; } - // Don't clean up items in rooms that have enemies inside. - if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } + foreach (Character c in Character.CharacterList) + { + if (c == character || !HumanAIController.IsActive(c)) { continue; } + if (c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c)) + { + // Don't clean up items in rooms that have enemies inside. + return false; + } + } return true; } @@ -89,9 +96,10 @@ namespace Barotrauma IsItemInsideValidSubmarine(container, character) && !container.IsClaimedByBallastFlora; - public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) + public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true, bool requireValidContainer = true, bool ignoreItemsMarkedForDeconstruction = true) { if (item == null) { return false; } + if (item.DontCleanUp) { return false; } if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } if (item.ParentInventory != null) { @@ -101,8 +109,9 @@ namespace Barotrauma return false; } if (!allowUnloading) { return false; } - if (!IsValidContainer(item.Container, character)) { return false; } + if (requireValidContainer && !IsValidContainer(item.Container, character)) { return false; } } + if (ignoreItemsMarkedForDeconstruction && Item.DeconstructItems.Contains(item)) { return false; } if (!item.HasAccess(character)) { return false; } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } if (item.HasBallastFloraInHull) { return false; } @@ -121,11 +130,16 @@ namespace Barotrauma return false; } } + if (item.GetComponent() is { IsActive: true, Snapped: false }) + { + // Don't clean up spears with an active rope component. + return false; + } if (!checkInventory) { return true; } - return CanEquip(character, item, allowWearing: false); + return CanPutInInventory(character, item, allowWearing: false); } public override void OnDeselected() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 9bbb3836b..a31e90903 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -22,13 +22,13 @@ namespace Barotrauma private readonly CombatMode initialMode; private float checkWeaponsTimer; - private readonly float checkWeaponsInterval = 1; + private const float checkWeaponsInterval = 1; private float ignoreWeaponTimer; - private readonly float ignoredWeaponsClearTime = 10; + private const float ignoredWeaponsClearTime = 10; - private readonly float goodWeaponPriority = 30; + private const float goodWeaponPriority = 30; - private readonly float arrestHoldFireTime = 8; + private const float arrestHoldFireTime = 8; private float holdFireTimer; private bool hasAimed; private bool isLethalWeapon; @@ -79,14 +79,17 @@ namespace Barotrauma private bool canSeeTarget; private float visibilityCheckTimer; - private readonly float visibilityCheckInterval = 0.2f; + private const float visibilityCheckInterval = 0.2f; private float sqrDistance; - private readonly float maxDistance = 2000; - private readonly float distanceCheckInterval = 0.2f; + private const float maxDistance = 2000; + private const float distanceCheckInterval = 0.2f; private float distanceTimer; + + private const float closeDistanceThreshold = 300; + private const float floorHeightApproximate = 100; - public bool allowHoldFire; + public bool AllowHoldFire; /// /// Don't start using a weapon if this condition is true @@ -95,26 +98,63 @@ namespace Barotrauma public enum CombatMode { - Defensive, // Use weapons against the enemy, but try to retreat to a safe place - Offensive, // Engage the enemy and keep attacking it - Arrest, // Try to arrest the enemy without using lethal weapons (stunning + handcuffs) - Retreat, // Run to a safe place without attacking the target - None // Don't use + /// + /// Use weapons against the enemy, but try to retreat to a safe place. + /// + Defensive, + /// + /// Engage the enemy and keep attacking it. + /// + Offensive, + /// + /// Try to arrest the enemy without using lethal weapons (stunning + handcuffs). + /// + Arrest, + /// + /// Attempt to retreat to a safe place. Unlike in the Defensive mode, the character won't try to attack the enemy. + /// + Retreat, + /// + /// Does nothing. + /// + None } public CombatMode Mode { get; private set; } - private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest; + private bool IsOffensiveOrArrest => initialMode is CombatMode.Offensive or CombatMode.Arrest; private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator; private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead; private float AimSpeed => HumanAIController.AimSpeed; private float AimAccuracy => HumanAIController.AimAccuracy; - private bool IsEnemyCloserThan(float margin) => - Enemy != null && Enemy.CurrentHull != null && - character.InWater && Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin || - HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull) && Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X) < margin; + /// + /// This is just an approximation that attempts to take different rooms and floors into account. + /// It can be equal to a simple distance check, but when the target is nearby, we only use the horizontal axis. + /// It's used for checking whether the enemy is close in certain situations, not for checking the distance to the enemy in general. + /// + private bool IsEnemyClose(float margin) + { + if (Enemy == null) { return false; } + Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition; + if (character.CurrentHull != null && Enemy.CurrentHull != null && character.CurrentHull != Enemy.CurrentHull) + { + // Inside, not in the same hull with the enemy + if (Math.Abs(toEnemy.Y) > floorHeightApproximate) + { + // Different floor + return false; + } + if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) + { + // Potentially visible and on the same floor -> use only the horizontal distance. + return Math.Abs(toEnemy.X) < margin; + } + } + // Outside or inside in the same hull -> use the normal distance check. + return Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin; + } public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f) : base(character, objectiveManager, priorityModifier) @@ -147,7 +187,7 @@ namespace Barotrauma protected override float GetPriority() { - if (Enemy == null) + if (Enemy == null || Enemy.Removed) { Priority = 0; Abandon = true; @@ -169,9 +209,9 @@ namespace Barotrauma else { // 91-100 - float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1; - float maxPriority = AIObjectiveManager.MaxObjectivePriority; - float priorityScale = maxPriority - minPriority; + const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1; + const float maxPriority = AIObjectiveManager.MaxObjectivePriority; + const float priorityScale = maxPriority - minPriority; float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X); float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y); if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) @@ -208,12 +248,12 @@ namespace Barotrauma ignoredWeapons.Clear(); ignoreWeaponTimer = ignoredWeaponsClearTime; } - bool isCurrentObjective = objectiveManager.IsCurrentObjective(); - if (findSafety != null && isCurrentObjective) + bool isFightingIntruders = objectiveManager.IsCurrentObjective(); + if (findSafety != null && isFightingIntruders) { findSafety.Priority = 0; } - if (!AllowCoolDown && !character.IsOnPlayerTeam && !isCurrentObjective) + if (!AllowCoolDown && !character.IsOnPlayerTeam && !isFightingIntruders) { distanceTimer -= deltaTime; if (distanceTimer < 0) @@ -226,7 +266,7 @@ namespace Barotrauma protected override bool CheckObjectiveSpecific() { - if (character.Submarine == null || character.Submarine.TeamID != CharacterTeamType.FriendlyNPC) + if (character.Submarine is not { TeamID: CharacterTeamType.FriendlyNPC }) { // Can't lose the target in friendly outposts. if (sqrDistance > maxDistance * maxDistance) @@ -343,12 +383,15 @@ namespace Barotrauma RemoveSubObjective(ref seekAmmunitionObjective); return false; } - bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest; + bool isAllowedToSeekWeapons = character.IsHostileEscortee || character.IsPrisoner || // Prisoners and terrorists etc are always allowed to seek new weapons. + (character.IsInFriendlySub // Other characters need to be on a friendly sub in order to "know" where the weapons are. This also prevents NPCs "stealing" player items. + && IsOffensiveOrArrest // = Defensive or retreating AI shouldn't seek new weapons. + && !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something. if (checkWeaponsTimer < 0) { checkWeaponsTimer = checkWeaponsInterval; // First go through all weapons and try to reload without seeking ammunition - var allWeapons = FindWeaponsFromInventory(); + HashSet allWeapons = FindWeaponsFromInventory(); while (allWeapons.Any()) { Weapon = GetWeapon(allWeapons, out _weaponComponent); @@ -369,14 +412,20 @@ namespace Barotrauma // All good, the weapon is loaded break; } - if (Reload(seekAmmo: isAllowedToSeekWeapons)) + bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(closeDistanceThreshold); + if (Reload(seekAmmo: seekAmmo)) { // All good, we can use the weapon. break; } + else if (seekAmmunitionObjective != null) + { + // Seeking ammo. + break; + } else { - // No ammo. + // No ammo and should not try to seek ammo. allWeapons.Remove(WeaponComponent); Weapon = null; } @@ -409,16 +458,16 @@ namespace Barotrauma Mode = CombatMode.Retreat; } } - else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority))) + else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority && !IsEnemyClose(closeDistanceThreshold)))) { - // Poor weapon equipped -> try to find better. - RemoveSubObjective(ref seekAmmunitionObjective); + // No weapon or only a poor weapon equipped -> try to find better. RemoveSubObjective(ref retreatObjective); RemoveSubObjective(ref followTargetObjective); TryAddSubObjective(ref seekWeaponObjective, constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false) { AllowStealing = HumanAIController.IsMentallyUnstable, + AbortCondition = obj => IsEnemyClose(200), EvaluateCombatPriority = false, // Use a custom formula instead GetItemPriority = i => { @@ -427,7 +476,39 @@ namespace Barotrauma float priority = 0; if (GetWeaponComponent(i) is ItemComponent ic) { - priority = GetWeaponPriority(ic, prioritizeMelee: false, isCloseToEnemy: false, out _) / 100; + priority = GetWeaponPriority(ic, prioritizeMelee: false, canSeekAmmo: true, out _) / 100; + } + if (priority <= 0) { return 0; } + // Check that we are not running directly towards the enemy. + Vector2 toItem = i.WorldPosition - character.WorldPosition; + float range = HumanAIController.FindWeaponsRange; + if (range is > 0 and < float.PositiveInfinity) + { + // Y distance is irrelevant when we are on the same floor. If we are on a different floor, let's double it. + float yDiff = Math.Abs(toItem.Y) > floorHeightApproximate ? toItem.Y * 2 : 0; + Vector2 adjustedDiff = new Vector2(toItem.X, yDiff); + if (adjustedDiff.LengthSquared() > MathUtils.Pow2(range)) + { + // Too far -> not allowed to seek. + return 0; + } + } + Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition; + if (Math.Sign(toItem.X) == Math.Sign(toEnemy.X)) + { + // Going towards the enemy -> reduce the priority. + priority *= 0.5f; + } + if (i.CurrentHull != null && !HumanAIController.VisibleHulls.Contains(i.CurrentHull)) + { + if (Math.Abs(toItem.Y) > floorHeightApproximate && Math.Abs(toEnemy.Y) > floorHeightApproximate) + { + if (Math.Sign(toItem.Y) == Math.Sign(toEnemy.Y)) + { + // Different floor, at the direction of the enemy -> reduce the priority. + priority *= 0.75f; + } + } } return priority; } @@ -441,19 +522,19 @@ namespace Barotrauma SpeakNoWeapons(); Mode = CombatMode.Retreat; } - else + else if (!objectiveManager.HasActiveObjective()) { + // Poor weapon equipped Mode = CombatMode.Defensive; } }); } } - else + else if (seekAmmunitionObjective == null && seekWeaponObjective == null) { if (!CheckWeapon(seekAmmo: false)) { Weapon = null; - RemoveSubObjective(ref seekAmmunitionObjective); } } return Weapon != null; @@ -504,10 +585,14 @@ namespace Barotrauma item.GetComponent() ?? item.GetComponent() as ItemComponent; - private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool isCloseToEnemy, out float lethalDmg) + /// + /// Normal range of combat priority is 0-100, but the value is not clamped. + /// + private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool canSeekAmmo, out float lethalDmg) { lethalDmg = -1; float priority = weapon.CombatPriority; + if (priority <= 0) { return 0; } if (weapon is RepairTool repairTool) { switch (repairTool.UsableIn) @@ -531,9 +616,9 @@ namespace Barotrauma } if (weapon.IsEmpty(character)) { - if (weapon is RangedWeapon && isCloseToEnemy) + if (weapon is RangedWeapon && !canSeekAmmo) { - // Ignore weapons that don't have any ammunition (-> Don't seek ammo). + // Ignore weapons that don't have any ammunition, when we are not allowed to seek more ammo. return 0; } else @@ -605,7 +690,45 @@ namespace Barotrauma Attack attack = GetAttackDefinition(weapon); priority = attack?.GetTotalDamage() ?? priority / 2; } + // Reduce the priority of the weapon, if we don't have requires skills to use it. + float startPriority = priority; + var skillRequirementHints = weapon.Item.Prefab.SkillRequirementHints; + if (skillRequirementHints != null) + { + // If there are any skill requirement hints defined, let's use them. + // This should be the most accurate (manually defined) representation of the requirements (taking into account property conditionals etc). + foreach (SkillRequirementHint hint in skillRequirementHints) + { + float skillLevel = character.GetSkillLevel(hint.Skill); + float targetLevel = hint.Level; + priority = ReducePriority(priority, skillLevel, targetLevel); + } + } + else + { + // If no skill requirement hints are defined, let's rely on the required skill definition. + // This can be inaccurate in some cases (hmg, rifle), but in those cases there should be a skill requirement hint defined for the weapon. + foreach (Skill skill in weapon.RequiredSkills) + { + float skillLevel = character.GetSkillLevel(skill.Identifier); + // Skill multiplier is currently always 1, so it's not really needed, but that could change(?) + float targetLevel = skill.Level * weapon.GetSkillMultiplier(); + priority = ReducePriority(priority, skillLevel, targetLevel); + } + } + // Don't allow to reduce more than half, because an assault rifle is still an assault rifle, even in untrained hands. + priority = Math.Max(priority, startPriority / 2); return priority; + + float ReducePriority(float prio, float skillLevel, float targetLevel) + { + float diff = targetLevel - skillLevel; + if (diff > 0) + { + prio -= diff; + } + return prio; + } } private float ApproximateStunDamage(ItemComponent weapon, Attack attack) @@ -632,12 +755,12 @@ namespace Barotrauma return attack.Stun + afflictionsStun + effectsStun; } - private bool CanMeleeStunnerStun(ItemComponent weapon) + private static bool CanMeleeStunnerStun(ItemComponent weapon) { // If there's an item container that takes a battery, // assume that it's required for the stun effect // as we can't check the status effect conditions here. - var mobileBatteryTag = Tags.MobileBattery; + Identifier mobileBatteryTag = Tags.MobileBattery; var containers = weapon.Item.Components.Where(ic => ic is ItemContainer container && container.ContainableItemIdentifiers.Contains(mobileBatteryTag)); @@ -651,11 +774,11 @@ namespace Barotrauma weaponComponent = null; float bestPriority = 0; float lethalDmg = -1; - bool isCloseToEnemy = IsEnemyCloserThan(300); - bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character); + bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character); + bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(closeDistanceThreshold); foreach (var weapon in weaponList) { - float priority = GetWeaponPriority(weapon, prioritizeMelee, isCloseToEnemy, out lethalDmg); + float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg); if (priority > bestPriority) { weaponComponent = weapon; @@ -678,7 +801,7 @@ namespace Barotrauma } isLethalWeapon = lethalDmg > 1; } - if (allowHoldFire && !hasAimed && holdFireTimer <= 0) + if (AllowHoldFire && !hasAimed && holdFireTimer <= 0) { holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f); } @@ -699,15 +822,12 @@ namespace Barotrauma private static Attack GetAttackDefinition(ItemComponent weapon) { - Attack attack = null; - if (weapon is MeleeWeapon meleeWeapon) + Attack attack = weapon switch { - attack = meleeWeapon.Attack; - } - else if (weapon is RangedWeapon rangedWeapon) - { - attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack; - } + MeleeWeapon meleeWeapon => meleeWeapon.Attack, + RangedWeapon rangedWeapon => rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack, + _ => null + }; return attack; } @@ -726,7 +846,7 @@ namespace Barotrauma return weapons; } - private void GetWeapons(Item item, ICollection weaponList) + private static void GetWeapons(Item item, ICollection weaponList) { if (item == null) { return; } foreach (var component in item.Components) @@ -765,14 +885,13 @@ namespace Barotrauma } if (!character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType)) { - //clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter - character.ClearInput(InputType.Aim); - character.ClearInput(InputType.Shoot); + ClearInputs(); Weapon.TryInteract(character, forceSelectKey: true); - var slots = Weapon.AllowedSlots.Where(s => CharacterInventory.IsHandSlotType(s)); + var slots = Weapon.AllowedSlots.Where(CharacterInventory.IsHandSlotType); if (character.Inventory.TryPutItem(Weapon, character, slots)) { SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed); + SetReloadTime(WeaponComponent); } else { @@ -786,7 +905,7 @@ namespace Barotrauma } private float findHullTimer; - private readonly float findHullInterval = 1.0f; + private const float findHullInterval = 1.0f; private void Retreat(float deltaTime) { @@ -796,6 +915,18 @@ namespace Barotrauma } RemoveFollowTarget(); RemoveSubObjective(ref seekAmmunitionObjective); + if (retreatTarget != null) + { + if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) + { + // In the same hull with the enemy + if (retreatTarget == character.CurrentHull) + { + // Go elsewhere + retreatTarget = null; + } + } + } if (retreatObjective != null && retreatObjective.Target != retreatTarget) { RemoveSubObjective(ref retreatObjective); @@ -809,7 +940,7 @@ namespace Barotrauma SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2); return; } - if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted)) + if (retreatTarget == null || retreatObjective is { CanBeCompleted: false }) { if (findHullTimer > 0) { @@ -942,9 +1073,13 @@ namespace Barotrauma if (!arrestingRegistered && followTargetObjective != null) { followTargetObjective.CloseEnough = - WeaponComponent is RangedWeapon ? 1000 : - WeaponComponent is MeleeWeapon mw ? mw.Range : - WeaponComponent is RepairTool rt ? rt.Range : 50; + WeaponComponent switch + { + RangedWeapon => 1000, + MeleeWeapon mw => mw.Range, + RepairTool rt => rt.Range, + _ => 50 + }; } } @@ -976,9 +1111,8 @@ namespace Barotrauma foreach (var item in Enemy.Inventory.AllItemsMod) { if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound || - item.HasTag(Tags.Weapon) || - item.GetComponent() != null || - item.GetComponent() != null) + item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || + GetWeaponComponent(item) is { CombatPriority: > 0 }) { item.Drop(character); character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot); @@ -1024,10 +1158,11 @@ namespace Barotrauma RemoveSubObjective(ref retreatObjective); RemoveSubObjective(ref seekWeaponObjective); RemoveFollowTarget(); + var itemContainer = Weapon.GetComponent(); TryAddSubObjective(ref seekAmmunitionObjective, - constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent(), objectiveManager) + constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager) { - ItemCount = Weapon.GetComponent().Capacity * Weapon.GetComponent().MaxStackSize, + ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize, checkInventory = false, MoveWholeStack = true }, @@ -1052,9 +1187,9 @@ namespace Barotrauma // Eject empty ammo HumanAIController.UnequipEmptyItems(Weapon); ImmutableHashSet ammunitionIdentifiers = null; - if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) + if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { - foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) + foreach (RelatedItem requiredItem in WeaponComponent.RequiredItems[RelatedItem.RelationType.Contained]) { if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; } ammunitionIdentifiers = requiredItem.Identifiers; @@ -1075,12 +1210,14 @@ namespace Barotrauma if (ammunition != null) { var container = Weapon.GetComponent(); - if (!container.Inventory.TryPutItem(ammunition, user: character)) + if (container.Inventory.TryPutItem(ammunition, user: character)) { - if (ammunition.ParentInventory == character.Inventory) - { - ammunition.Drop(character); - } + ClearInputs(); + SetReloadTime(WeaponComponent); + } + else if (ammunition.ParentInventory == character.Inventory) + { + ammunition.Drop(character); } } } @@ -1127,7 +1264,7 @@ namespace Barotrauma } if (Weapon.RequireAimToUse) { - character.SetInput(InputType.Aim, false, true); + character.SetInput(InputType.Aim, hit: false, held: true); } hasAimed = true; if (holdFireTimer > 0) @@ -1194,23 +1331,17 @@ namespace Barotrauma float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy); if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor) { - if (myBodies == null) - { - myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); - } + myBodies ??= character.AnimController.Limbs.Select(l => l.body.FarseerBody); // Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay) var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter); foreach (var body in pickedBodies) { - Character target = null; - if (body.UserData is Character c) + Character target = body.UserData switch { - target = c; - } - else if (body.UserData is Limb limb) - { - target = limb.character; - } + Character c => c, + Limb limb => limb.character, + _ => null + }; if (target != null && target != Enemy && HumanAIController.IsFriendly(target)) { return; @@ -1225,26 +1356,48 @@ namespace Barotrauma { // Never allow to attack characters with deadly weapons while trying to arrest. if (Mode == CombatMode.Arrest && isLethalWeapon) { return; } - float reloadTime = 0; - if (WeaponComponent is RangedWeapon rangedWeapon) - { - // If the weapon is just equipped, we can't shoot just yet. - if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger) - { - reloadTime = rangedWeapon.Reload; - } - } - if (WeaponComponent is MeleeWeapon mw) - { - if (!((HumanoidAnimController)character.AnimController).Crouching) - { - reloadTime = mw.Reload; - } - } - character.SetInput(InputType.Shoot, false, true); + character.SetInput(InputType.Shoot, hit: false, held: true); Weapon.Use(deltaTime, user: character); + SetReloadTime(WeaponComponent); + } + + private float GetReloadTime(ItemComponent weaponComponent) + { + float reloadTime = 0; + switch (weaponComponent) + { + case RangedWeapon rangedWeapon: + { + if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger) + { + reloadTime = rangedWeapon.Reload; + } + break; + } + case MeleeWeapon mw: + { + if (character.AnimController is HumanoidAnimController { Crouching: false }) + { + reloadTime = mw.Reload; + } + break; + } + } + return reloadTime; + } + + private void SetReloadTime(ItemComponent weaponComponent) + { + float reloadTime = GetReloadTime(weaponComponent); reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed); } + + private void ClearInputs() + { + //clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter + character.ClearInput(InputType.Aim); + character.ClearInput(InputType.Shoot); + } private bool ShouldUnequipWeapon => Weapon != null && diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs new file mode 100644 index 000000000..80115f5c3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs @@ -0,0 +1,116 @@ +using Barotrauma.Items.Components; +using System.Linq; + +namespace Barotrauma +{ + class AIObjectiveDeconstructItem : AIObjective + { + public override Identifier Identifier { get; set; } = "deconstruct item".ToIdentifier(); + public override bool AllowWhileHandcuffed => false; + + public override bool AllowInFriendlySubs => true; + + public readonly Item Item; + + private Deconstructor deconstructor; + + private AIObjectiveDecontainItem decontainObjective; + + public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) + { + Item = item; + } + + protected override void Act(float deltaTime) + { + if (subObjectives.Any()) { return; } + + if (deconstructor == null) + { + deconstructor = FindDeconstructor(); + if (deconstructor == null) + { + Abandon = true; + return; + } + } + + TryAddSubObjective(ref decontainObjective, + constructor: () => new AIObjectiveDecontainItem(character, Item, objectiveManager, + sourceContainer: Item.Container?.GetComponent(), targetContainer: deconstructor.InputContainer, priorityModifier: PriorityModifier) + { + Equip = true, + RemoveExistingWhenNecessary = true + }, + onCompleted: () => + { + StartDeconstructor(); + //make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost + if (deconstructor.Item.Submarine is { Info.IsOutpost: true }) + { + HumanAIController.HandleRelocation(Item); + deconstructor.RelocateOutputToMainSub = true; + } + IsCompleted = true; + RemoveSubObjective(ref decontainObjective); + }, + onAbandon: () => + { + Abandon = true; + }); + } + + private Deconstructor FindDeconstructor() + { + Deconstructor closestDeconstructor = null; + float bestDistFactor = 0; + foreach (var otherItem in Item.ItemList) + { + var potentialDeconstructor = otherItem.GetComponent(); + if (potentialDeconstructor?.InputContainer == null) { continue; } + if (!potentialDeconstructor.InputContainer.Inventory.CanBePut(Item)) { continue; } + if (!potentialDeconstructor.Item.HasAccess(character)) { continue; } + float distFactor = GetDistanceFactor(Item.WorldPosition, potentialDeconstructor.Item.WorldPosition, factorAtMaxDistance: 0.2f); + if (distFactor > bestDistFactor) + { + closestDeconstructor = potentialDeconstructor; + bestDistFactor = distFactor; + } + } + return closestDeconstructor; + } + + private void StartDeconstructor() + { + deconstructor.SetActive(active: true, user: character, createNetworkEvent: true); + } + + protected override bool CheckObjectiveSpecific() + { + if (Item.IgnoreByAI(character)) + { + Abandon = true; + } + else if (deconstructor != null && deconstructor.Item.IgnoreByAI(character)) + { + Abandon = true; + } + return !Abandon && IsCompleted; + } + + public override void Reset() + { + base.Reset(); + decontainObjective = null; + } + + public void DropTarget() + { + if (Item != null && character.HasItem(Item)) + { + Item.Drop(character); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs new file mode 100644 index 000000000..fbb83b3be --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs @@ -0,0 +1,122 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using System.Collections.Generic; + +namespace Barotrauma +{ + class AIObjectiveDeconstructItems : AIObjectiveLoop + { + public override Identifier Identifier { get; set; } = "deconstruct items".ToIdentifier(); + + //Clear periodically, because we may ending up ignoring items when all deconstructors are full + protected override float IgnoreListClearInterval => 30; + + public override bool AllowInFriendlySubs => true; + + protected override int MaxTargets => 10; + + private bool checkedDeconstructorExists; + + public AIObjectiveDeconstructItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) + { + } + + public override void OnSelected() + { + base.OnSelected(); + if (!checkedDeconstructorExists) + { + if (character.Submarine == null || + Item.ItemList.None(it => + it.GetComponent() != null && + it.IsInteractable(character) && + character.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true, allowDifferentTeam: true, allowDifferentType: true))) + { + character.Speak(TextManager.Get("orderdialogself.deconstructitem.nodeconstructor").Value, delay: 5.0f, + identifier: "nodeconstructor".ToIdentifier(), minDurationBetweenSimilar: 30.0f); + Abandon = true; + } + checkedDeconstructorExists = true; + } + } + + public override void Reset() + { + base.Reset(); + checkedDeconstructorExists = false; + } + + protected override float TargetEvaluation() + { + if (Targets.None()) { return 0; } + if (objectiveManager.IsOrder(this)) + { + return objectiveManager.GetOrderPriority(this); + } + return AIObjectiveManager.RunPriority - 0.5f; + } + + protected override bool Filter(Item target) + { + // If the target was selected as a valid target, we'll have to accept it so that the objective can be completed. + // The validity changes when a character picks the item up. + if (!IsValidTarget(target, character, checkInventory: true)) + { + return Objectives.ContainsKey(target) && AIObjectiveCleanupItems.IsItemInsideValidSubmarine(target, character); + } + if (target.CurrentHull.FireSources.Count > 0) { return false; } + + foreach (Character c in Character.CharacterList) + { + if (c == character || !HumanAIController.IsActive(c)) { continue; } + if (c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c)) + { + // Don't deconstruct items in rooms that have enemies inside. + return false; + } + else if (c.TeamID == character.TeamID && c.AIController is HumanAIController humanAi) + { + if (humanAi.ObjectiveManager.CurrentObjective is AIObjectiveDeconstructItem deconstruct && deconstruct.Item == target) + { + return false; + } + } + } + return true; + } + + protected override IEnumerable GetList() => Item.DeconstructItems; + + protected override AIObjective ObjectiveConstructor(Item item) + => new AIObjectiveDeconstructItem(item, character, objectiveManager, priorityModifier: PriorityModifier); + + protected override void OnObjectiveCompleted(AIObjective objective, Item target) + => HumanAIController.RemoveTargets(character, target); + + private static bool IsValidTarget(Item item, Character character, bool checkInventory) + { + if (item == null) { return false; } + if (item.GetRootInventoryOwner() == character) { return true; } + return AIObjectiveCleanupItems.IsValidTarget( + item, + character, + checkInventory, + allowUnloading: true, + requireValidContainer: false, + ignoreItemsMarkedForDeconstruction: false); + } + + public override void OnDeselected() + { + base.OnDeselected(); + foreach (var subObjective in SubObjectives) + { + if (subObjective is AIObjectiveDeconstructItem deconstructObjective) + { + deconstructObjective.DropTarget(); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index d8cfdf3fb..f3a5c130c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -39,6 +39,9 @@ namespace Barotrauma /// public bool DropIfFails { get; set; } = true; + /// + /// Should existing item(s) be removed from the targetContainer if the targetItem won't fit otherwise? + /// public bool RemoveExistingWhenNecessary { get; set; } public Func RemoveExistingPredicate { get; set; } public int? RemoveExistingMax { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 7cd20e569..b3d97d251 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -45,13 +45,18 @@ namespace Barotrauma else { float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; - float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 3 : 0; - float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist; - float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); - if (targetHull == character.CurrentHull || HumanAIController.VisibleHulls.Contains(targetHull)) + + float distanceFactor = 1.0f; + if (targetHull != character.CurrentHull && + !HumanAIController.VisibleHulls.Contains(targetHull)) { - distanceFactor = 1; + distanceFactor = + GetDistanceFactor( + new Vector2(character.WorldPosition.Y, characterY), + targetHull.WorldPosition, + verticalDistanceMultiplier: 3, + maxDistance: 5000, + factorAtMaxDistance: 0.1f); } float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull); if (severity > 0.75f && !isOrder && diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index a81367582..ad90d4b31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -12,7 +12,7 @@ namespace Barotrauma protected override float TargetUpdateTimeMultiplier => 0.2f; - public bool TargetCharactersInOtherSubs { get; set; } + public bool TargetCharactersInOtherSubs { get; init; } public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } @@ -33,20 +33,22 @@ namespace Barotrauma protected override AIObjective ObjectiveConstructor(Character target) { - AIObjectiveCombat.CombatMode combatMode = ShouldArrest(target, character) ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive; - var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier); - if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) + AIObjectiveCombat.CombatMode combatMode = AIObjectiveCombat.CombatMode.Offensive; + if (character.IsOnPlayerTeam && target is { IsEscorted: true }) { - if (campaign.CurrentLocation is { IsFactionHostile: true }) + // Try to arrest escorted characters, instead of killing them. + combatMode = AIObjectiveCombat.CombatMode.Arrest; + } + var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier); + if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode { CurrentLocation.IsFactionHostile: true }) + { + combatObjective.holdFireCondition = () => { - combatObjective.holdFireCondition = () => - { - //hold fire while the enemy is in the airlock (except if they've attacked us) - if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; } - return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock"); - }; - character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f); - } + //hold fire while the enemy is in the airlock (except if they've attacked us) + if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; } + return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock"); + }; + character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f); } return combatObjective; } @@ -77,10 +79,5 @@ namespace Barotrauma if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; } return true; } - - public static bool ShouldArrest(Character target, Character character) - { - return target != null && target.IsEscorted && character.TeamID == CharacterTeamType.Team1; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 19fb5d725..3cfecafbe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -33,22 +33,27 @@ namespace Barotrauma protected override void Act(float deltaTime) { - TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true)); + TrySetTargetItem(character.Inventory.FindItem(it => it.HasTag(gearTag) && IsSuitablePressureProtection(it, gearTag, character), true)); if (targetItem == null && gearTag == Tags.LightDivingGear) { - TrySetTargetItem(character.Inventory.FindItemByTag(Tags.HeavyDivingGear, true)); + TrySetTargetItem(character.Inventory.FindItem( + it => it.HasTag(Tags.HeavyDivingGear) && IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true)); } if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) && targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it))) { + bool mustFindMorePressureProtection = + !objectiveManager.FailedToFindDivingGearForDepth && + character.Inventory.FindItem( + it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null; TryAddSubObjective(ref getDivingGear, () => { if (targetItem == null && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f); } - return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) + var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _), AllowToFindDivingGear = false, @@ -56,8 +61,42 @@ namespace Barotrauma EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head, Wear = true }; + if (gearTag == Tags.HeavyDivingGear) + { + if (mustFindMorePressureProtection) + { + //if we're looking for a suit specifically because the current suit isn't enough, + //let's ignore unsuitable suits altogether... + getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character); + } + else + { + //...Otherwise it's fine to give a very small priority + //to inadequate suits (a suit not adequate for the depth is better than no suit) + getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f; + } + getItemObjective.GetItemPriority = it => + { + if (IsSuitablePressureProtection(it, gearTag, character)) + { + return 1000.0f; + } + else + { + //if we're looking for a suit specifically because the current suit isn't enough, + //let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority + //to inadequate suits (a suit not adequate for the depth is better than no suit) + return mustFindMorePressureProtection ? 0.0f : 1.0f; + } + }; + } + return getItemObjective; }, - onAbandon: () => Abandon = true, + onAbandon: () => + { + if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; } + Abandon = true; + }, onCompleted: () => { RemoveSubObjective(ref getDivingGear); @@ -160,6 +199,20 @@ namespace Barotrauma } } + public static bool IsSuitablePressureProtection(Item item, Identifier tag, Character character) + { + if (tag == Tags.HeavyDivingGear) + { + float realWorldDepth = Level.Loaded?.GetRealWorldDepth(character.WorldPosition.Y) ?? 0.0f; + if (item.GetComponent() is not { } wearable || wearable.PressureProtection < realWorldDepth + Steering.PressureWarningThreshold) + { + return false; + } + } + return true; + } + + private bool IsSuitableContainedOxygenSource(Item item) { return diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 253dc0e5c..6ef4b1e76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -52,12 +52,26 @@ namespace Barotrauma { bool isSuffocatingInDivingSuit = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); static bool IsSuffocatingWithoutDivingGear(Character c) => c.IsLowInOxygen && c.AnimController.HeadInWater && !HumanAIController.HasDivingGear(c, requireOxygenTank: true); - if (isSuffocatingInDivingSuit || - NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)) || - (!objectiveManager.HasActiveObjective() && IsSuffocatingWithoutDivingGear(character))) + + if (isSuffocatingInDivingSuit || (!objectiveManager.HasActiveObjective() && IsSuffocatingWithoutDivingGear(character))) { Priority = AIObjectiveManager.MaxObjectivePriority; } + else if (NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character))) + { + if (objectiveManager.FailedToFindDivingGearForDepth && + HumanAIController.HasDivingSuit(character, requireSuitablePressureProtection: false)) + { + //we have a suit that's not suitable for the pressure, + //but we've failed to find a better one + // shit, not much we can do here, let's just allow the bot to get on with their current objective + Priority = 0; + } + else + { + Priority = AIObjectiveManager.MaxObjectivePriority; + } + } else if ((objectiveManager.IsCurrentOrder() || objectiveManager.IsCurrentOrder()) && character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID)) { @@ -259,7 +273,7 @@ namespace Barotrauma bool inFriendlySub = character.IsInFriendlySub || (character.IsEscorted && character.IsInPlayerSub); - if (cannotFindSafeHull && !inFriendlySub && objectiveManager.Objectives.None(o => o is AIObjectiveReturn)) + if (cannotFindSafeHull && !inFriendlySub && character.IsOnPlayerTeam && objectiveManager.Objectives.None(o => o is AIObjectiveReturn)) { if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab)) { @@ -401,10 +415,7 @@ namespace Barotrauma if (isCharacterInside) { hullSafety = HumanAIController.GetHullSafety(potentialHull, potentialHull.GetConnectedHulls(true, 1), character); - float yDist = Math.Abs(character.WorldPosition.Y - potentialHull.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 3 : 0; - float dist = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist; - float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist)); + float distanceFactor = GetDistanceFactor(potentialHull.WorldPosition, factorAtMaxDistance: 0.9f); hullSafety *= distanceFactor; //skip the hull if the safety is already less than the best hull //(no need to do the expensive pathfinding if we already know we're not going to choose this hull) @@ -446,16 +457,13 @@ namespace Barotrauma hullSafety = 100; hullIsAirlock = true; } - else if(!bestHullIsAirlock && potentialHull.LeadsOutside(character)) + else if (!bestHullIsAirlock && potentialHull.LeadsOutside(character)) { hullSafety = 100; } float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; - float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 3 : 0; - float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist; // Huge preference for closer targets - float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance)); + float distanceFactor = GetDistanceFactor(new Vector2(character.WorldPosition.X, characterY), potentialHull.WorldPosition, factorAtMaxDistance: 0.2f); hullSafety *= distanceFactor; // If the target is not inside a friendly submarine, considerably reduce the hull safety. // Intentionally exclude wrecks from this check diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs index f6ac0bec8..2d7081475 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs @@ -39,8 +39,8 @@ namespace Barotrauma if (campaign.Map?.CurrentLocation?.Reputation is { } reputation) { return MathHelper.Lerp( - campaign.Settings.MaxStolenItemInspectionProbability, - campaign.Settings.MinStolenItemInspectionProbability, + campaign.Settings.PatdownProbabilityMax, + campaign.Settings.PatdownProbabilityMin, reputation.NormalizedValue); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index b4c28bfb7..a065627fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -120,7 +120,7 @@ namespace Barotrauma Abandon = true; return; } - if (weldingTool.OwnInventory == null && repairTool.requiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained)) + if (weldingTool.OwnInventory == null && repairTool.RequiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained)) { #if DEBUG DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no proper inventory"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 963be9a2b..2335d8f3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -159,13 +159,14 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (IdentifiersOrTags != null && !isDoneSeeking) + if (IdentifiersOrTags != null) { if (checkInventory) { if (CheckInventory()) { isDoneSeeking = true; + itemCandidates.Clear(); } } if (!isDoneSeeking) @@ -189,7 +190,14 @@ namespace Barotrauma } } FindTargetItem(); - if (!objectiveManager.IsCurrentOrder()) + } + if (targetItem == null) + { + if (isDoneSeeking) + { + HandlePotentialItems(); + } + if (objectiveManager.CurrentOrder is not AIObjectiveGoTo) { objectiveManager.GetObjective().Wander(deltaTime); } @@ -201,20 +209,28 @@ namespace Barotrauma Abandon = true; return; } - if (targetItem == null || targetItem.Removed) + bool ShouldAbort() => IdentifiersOrTags is null || isDoneSeeking && itemCandidates.None(); + if (targetItem is null or { Removed: true }) { + if (ShouldAbort()) + { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red); + DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red); #endif - Abandon = true; + Abandon = true; + } return; } - else if (isDoneSeeking && moveToTarget == null) + if (moveToTarget is null) { + if (ShouldAbort()) + { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red); + DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red); #endif - Abandon = true; + Abandon = true; + return; + } return; } if (character.IsItemTakenBySomeoneElse(targetItem)) @@ -399,16 +415,8 @@ namespace Barotrauma { StopWatch.Restart(); } - float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100); - if (!CheckPathForEachItem) - { - // While following the player, let's ensure that there's a valid path to the target before accepting it. - // Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can. - // This is relatively expensive, so let's do this only when it significantly improves the behavior. - // Only allow one path find call per frame. - CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrder); - } - bool checkPath = CheckPathForEachItem; + float priority = objectiveManager.GetCurrentPriority(); + bool checkPath = CheckPathForEachItem || priority >= AIObjectiveManager.RunPriority || ItemCount > 1; // Reset if the character has switched subs. if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true)) { @@ -434,9 +442,9 @@ namespace Barotrauma // Ignore items in the inventory when defined not to check it. if (item.IsOwnedBy(character)) { continue; } } - if (!AllowStealing) + if (!AllowStealing && character.IsOnPlayerTeam) { - if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInCurrentOutpost) { continue; } + if (item.SpawnedInCurrentOutpost && !item.AllowStealing) { continue; } } if (!CheckItem(item)) { continue; } if (item.Container != null) @@ -454,11 +462,11 @@ namespace Barotrauma if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; } } float itemPriority = item.Prefab.BotPriority; - if (itemPriority <= 0) { continue; } if (GetItemPriority != null) { itemPriority *= GetItemPriority(item); } + if (itemPriority <= 0) { continue; } Entity rootInventoryOwner = item.GetRootInventoryOwner(); if (rootInventoryOwner is Item ownerItem) { @@ -474,11 +482,13 @@ namespace Barotrauma } } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; - float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y); - yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist; - float minDistFactor = EvaluateCombatPriority ? 0.1f : 0; - float distanceFactor = MathHelper.Lerp(1, minDistFactor, MathUtils.InverseLerp(100, 10000, dist)); + float distanceFactor = + GetDistanceFactor( + itemPos, + verticalDistanceMultiplier: 5, + maxDistance: 10000, + factorAtMinDistance: 1.0f, + factorAtMaxDistance: EvaluateCombatPriority ? 0.1f : 0); itemPriority *= distanceFactor; if (EvaluateCombatPriority) { @@ -510,7 +520,7 @@ namespace Barotrauma } else { - combatFactor = Math.Min(item.Components.Sum(ic => AIObjectiveCombat.GetLethalDamage(ic)) / 1000, 0.1f); + combatFactor = Math.Min(item.Components.Sum(AIObjectiveCombat.GetLethalDamage) / 1000, 0.1f); } itemPriority *= combatFactor; } @@ -518,10 +528,6 @@ namespace Barotrauma { itemPriority *= item.Condition / item.MaxCondition; } - if (checkPath) - { - itemCandidates.Add((item, itemPriority)); - } // Ignore if the item has a lower priority than the currently selected one if (itemPriority < currItemPriority) { continue; } if (EvaluateCombatPriority && itemPriority <= 0) @@ -529,23 +535,27 @@ namespace Barotrauma // Not good enough continue; } - currItemPriority = itemPriority; - targetItem = item; - moveToTarget = rootInventoryOwner ?? item; + if (checkPath) + { + itemCandidates.Add((item, itemPriority)); + } + else + { + currItemPriority = itemPriority; + targetItem = item; + moveToTarget = rootInventoryOwner ?? item; + } } if (currentSearchIndex >= itemList.Count - 1) { isDoneSeeking = true; - } - if (checkedItems > 0) - { - if (isDoneSeeking && itemCandidates.Any()) + if (itemCandidates.Any()) { itemCandidates.Sort((x, y) => y.priority.CompareTo(x.priority)); } - if (HumanAIController.DebugAI && targetItem != null && StopWatch.ElapsedMilliseconds > 2) - { - var msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}"; + if (HumanAIController.DebugAI && StopWatch.ElapsedMilliseconds > 2) + { + string msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem?.Name ?? "NULL"} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}"; if (StopWatch.ElapsedMilliseconds > 5) { DebugConsole.ThrowError(msg); @@ -557,60 +567,66 @@ namespace Barotrauma } } } - if (isDoneSeeking) + } + + private void HandlePotentialItems() + { + Debug.Assert(isDoneSeeking); + if (itemCandidates.Any()) { if (PathSteering == null) { itemCandidates.Clear(); + Abandon = true; + return; } - if (itemCandidates.Any()) + if (itemCandidates.FirstOrDefault() is var itemCandidate) { - if (itemCandidates.FirstOrDefault() is { } itemCandidate) + var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); + if (path.Unreachable) { - var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); - if (path.Unreachable) - { - // Remove the invalid candidates and continue on the next frame. - itemCandidates.Remove(itemCandidate); - } - else - { - // The path was valid -> we are done. - itemCandidates.Clear(); - } - } - } - if (targetItem == null && itemCandidates.None()) - { - if (spawnItemIfNotFound) - { - ItemPrefab prefab = FindItemToSpawn(); - if (prefab == null) - { -#if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); -#endif - Abandon = true; - } - else - { - Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) => - { - targetItem = spawnedItem; - if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false)) - { - spawnedItem.SpawnedInCurrentOutpost = true; - } - }); - } + // Remove the invalid candidates and continue on the next frame. + itemCandidates.Remove(itemCandidate); } else { + // The path was valid -> we are done. + itemCandidates.Clear(); + targetItem = itemCandidate.item; + moveToTarget = targetItem.GetRootInventoryOwner() ?? targetItem; + } + } + } + if (targetItem == null) + { + if (spawnItemIfNotFound) + { + ItemPrefab prefab = FindItemToSpawn(); + if (prefab == null) + { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); #endif Abandon = true; } + else + { + Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) => + { + targetItem = spawnedItem; + if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false)) + { + spawnedItem.SpawnedInCurrentOutpost = true; + } + }); + } + } + else + { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow); +#endif + Abandon = true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs index 5c909a4a0..afd5287e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -3,6 +3,7 @@ using Barotrauma.Extensions; using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; +using System; namespace Barotrauma { @@ -24,6 +25,12 @@ namespace Barotrauma public bool CheckPathForEachItem { get; set; } public bool RequireNonEmpty { get; set; } public bool RequireAllItems { get; set; } + public bool RequireDivingSuitAdequate { get; set; } + + /// + /// T1 = item to check, T2 = tag we're trying to find a suitable item for + /// + public Func? ItemFilter; private readonly ImmutableArray gearTags; private readonly ImmutableHashSet ignoredTags; @@ -48,7 +55,8 @@ namespace Barotrauma int count = gearTags.Count(t => t == tag); AIObjectiveGetItem? getItem = null; TryAddSubObjective(ref getItem, () => - new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1) + { + var getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1) { AllowVariants = AllowVariants, Wear = Wear, @@ -58,29 +66,36 @@ namespace Barotrauma CheckPathForEachItem = CheckPathForEachItem, RequireNonEmpty = RequireNonEmpty, ItemCount = count, - SpeakIfFails = RequireAllItems - }, - onCompleted: () => + SpeakIfFails = RequireAllItems, + + }; + if (ItemFilter != null) { - var item = getItem?.TargetItem; - if (item?.IsOwnedBy(character) != null) - { - achievedItems.Add(item); - } - }, - onAbandon: () => + getItem.ItemFilter = (Item it) => ItemFilter(it, tag); + } + return getItem; + }, + onCompleted: () => + { + var item = getItem?.TargetItem; + if (item?.IsOwnedBy(character) != null) { - var item = getItem?.TargetItem; - if (item != null) - { - achievedItems.Remove(item); - } - RemoveSubObjective(ref getItem); - if (RequireAllItems) - { - Abandon = true; - } - }); + achievedItems.Add(item); + } + }, + onAbandon: () => + { + var item = getItem?.TargetItem; + if (item != null) + { + achievedItems.Remove(item); + } + RemoveSubObjective(ref getItem); + if (RequireAllItems) + { + Abandon = true; + } + }); } subObjectivesCreated = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index c5b039824..06f4b7b4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -85,6 +85,8 @@ namespace Barotrauma public bool IgnoreIfTargetDead { get; set; } public bool AllowGoingOutside { get; set; } + public bool FaceTargetOnCompleted { get; set; } = true; + public bool AlwaysUseEuclideanDistance { get; set; } = true; /// @@ -324,7 +326,7 @@ namespace Barotrauma float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character); if (tryToGetDivingSuit) { - needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen); + needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth); } else if (tryToGetDivingGear) { @@ -346,26 +348,26 @@ namespace Barotrauma TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager), onAbandon: () => { - cantFindDivingGear = true; - if (needsDivingSuit) - { - // Shouldn't try to reach the target without a suit, because it's lethal. - Abandon = true; - } - else - { - // Try again without requiring the diving suit - RemoveSubObjective(ref findDivingGear); - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), - onAbandon: () => - { - Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); - RemoveSubObjective(ref findDivingGear); - }, - onCompleted: () => - { - RemoveSubObjective(ref findDivingGear); - }); + cantFindDivingGear = true; + if (needsDivingSuit) + { + // Shouldn't try to reach the target without a suit, because it's lethal. + Abandon = true; + } + else + { + // Try again without requiring the diving suit + RemoveSubObjective(ref findDivingGear); + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), + onAbandon: () => + { + Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); + RemoveSubObjective(ref findDivingGear); + }, + onCompleted: () => + { + RemoveSubObjective(ref findDivingGear); + }); } }, onCompleted: () => RemoveSubObjective(ref findDivingGear)); @@ -450,10 +452,8 @@ namespace Barotrauma { useScooter = false; checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f); - Identifier scooterTag = "scooter".ToIdentifier(); - Identifier batteryTag = "mobilebattery".ToIdentifier(); Item scooter = null; - bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false); + bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(Tags.Scooter, allowBroken: false); if (!shouldUseScooter) { float threshold = 500; @@ -467,7 +467,7 @@ namespace Barotrauma shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold; } } - if (HumanAIController.HasItem(character, scooterTag, out IEnumerable equippedScooters, recursive: false, requireEquipped: true)) + if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable equippedScooters, recursive: false, requireEquipped: true)) { // Currently equipped scooter scooter = equippedScooters.FirstOrDefault(); @@ -477,23 +477,23 @@ namespace Barotrauma var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand); var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand); bool handsFull = - (leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) || - (rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem)); + (leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem) && !character.Inventory.TryPutItem(leftHandItem, character, InvSlotType.Bag.ToEnumerable())) || + (rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem) && !character.Inventory.TryPutItem(rightHandItem, character, InvSlotType.Bag.ToEnumerable())); if (!handsFull) { bool hasBattery = false; - if (HumanAIController.HasItem(character, scooterTag, out IEnumerable nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false)) + if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable nonEquippedScooters, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false)) { // Non-equipped scooter with a battery scooter = nonEquippedScooters.FirstOrDefault(); hasBattery = true; } - else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable _nonEquippedScooters, requireEquipped: false)) + else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable _nonEquippedScooters, requireEquipped: false)) { // Non-equipped scooter without a battery scooter = _nonEquippedScooters.FirstOrDefault(); // Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this. - hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false); + hasBattery = HumanAIController.HasItem(character, Tags.MobileBattery, out _, requireEquipped: false, conditionPercentage: 1, recursive: false); } if (scooter != null && hasBattery) { @@ -511,7 +511,7 @@ namespace Barotrauma if (scooter.ContainedItems.None(i => i.Condition > 0)) { // Try to switch batteries - if (HumanAIController.HasItem(character, batteryTag, out IEnumerable batteries, conditionPercentage: 1, recursive: false)) + if (HumanAIController.HasItem(character, Tags.MobileBattery, out IEnumerable batteries, conditionPercentage: 1, recursive: false)) { scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot)); if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character)) @@ -811,16 +811,15 @@ namespace Barotrauma private void StopMovement() { SteeringManager?.Reset(); - if (Target != null) + if (FaceTargetOnCompleted && Target is Entity { Removed: false }) { - character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left; + HumanAIController.FaceTarget(Target); } } protected override void OnCompleted() { StopMovement(); - HumanAIController.FaceTarget(Target); if (Target is WayPoint { Ladders: null }) { // Release ladders when ordered to wait at a spawnpoint. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 1e22487b1..4845307ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -454,14 +454,16 @@ namespace Barotrauma { targetHulls.Add(hull); float weight = hull.RectWidth; - // Prefer rooms that are closer. Avoid rooms that are not in the same level. - // If the behavior is active, prefer rooms that are not close. - float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist; - float distanceFactor = behavior == BehaviorType.Patrol ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(2500, 0, dist)) : MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist)); + float distanceFactor = GetDistanceFactor(hull.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 2500, + factorAtMinDistance: 1, factorAtMaxDistance: 0); + if (behavior == BehaviorType.Patrol) + { + //invert when patrolling (= prefer travelling to far-away hulls) + distanceFactor = 1.0f - distanceFactor; + } float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2)); weight *= distanceFactor * waterFactor; + System.Diagnostics.Debug.Assert(weight >= 0); hullWeights.Add(weight); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index 74ac238a2..1150c17a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -193,7 +193,10 @@ namespace Barotrauma if (yDist > 100) { dist += yDist * 5; } dist += Math.Abs(character.WorldPosition.X - targetPos.X); } - float distanceFactor = dist > 0.0f ? MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist)) : 0.9f; + + float distanceFactor = + GetDistanceFactor(targetItem.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000, factorAtMinDistance: 0.9f, factorAtMaxDistance: 0); + bool hasContainable = character.HasItem(targetItem); float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100; float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 4b4433332..fa3b1d731 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -67,6 +67,10 @@ namespace Barotrauma } private AIObjective currentOrder; public AIObjective ForcedOrder { get; private set; } + + /// + /// Includes orders. + /// public AIObjective CurrentObjective { get; private set; } public AIObjectiveManager(Character character) @@ -104,6 +108,8 @@ namespace Barotrauma public Dictionary DelayedObjectives { get; private set; } = new Dictionary(); public bool FailedAutonomousObjectives { get; private set; } + public bool FailedToFindDivingGearForDepth; + private void ClearIgnored() { if (character.AIController is HumanAIController humanAi) @@ -220,8 +226,11 @@ namespace Barotrauma if (previousObjective == CurrentObjective) { return CurrentObjective; } previousObjective?.OnDeselected(); - CurrentObjective?.OnSelected(); - GetObjective().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0)); + if (CurrentObjective != null) + { + CurrentObjective.OnSelected(); + GetObjective().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0)); + } if (GameMain.NetworkMember is { IsServer: true }) { GameMain.NetworkMember.CreateEntityEvent(character, @@ -230,9 +239,14 @@ namespace Barotrauma return CurrentObjective; } + /// + /// Returns the highest priority of the current objective and its subobjectives. + /// public float GetCurrentPriority() { - return CurrentObjective == null ? 0.0f : CurrentObjective.Priority; + if (CurrentObjective == null) { return 0; } + float subObjectivePriority = CurrentObjective.SubObjectives.Any() ? CurrentObjective.SubObjectives.Max(so => so.Priority) : 0; + return Math.Max(CurrentObjective.Priority, subObjectivePriority); } public void UpdateObjectives(float deltaTime) @@ -241,7 +255,7 @@ namespace Barotrauma if (CurrentOrders.Any()) { - foreach(var order in CurrentOrders) + foreach (var order in CurrentOrders) { var orderObjective = order.Objective; UpdateOrderObjective(orderObjective); @@ -396,6 +410,9 @@ namespace Barotrauma } } + //reset this here so the bots can retry finding a better suit if it's needed for the new order + FailedToFindDivingGearForDepth = false; + var newCurrentObjective = CreateObjective(order); if (newCurrentObjective != null) { @@ -592,6 +609,9 @@ namespace Barotrauma case "loaditems": newObjective = new AIObjectiveLoadItems(character, this, order.Option, order.GetTargetItems(order.Option), order.TargetEntity as Item, priorityModifier); break; + case "deconstructitems": + newObjective = new AIObjectiveDeconstructItems(character, this, priorityModifier); + break; default: if (order.TargetItemComponent == null) { return null; } if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } @@ -613,6 +633,11 @@ namespace Barotrauma return newObjective; } + /// + /// Sets the order as dismissed, and enables the option to reissue the order on the crew list. + /// Note that this is not the same thing as just removing the order entirely! + /// + /// private void DismissSelf(Order order) { var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order.Identifier, order.Option)); @@ -651,13 +676,27 @@ namespace Barotrauma return true; } + /// + /// Only checks the current order. Deprecated, use pattern matching instead. + /// public bool IsCurrentOrder() where T : AIObjective => CurrentOrder is T; + /// + /// Checks the current objective (which can be an order too). Deprecated, use pattern matching instead. + /// public bool IsCurrentObjective() where T : AIObjective => CurrentObjective is T; - public bool IsActiveObjective() where T : AIObjective => GetActiveObjective() is T; public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); + + /// + /// Return the first order whose objective is of the given type. Can return null. + /// public T GetOrder() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T; + /// + /// Return the first order with the specified objective. Can return null. + /// + public Order GetOrder(AIObjective objective) => CurrentOrders.FirstOrDefault(o => o.Objective == objective); + public T GetLastActiveObjective() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; @@ -665,12 +704,12 @@ namespace Barotrauma => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T; /// - /// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently. + /// Returns all active objectives of the specific type. /// public IEnumerable GetActiveObjectives() where T : AIObjective { if (CurrentObjective == null) { return Enumerable.Empty(); } - return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T); + return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).OfType(); } public bool HasActiveObjective() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 2e35e6ff0..d9bfcfe0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -211,6 +211,10 @@ namespace Barotrauma return; } } + + //the character shouldn't be grabbing anyone if it's trying to operate an item + character.SelectedCharacter = null; + if (target.CanBeSelected) { if (!character.IsClimbing && character.CanInteractWith(target.Item, out _, checkLinked: false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index b5515d5de..180d0e963 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -87,13 +87,29 @@ namespace Barotrauma AIObjectiveGetItems CreateObjectives(IEnumerable itemTags, bool requireAll) { AIObjectiveGetItems objectiveReference = null; - if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags) + if (!TryAddSubObjective(ref objectiveReference, () => { - CheckInventory = CheckInventory, - Equip = Equip, - EvaluateCombatPriority = EvaluateCombatPriority, - RequireNonEmpty = RequireNonEmpty, - RequireAllItems = requireAll + var getItems = new AIObjectiveGetItems(character, objectiveManager, itemTags) + { + CheckInventory = CheckInventory, + Equip = Equip, + EvaluateCombatPriority = EvaluateCombatPriority, + RequireNonEmpty = RequireNonEmpty, + RequireAllItems = requireAll + }; + + if (itemTags.Contains(Tags.HeavyDivingGear)) + { + getItems.ItemFilter = (Item it, Identifier tag) => + { + if (tag == Tags.HeavyDivingGear) + { + return AIObjectiveFindDivingGear.IsSuitablePressureProtection(it, tag, character); + } + return true; + }; + } + return getItems; }, onCompleted: () => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 81197c7e0..e9594d864 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -64,10 +64,7 @@ namespace Barotrauma float distanceFactor = 1; if (!isPriority && Item.CurrentHull != character.CurrentHull) { - float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist; - distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist)); + distanceFactor = GetDistanceFactor(Item.WorldPosition, factorAtMaxDistance: 0.25f, verticalDistanceMultiplier: 5, maxDistance: 4000); } float requiredSuccessFactor = objectiveManager.HasOrder() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor; float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100; @@ -113,7 +110,7 @@ namespace Barotrauma if (!repairable.HasRequiredItems(character, false)) { //make sure we have all the items required to fix the target item - foreach (var kvp in repairable.requiredItems) + foreach (var kvp in repairable.RequiredItems) { foreach (RelatedItem requiredItem in kvp.Value) { @@ -140,7 +137,7 @@ namespace Barotrauma } if (repairTool != null) { - if (repairTool.requiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems)) + if (repairTool.RequiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems)) { if (repairTool.Item.OwnInventory == null) { @@ -282,7 +279,7 @@ namespace Barotrauma { foreach (Repairable repairable in Item.Repairables) { - foreach (var kvp in repairable.requiredItems) + foreach (var kvp in repairable.RequiredItems) { foreach (RelatedItem requiredItem in kvp.Value) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 3e10ab620..1403c14ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -74,7 +74,7 @@ namespace Barotrauma } if (!RelevantSkill.IsEmpty) { - if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; } + if (item.Repairables.None(r => r.RequiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; } } return !HumanAIController.IsItemRepairedByAnother(item, out _); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 14963a008..d62bb4e2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -278,52 +278,66 @@ namespace Barotrauma float cprSuitability = Target.Oxygen < 0.0f ? -Target.Oxygen * 100.0f : 0.0f; - //find which treatments are the most suitable to treat the character's current condition - Target.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f); - - //check if we already have a suitable treatment for any of the afflictions + float bestSuitability = 0.0f; + Item bestItem = null; + Affliction afflictionToTreat = null; foreach (Affliction affliction in GetSortedAfflictions(Target)) { - if (affliction == null) { throw new Exception("Affliction was null"); } - if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); } - float bestSuitability = 0.0f; - Item bestItem = null; - foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitabilities) + //find which treatments are the most suitable to treat the character's current condition + Target.CharacterHealth.GetSuitableTreatments( + currentTreatmentSuitabilities, + limb: Target.CharacterHealth.GetAfflictionLimb(affliction), + user: character, + predictFutureDuration: 10.0f); + + foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) { - if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && - currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability) + float thisSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key]; + if (thisSuitability <= 0) { continue; } + + Item matchingItem = FindMedicalItem(character.Inventory, treatmentSuitability.Key); + //allow taking items from the target's inventory too if the target is unconscious + if (matchingItem == null && Target.IsIncapacitated) { - Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true); - //allow taking items from the target's inventory too if the target is unconscious - if (matchingItem == null && Target.IsIncapacitated) - { - matchingItem ??= Target.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true); - } - if (matchingItem != null) - { - bestItem = matchingItem; - bestSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key]; - } + matchingItem = FindMedicalItem(Target.Inventory, treatmentSuitability.Key); } - } - if (bestItem != null) - { - if (Target != character) { character.SelectCharacter(Target); } - ApplyTreatment(affliction, bestItem); - //wait a bit longer after applying a treatment to wait for potential side-effects to manifest - treatmentTimer = TreatmentDelay * 4; - return; + if (matchingItem == null) { continue; } + + //also check how suitable the treatment is for the specific affliction we're now checking + //we don't want to e.g. give fentanyl for oxygen low just because the character has burns on other limbs + //that would also be healed by it! + float suitabilityForThisAffliction = affliction.Prefab.GetTreatmentSuitability(matchingItem); + float totalSuitability = thisSuitability * suitabilityForThisAffliction; + if (matchingItem != null && totalSuitability > bestSuitability) + { + bestItem = matchingItem; + afflictionToTreat = affliction; + bestSuitability = totalSuitability; + } } } + + if (bestItem != null && bestSuitability > cprSuitability) + { + if (Target != character) { character.SelectCharacter(Target); } + ApplyTreatment(afflictionToTreat, bestItem); + //wait a bit longer after applying a treatment to wait for potential side-effects to manifest + treatmentTimer = TreatmentDelay * 4; + return; + } + // Find treatments outside of own inventory only if inside the own sub. if (character.Submarine != null && character.Submarine.TeamID == character.TeamID) { + //get "overall" suitability for no specific limb at this point + Target.CharacterHealth.GetSuitableTreatments( + currentTreatmentSuitabilities, user: character, predictFutureDuration: 10.0f); //didn't have any suitable treatments available, try to find some medical items if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability)) { itemNameList.Clear(); suitableItemIdentifiers.Clear(); - foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) + foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities.OrderByDescending(s => s.Value)) { if (treatmentSuitability.Value <= cprSuitability) { continue; } if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab)) @@ -420,6 +434,28 @@ namespace Barotrauma } } + public static Item FindMedicalItem(Inventory inventory, Identifier itemIdentifier) + { + return FindMedicalItem(inventory, it => it.Prefab.Identifier == itemIdentifier); + } + + public static Item FindMedicalItem(Inventory inventory, Func predicate) + { + if (inventory == null) { return null; } + //prefer items not in a container + Item match = inventory.FindItem(predicate, recursive: false); + if (match != null) { return match; } + + //start from the inventories with most slots + //= prefer taking items from things like toolbelts or doctor's uniforms, as opposed to e.g. autoinjectors which tend to have one or two slots + foreach (var potentialContainer in inventory.AllItems.OrderByDescending(it => it.OwnInventory?.Capacity ?? -1)) + { + match = potentialContainer.OwnInventory?.FindItem(predicate, recursive: true); + if (match != null) { return match; } + } + return null; + } + private void SpeakCannotTreat() { LocalizedString msg = character == Target ? diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 04ee9ccad..9533252c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -122,6 +122,11 @@ namespace Barotrauma public bool HasOptions => Options.Length > 1; public readonly bool MustManuallyAssign; + + /// + /// If enabled and this is an Operate order, it will remove Operate orders of the same item from other characters. + /// If this is a Movement order, removes other Movement orders from the character who receives the order. + /// public readonly bool AutoDismiss; /// @@ -137,7 +142,9 @@ namespace Barotrauma } public OrderTargetType TargetType { get; } public int? WallSectionIndex { get; } - public bool IsIgnoreOrder => Identifier == "ignorethis" || Identifier == "unignorethis"; + public bool IsIgnoreOrder => Identifier == Tags.IgnoreThis || Identifier == Tags.UnignoreThis; + + public bool IsDeconstructOrder => Identifier == Tags.DeconstructThis || Identifier == Tags.DontDeconstructThis; /// /// Should the order icon be drawn when the order target is inside a container @@ -273,7 +280,7 @@ namespace Barotrauma public bool HasPreferredJob(Character character) => HasSpecifiedJob(character, PreferredJobs); - public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true) + public string GetChatMessage(string targetCharacterName, string targetRoomName, Entity targetEntity, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true) { if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed") { @@ -304,8 +311,27 @@ namespace Barotrauma } } } + + LocalizedString targetEntityName = string.Empty; + switch (targetEntity) + { + case Item item: + targetEntityName = item.Name; + break; + case Hull hull: + targetEntityName = hull.DisplayName; + break; + case Structure structure: + targetEntityName = structure.Name; + break; + case Character character: + targetEntityName = character.DisplayName; + break; + } + return TextManager.GetWithVariables(messageTag, ("[name]", targetCharacterName ?? string.Empty, FormatCapitals.No), + ("[target]", targetEntityName, FormatCapitals.No), ("[roomname]", targetRoomName ?? string.Empty, FormatCapitals.Yes)).Fallback("").Value; } @@ -413,6 +439,8 @@ namespace Barotrauma public bool TargetItemsMatchItem(Item item, Identifier option = default) { if (item == null) { return false; } + if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct && !Item.DeconstructItems.Contains(item)) { return true; } + if (Identifier == Tags.DontDeconstructThis && Item.DeconstructItems.Contains(item)) { return true; } ImmutableArray targetItems = GetTargetItems(option); return TargetItemsMatchItem(targetItems, item); } @@ -528,6 +556,7 @@ namespace Barotrauma public OrderCategory? Category => Prefab.Category; public bool MustManuallyAssign => Prefab.MustManuallyAssign; public bool IsIgnoreOrder => Prefab.IsIgnoreOrder; + public bool IsDeconstructOrder => Prefab.IsDeconstructOrder; public bool DrawIconWhenContained => Prefab.DrawIconWhenContained; public bool Hidden => Prefab.Hidden; public bool IgnoreAtOutpost => Prefab.IgnoreAtOutpost; @@ -538,7 +567,6 @@ namespace Barotrauma public bool ColoredWhenControllingGiver => Prefab.ColoredWhenControllingGiver; public bool DisplayGiverInTooltip => Prefab.DisplayGiverInTooltip; - public readonly bool UseController; /// @@ -762,7 +790,7 @@ namespace Barotrauma public string GetChatMessage( string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true) - => Prefab.GetChatMessage(targetCharacterName, targetRoomName, givingOrderToSelf, orderOption, isNewOrder); + => Prefab.GetChatMessage(targetCharacterName, targetRoomName, TargetEntity, givingOrderToSelf, orderOption, isNewOrder); /// /// Get the target item component based on the target item type diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index 874a081bc..bf6db4177 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -52,7 +52,7 @@ namespace Barotrauma { if (orderedCharacter != CommandingCharacter) { - CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5); + CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false), minDurationBetweenSimilar: 5); } CurrentOrder = SuggestedOrder .WithOption(Option) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs index f415c5b96..a500b8178 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs @@ -75,7 +75,8 @@ namespace Barotrauma { steering.Y = 0.0f; } - + + /// Update speed for the steering. Should normally match the characters current animation speed. public virtual void Update(float speed) { if (steering == Vector2.Zero || !MathUtils.IsValid(steering)) @@ -86,6 +87,7 @@ namespace Barotrauma } if (steering.LengthSquared() > speed * speed) { + // Can't steer faster than the max speed. steering = Vector2.Normalize(steering) * Math.Abs(speed); } if (host is AIController aiController && aiController?.Character.CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()) != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index ad724f0bd..958368329 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -32,6 +32,48 @@ namespace Barotrauma public abstract GroundedMovementParams RunParams { get; set; } public abstract SwimParams SwimSlowParams { get; set; } public abstract SwimParams SwimFastParams { get; set; } + + protected class AnimSwap + { + public readonly AnimationType AnimationType; + public readonly AnimationParams TemporaryAnimation; + public readonly float Priority; + public bool IsActive + { + get { return _isActive; } + set + { + if (value) + { + expirationTimer = expirationTime; + } + _isActive = value; + } + } + private bool _isActive; + private float expirationTimer; + private const float expirationTime = 0.1f; + + public AnimSwap(AnimationParams temporaryAnimation, float priority) + { + AnimationType = temporaryAnimation.AnimationType; + TemporaryAnimation = temporaryAnimation; + Priority = priority; + IsActive = true; + } + + public void Update(float deltaTime) + { + expirationTimer -= deltaTime; + if (expirationTimer <= 0) + { + IsActive = false; + } + } + } + + protected readonly Dictionary tempAnimations = new Dictionary(); + protected readonly HashSet expiredAnimations = new HashSet(); public AnimationParams CurrentAnimationParams { @@ -87,7 +129,11 @@ namespace Barotrauma } public bool CanWalk => RagdollParams.CanWalk; - public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir) && CurrentAnimationParams is not FishGroundedParams { Flip: false }; + public bool IsMovingBackwards => + !InWater && + Math.Sign(targetMovement.X) == -Math.Sign(Dir) && + CurrentAnimationParams is not FishGroundedParams { Flip: false } && + Anim != Animation.Climbing; // TODO: define death anim duration in XML protected float deathAnimTimer, deathAnimDuration = 5.0f; @@ -177,8 +223,14 @@ namespace Barotrauma public float WalkPos { get; protected set; } public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { } + + public void UpdateAnimations(float deltaTime) + { + UpdateTemporaryAnimations(deltaTime); + UpdateAnim(deltaTime); + } - public abstract void UpdateAnim(float deltaTime); + protected abstract void UpdateAnim(float deltaTime); public abstract void DragCharacter(Character target, float deltaTime); @@ -253,23 +305,26 @@ namespace Barotrauma switch (type) { case AnimationType.Walk: - return WalkParams; + return CanWalk ? WalkParams : null; case AnimationType.Run: - return RunParams; + return CanWalk ? RunParams : null; case AnimationType.Crouch: if (this is HumanoidAnimController humanAnimController) { return humanAnimController.HumanCrouchParams; } - throw new NotImplementedException(type.ToString()); + else + { + DebugConsole.ThrowError($"Animation params of type {type} not implemented for non-humanoids!"); + return null; + } case AnimationType.SwimSlow: return SwimSlowParams; case AnimationType.SwimFast: return SwimFastParams; case AnimationType.NotDefined: - return null; default: - throw new NotImplementedException(type.ToString()); + return null; } } @@ -376,7 +431,7 @@ namespace Barotrauma private Direction previousDirection; private readonly Vector2[] transformedHandlePos = new Vector2[2]; //TODO: refactor this method, it's way too convoluted - public void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimMelee = false) + public void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 itemPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimMelee = false, Vector2? targetPos = null) { aimingMelee = aimMelee; if (character.Stun > 0.0f || character.IsIncapacitated) @@ -385,22 +440,20 @@ namespace Barotrauma } //calculate the handle positions - Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation); - transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransfrom); - transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransfrom); + Matrix itemTransform = Matrix.CreateRotationZ(item.body.Rotation); + transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransform); + transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransform); Limb torso = GetLimb(LimbType.Torso) ?? MainLimb; Limb leftHand = GetLimb(LimbType.LeftHand); Limb rightHand = GetLimb(LimbType.RightHand); - Vector2 itemPos = aim ? aimPos : holdPos; - var controller = character.SelectedItem?.GetComponent(); - bool usingController = controller != null && !controller.AllowAiming; + bool usingController = controller is { AllowAiming: false }; if (!usingController) { controller = character.SelectedSecondaryItem?.GetComponent(); - usingController = controller != null && !controller.AllowAiming; + usingController = controller is { AllowAiming: false }; } bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f; float itemAngle; @@ -408,15 +461,17 @@ namespace Barotrauma float torsoRotation = torso.Rotation; Item rightHandItem = character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand); - bool equippedInRightHand = rightHandItem == item && rightHand != null && !rightHand.IsSevered; + bool equippedInRightHand = rightHandItem == item && rightHand is { IsSevered: false }; Item leftHandItem = character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand); - bool equippedInLefthand = leftHandItem == item && leftHand != null && !leftHand.IsSevered; + bool equippedInLeftHand = leftHandItem == item && leftHand is { IsSevered: false }; if (aim && !isClimbing && !usingController && character.Stun <= 0.0f && itemPos != Vector2.Zero && !character.IsIncapacitated) { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); - Vector2 diff = holdable.Aimable ? - (mousePos - AimSourceSimPos) * Dir : + targetPos ??= ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); + + Vector2 diff = holdable.Aimable ? + (targetPos.Value - AimSourceSimPos) * Dir : MathUtils.RotatePoint(Vector2.UnitX, torsoRotation); + holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torsoRotation * Dir; holdAngle += GetAimWobble(rightHand, leftHand, item); itemAngle = torsoRotation + holdAngle * Dir; @@ -424,7 +479,7 @@ namespace Barotrauma if (holdable.ControlPose) { //if holding two items that should control the characters' pose, let the item in the right hand do it - bool anotherItemControlsPose = equippedInLefthand && rightHandItem != item && (rightHandItem?.GetComponent()?.ControlPose ?? false); + bool anotherItemControlsPose = equippedInLeftHand && rightHandItem != item && (rightHandItem?.GetComponent()?.ControlPose ?? false); if (!anotherItemControlsPose && TargetMovement == Vector2.Zero && inWater) { torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f; @@ -441,7 +496,7 @@ namespace Barotrauma { itemAngle = rightHand.Rotation + holdAngle * Dir; } - else if (equippedInLefthand) + else if (equippedInLeftHand) { itemAngle = leftHand.Rotation + holdAngle * Dir; } @@ -465,7 +520,7 @@ namespace Barotrauma transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0]; itemAngle = rightHand.Rotation + (holdAngle - rightHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir; } - else if (equippedInLefthand) + else if (equippedInLeftHand) { transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; itemAngle = leftHand.Rotation + (holdAngle - leftHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir; @@ -478,7 +533,7 @@ namespace Barotrauma transformedHoldPos = rightShoulder.WorldAnchorA; rightHand.Disabled = true; } - if (equippedInLefthand) + if (equippedInLeftHand) { if (leftShoulder == null) { return; } transformedHoldPos = leftShoulder.WorldAnchorA; @@ -803,5 +858,260 @@ namespace Barotrauma public void StopUsingItem() => StopAnimation(Animation.UsingItem); public void StopClimbing() => StopAnimation(Animation.Climbing); + + private readonly Dictionary defaultAnimations = new Dictionary(); + + /// + /// Loads an animation (variation) that automatically resets in 0.1s, unless triggered again. + /// Meant e.g. for triggering animations in status effects, without having to worry about resetting them. + /// + public bool TryLoadTemporaryAnimation(StatusEffect.AnimLoadInfo animLoadInfo, bool throwErrors) + { + AnimationType animType = animLoadInfo.Type; + if (tempAnimations.TryGetValue(animType, out AnimSwap animSwap)) + { + if (animLoadInfo.File.TryGet(out string fileName) && animSwap.TemporaryAnimation.FileNameWithoutExtension.Equals(fileName, StringComparison.OrdinalIgnoreCase)) + { + // Already loaded, keep active + animSwap.IsActive = true; + return true; + } + else if (animLoadInfo.File.TryGet(out ContentPath contentPath) && animSwap.TemporaryAnimation.Path == contentPath) + { + // Already loaded, keep active + animSwap.IsActive = true; + return true; + } + else + { + if (animSwap.Priority >= animLoadInfo.Priority) + { + // If the priority of the current animation is higher than the new animation, just return and do nothing. + // Returning false would tell the status effect to not try again, which is not what we want here, which is why we fake a bit with the return value. + return true; + } + else + { + // Override any previous animations of the same type. + tempAnimations.Remove(animType); + } + } + } + AnimationParams defaultAnimation = GetAnimationParamsFromType(animType); + if (defaultAnimation == null) { return false; } + if (!TryLoadAnimation(animType, animLoadInfo.File, out AnimationParams tempParams, throwErrors)) { return false; } + // Store the default animation, if not yet stored. There should always be just one of the same type. + defaultAnimations.TryAdd(animType, defaultAnimation); + tempAnimations.Add(animType, new AnimSwap(tempParams, animLoadInfo.Priority)); + return true; + } + + private void UpdateTemporaryAnimations(float deltaTime) + { + if (tempAnimations.None()) { return; } + foreach ((AnimationType animationType, AnimSwap animSwap) in tempAnimations) + { + if (!animSwap.IsActive) + { + if (defaultAnimations.TryGetValue(animSwap.AnimationType, out AnimationParams defaultAnimation)) + { + TrySwapAnimParams(defaultAnimation); + expiredAnimations.Add(animationType); + } + else + { + DebugConsole.ThrowError($"[AnimController] Failed to find the default animation parameters of type {animSwap.AnimationType}. Cannot swap back the default animations!"); + tempAnimations.Clear(); + } + } + } + foreach (AnimationType anim in expiredAnimations) + { + tempAnimations.Remove(anim); + } + expiredAnimations.Clear(); + foreach (AnimSwap animSwap in tempAnimations.Values) + { + animSwap.Update(deltaTime); + } + } + + /// + /// Loads animations. Non-permanent (= resets on load). + /// + public bool TryLoadAnimation(AnimationType animationType, Either file, out AnimationParams animParams, bool throwErrors) + { + animParams = null; + if (character.IsHumanoid && this is HumanoidAnimController humanAnimController) + { + switch (animationType) + { + case AnimationType.Walk: + humanAnimController.WalkParams = HumanWalkParams.GetAnimParams(character, file, throwErrors); + animParams = humanAnimController.WalkParams; + break; + case AnimationType.Run: + humanAnimController.RunParams = HumanRunParams.GetAnimParams(character, file, throwErrors); + animParams = humanAnimController.RunParams; + break; + case AnimationType.Crouch: + humanAnimController.HumanCrouchParams = HumanCrouchParams.GetAnimParams(character, file, throwErrors); + animParams = humanAnimController.HumanCrouchParams; + break; + case AnimationType.SwimSlow: + humanAnimController.SwimSlowParams = HumanSwimSlowParams.GetAnimParams(character, file, throwErrors); + animParams = humanAnimController.SwimSlowParams; + break; + case AnimationType.SwimFast: + humanAnimController.SwimFastParams = HumanSwimFastParams.GetAnimParams(character, file, throwErrors); + animParams = humanAnimController.SwimFastParams; + break; + default: + DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!"); + break; + } + } + else + { + switch (animationType) + { + case AnimationType.Walk: + if (CanWalk) + { + character.AnimController.WalkParams = FishWalkParams.GetAnimParams(character, file, throwErrors); + animParams = character.AnimController.WalkParams; + } + break; + case AnimationType.Run: + if (CanWalk) + { + character.AnimController.RunParams = FishRunParams.GetAnimParams(character, file, throwErrors); + animParams = character.AnimController.RunParams; + } + break; + case AnimationType.SwimSlow: + character.AnimController.SwimSlowParams = FishSwimSlowParams.GetAnimParams(character, file, throwErrors); + animParams = character.AnimController.SwimSlowParams; + break; + case AnimationType.SwimFast: + character.AnimController.SwimFastParams = FishSwimFastParams.GetAnimParams(character, file, throwErrors); + animParams = character.AnimController.SwimFastParams; + break; + default: + DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!"); + break; + } + } + + bool success = animParams != null; + if (!file.TryGet(out string fileName)) + { + if (file.TryGet(out ContentPath contentPath)) + { + fileName = contentPath.Value; + if (success) + { + success = contentPath == animParams.Path; + } + } + } + else + { + if (success) + { + success = animParams.FileNameWithoutExtension.Equals(fileName, StringComparison.OrdinalIgnoreCase); + } + } + if (success) + { + DebugConsole.NewMessage($"Animation {fileName} successfully loaded for {character.DisplayName}", Color.LightGreen, debugOnly: true); + } + else if (throwErrors) + { + DebugConsole.ThrowError($"Animation {fileName} for {character.DisplayName} could not be loaded!"); + } + return success; + } + + /// + /// Simply swaps existing animation parameters as current parameters. + /// + protected bool TrySwapAnimParams(AnimationParams newParams) + { + AnimationType animationType = newParams.AnimationType; + if (character.IsHumanoid && this is HumanoidAnimController humanAnimController) + { + switch (animationType) + { + case AnimationType.Walk: + if (newParams is HumanWalkParams newWalkParams) + { + humanAnimController.WalkParams = newWalkParams; + } + return true; + case AnimationType.Run: + if (newParams is HumanRunParams newRunParams) + { + humanAnimController.HumanRunParams = newRunParams; + } + break; + case AnimationType.Crouch: + if (newParams is HumanCrouchParams newCrouchParams) + { + humanAnimController.HumanCrouchParams = newCrouchParams; + } + return true; + case AnimationType.SwimSlow: + if (newParams is HumanSwimSlowParams newSwimSlowParams) + { + humanAnimController.HumanSwimSlowParams = newSwimSlowParams; + } + return true; + case AnimationType.SwimFast: + if (newParams is HumanSwimFastParams newSwimFastParams) + { + humanAnimController.HumanSwimFastParams = newSwimFastParams; + } + return true; + default: + DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!"); + return false; + } + } + else + { + switch (animationType) + { + case AnimationType.Walk: + if (newParams is FishWalkParams walkParams) + { + character.AnimController.WalkParams = walkParams; + } + return true; + case AnimationType.Run: + if (newParams is FishRunParams runParams) + { + character.AnimController.RunParams = runParams; + } + return true; + case AnimationType.SwimSlow: + if (newParams is FishSwimSlowParams swimSlowParams) + { + character.AnimController.SwimSlowParams = swimSlowParams; + } + return true; + case AnimationType.SwimFast: + if (newParams is FishSwimFastParams swimFastParams) + { + character.AnimController.SwimFastParams = swimFastParams; + } + return true; + default: + DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!"); + break; + } + } + return false; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 6d726f848..c32e754fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -22,11 +22,7 @@ namespace Barotrauma { if (_ragdollParams == null) { - _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName); - if (!character.VariantOf.IsEmpty) - { - _ragdollParams.ApplyVariantScale(character.Params.VariantFile); - } + _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character); } return _ragdollParams; } @@ -133,7 +129,7 @@ namespace Barotrauma public FishAnimController(Character character, string seed, FishRagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { } - public override void UpdateAnim(float deltaTime) + protected override void UpdateAnim(float deltaTime) { //wait a bit for the ragdoll to "settle" (for joints to force the limbs to appropriate positions) before starting to animate if (Timing.TotalTime - character.SpawnTime < 0.1f) { return; } @@ -145,7 +141,7 @@ namespace Barotrauma } var mainLimb = MainLimb; - levitatingCollider = !IsHanging; + levitatingCollider = !IsHangingWithRope; if (!character.CanMove) { @@ -1011,15 +1007,7 @@ namespace Barotrauma { //make sure the angle "has the same number of revolutions" as the reference limb //(e.g. we don't want to rotate the legs to 0 if the torso is at 360, because that'd blow up the hip joints) - while (referenceLimb.Rotation - angle > MathHelper.TwoPi) - { - angle += MathHelper.TwoPi; - } - while (referenceLimb.Rotation - angle < -MathHelper.TwoPi) - { - angle -= MathHelper.TwoPi; - } - + angle = referenceLimb.body.WrapAngleToSameNumberOfRevolutions(angle); limb?.body.SmoothRotate(angle, torque, wrapAngle: false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 966417d20..ba6f7eb5a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -10,7 +10,7 @@ namespace Barotrauma { class HumanoidAnimController : AnimController { - private const float SteepestWalkableSlopeAngleDegrees = 50f; + private const float SteepestWalkableSlopeAngleDegrees = 55f; private const float SlowlyWalkableSlopeAngleDegrees = 30f; private static readonly float SteepestWalkableSlopeNormalX = @@ -36,7 +36,7 @@ namespace Barotrauma { if (_ragdollParams == null) { - _ragdollParams = RagdollParams.GetDefaultRagdollParams(character.SpeciesName); + _ragdollParams = HumanRagdollParams.GetDefaultRagdollParams(character); } return _ragdollParams; } @@ -248,12 +248,12 @@ namespace Barotrauma GetLimb(footType).PullJointLocalAnchorA); } - public override void UpdateAnim(float deltaTime) + protected override void UpdateAnim(float deltaTime) { if (Frozen) { return; } if (MainLimb == null) { return; } - levitatingCollider = !IsHanging; + levitatingCollider = !IsHangingWithRope; if (onGround && character.CanMove) { if ((character.SelectedItem?.GetComponent()?.ControlCharacterPose ?? false) || @@ -396,7 +396,9 @@ namespace Barotrauma if (SimplePhysicsEnabled) { UpdateStandingSimple(); - IsHanging = false; + StopHangingWithRope(); + StopHoldingToRope(); + StopGettingDraggedWithRope(); return; } @@ -490,7 +492,21 @@ namespace Barotrauma aiming = false; wasAimingMelee = aimingMelee; aimingMelee = false; - IsHanging = IsHanging && character.IsRagdolled; + if (!shouldHangWithRope) + { + StopHangingWithRope(); + } + if (!shouldHoldToRope) + { + StopHoldingToRope(); + } + if (!shouldBeDraggedWithRope) + { + StopGettingDraggedWithRope(); + } + shouldHoldToRope = false; + shouldHangWithRope = false; + shouldBeDraggedWithRope = false; } void UpdateStanding() @@ -686,6 +702,16 @@ namespace Barotrauma if (!onGround) { + const float MaxFootVelocityDiff = 5.0f; + const float MaxFootVelocityDiffSqr = MaxFootVelocityDiff * MaxFootVelocityDiff; + //if the feet have a significantly different velocity from the main limb, try moving them back to a neutral pose below the torso + //this can happen e.g. when jumping over an obstacle: the feet can have a large upwards velocity during the walk/run animation, + //and just "letting go of the animations" would let them keep moving upwards, twisting the character to a weird pose + if ((leftFoot != null && (MainLimb.LinearVelocity - leftFoot.LinearVelocity).LengthSquared() > MaxFootVelocityDiffSqr) || + (rightFoot != null && (MainLimb.LinearVelocity - rightFoot.LinearVelocity).LengthSquared() > MaxFootVelocityDiffSqr)) + { + UpdateFallingProne(10.0f, moveHands: false, moveTorso: false, moveLegs: true); + } return; } @@ -786,7 +812,12 @@ namespace Barotrauma } else { - footPos = new Vector2(colliderPos.X + stepSize.X * i * 0.2f, colliderPos.Y - 0.1f); + float footPosX = stepSize.X * i * 0.2f; + if (CurrentGroundedParams.StepSizeWhenStanding != Vector2.Zero) + { + footPosX = Math.Sign(stepSize.X) * CurrentGroundedParams.StepSizeWhenStanding.X * i; + } + footPos = new Vector2(colliderPos.X + footPosX, colliderPos.Y - 0.1f); } if (Stairs == null && !onSlopeThatMakesSlow) { @@ -1519,6 +1550,8 @@ namespace Barotrauma { torso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + //the pumping animation can sometimes cause impact damage, prevent that by briefly disabling it + target.DisableImpactDamageTimer = 0.15f; cprPumpTimer = 0; if (skill < CPRSettings.Active.DamageSkillThreshold) @@ -1727,7 +1760,7 @@ namespace Barotrauma if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb) { pullLimb.PullJointMaxForce = 5000.0f; - if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) + if (!character.CanRunWhileDragging()) { targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f); } @@ -1811,7 +1844,7 @@ namespace Barotrauma } //limit movement if moving away from the target - if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging) && Vector2.Dot(target.WorldPosition - WorldPosition, targetMovement) < 0) + if (!character.CanRunWhileDragging() && Vector2.Dot(target.WorldPosition - WorldPosition, targetMovement) < 0) { targetMovement *= MathHelper.Clamp(1.5f - dist, 0.0f, 1.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index f9d751411..8d75301d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -70,8 +70,8 @@ namespace Barotrauma public bool Frozen { get { return frozen; } - set - { + set + { if (frozen == value) return; frozen = value; @@ -81,7 +81,7 @@ namespace Barotrauma Collider.FarseerBody.IgnoreGravity = frozen; //Collider.PhysEnabled = !frozen; - if (frozen && MainLimb != null) MainLimb.PullJointWorldAnchorB = MainLimb.SimPosition; + if (frozen && MainLimb != null) { MainLimb.PullJointWorldAnchorB = MainLimb.SimPosition; } } } @@ -109,12 +109,12 @@ namespace Barotrauma //a movement vector that overrides targetmovement if trying to steer //a Character to the position sent by server in multiplayer mode protected Vector2 overrideTargetMovement; - + protected float floorY, standOnFloorY; protected Fixture floorFixture; protected Vector2 floorNormal = Vector2.UnitY; protected float surfaceY; - + protected bool inWater, headInWater; protected bool onGround; public bool OnGround => onGround; @@ -128,7 +128,7 @@ namespace Barotrauma public float ColliderHeightFromFloor => ConvertUnits.ToSimUnits(RagdollParams.ColliderHeightFromFloor) * RagdollParams.JointScale; public Structure Stairs; - + protected Direction dir; public Direction TargetDir; @@ -153,7 +153,7 @@ namespace Barotrauma collider = null; try { - collider = this.collider?[index]; + collider = this.collider?[index]; return true; } catch @@ -322,7 +322,7 @@ namespace Barotrauma impactTolerance = RagdollParams.ImpactTolerance; if (character.Params.VariantFile != null) { - float? tolerance = character.Params.VariantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("impacttolerance", impactTolerance.Value); + float? tolerance = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("ragdoll")?.GetAttributeFloat("impacttolerance", impactTolerance.Value); if (tolerance.HasValue) { impactTolerance = tolerance; @@ -334,7 +334,8 @@ namespace Barotrauma } public bool Draggable => RagdollParams.Draggable; - public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine; + + public CanEnterSubmarine CanEnterSubmarine => RagdollParams.CanEnterSubmarine; public float Dir => dir == Direction.Left ? -1.0f : 1.0f; @@ -384,6 +385,10 @@ namespace Barotrauma if (ragdollParams != null) { RagdollParams = ragdollParams; + if (!character.VariantOf.IsEmpty) + { + RagdollParams.TryApplyVariantScale(character.Params.VariantFile); + } } else { @@ -696,21 +701,34 @@ namespace Barotrauma public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact) { - if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; } - if (f2.UserData is Hull && character.Submarine != null) { return false; } + if (f2.Body.UserData is Submarine submarine && character.Submarine == submarine) { return false; } + if (f2.UserData is Hull) + { + if (character.Submarine != null) + { + return false; + } + if (CanEnterSubmarine == CanEnterSubmarine.Partial) + { + //collider collides with hulls to prevent the character going fully inside the sub, limbs don't + return + f1.Body == Collider.FarseerBody || + (f1.Body.UserData is Limb limb && !limb.Params.CanEnterSubmarine); + } + } //using the velocity of the limb would make the impact damage more realistic, //but would also make it harder to edit the animations because the forces/torques //would all have to be balanced in a way that prevents the character from doing //impact damage to itself Vector2 velocity = Collider.LinearVelocity; - if (character.Submarine == null && f2.Body.UserData is Submarine) + if (character.Submarine == null && f2.Body.UserData is Submarine sub) { - velocity -= ((Submarine)f2.Body.UserData).Velocity; + velocity -= sub.Velocity; } //always collides with bodies other than structures - if (!(f2.Body.UserData is Structure structure)) + if (f2.Body.UserData is not Structure structure) { if (!f2.IsSensor) { @@ -1021,7 +1039,7 @@ namespace Barotrauma { foreach (Ragdoll r in list) { - r.Update(deltaTime, cam); + r.UpdateRagdoll(deltaTime, cam); } } @@ -1041,7 +1059,8 @@ namespace Barotrauma if (newHull == currentHull) { return; } - if (!CanEnterSubmarine || (character.AIController != null && !character.AIController.CanEnterSubmarine)) + if (CanEnterSubmarine == CanEnterSubmarine.False || + (character.AIController != null && character.AIController.CanEnterSubmarine == CanEnterSubmarine.False)) { //character is inside the sub even though it shouldn't be able to enter -> teleport it out @@ -1066,6 +1085,11 @@ namespace Barotrauma } } + if (CanEnterSubmarine != CanEnterSubmarine.True) + { + return; + } + if (setSubmarine) { //in -> out @@ -1075,13 +1099,13 @@ namespace Barotrauma if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f) != null) { return; } if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine())) != null)) { return; } character.MemLocalState?.Clear(); - Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity, detachProjectiles: false); + Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity); } //out -> in else if (currentHull == null && newHull.Submarine != null) { character.MemLocalState?.Clear(); - Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity, detachProjectiles: false); + Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity); } //from one sub to another else if (newHull != null && currentHull != null && newHull.Submarine != currentHull.Submarine) @@ -1089,7 +1113,7 @@ namespace Barotrauma character.MemLocalState?.Clear(); Vector2 newSubPos = newHull.Submarine == null ? Vector2.Zero : newHull.Submarine.Position; Vector2 prevSubPos = currentHull.Submarine == null ? Vector2.Zero : currentHull.Submarine.Position; - Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero, detachProjectiles: false); + Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero); } } @@ -1156,7 +1180,7 @@ namespace Barotrauma character.DisableImpactDamageTimer = 0.25f; - SetPosition(Collider.SimPosition + moveAmount, detachProjectiles: detachProjectiles); + SetPosition(Collider.SimPosition + moveAmount); character.CursorPosition += moveAmount; Collider?.UpdateDrawPosition(); @@ -1221,7 +1245,7 @@ namespace Barotrauma public bool forceStanding; public bool forceNotStanding; - public void Update(float deltaTime, Camera cam) + public void UpdateRagdoll(float deltaTime, Camera cam) { if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; } @@ -1389,7 +1413,8 @@ namespace Barotrauma if (floorNormal.Y is > 0f and < 1f && Math.Sign(movement.X) == Math.Sign(floorNormal.X)) { - slopePull = Math.Abs(movement.X * floorNormal.X / floorNormal.Y) / LevitationSpeedMultiplier; + float steepness = Math.Abs(floorNormal.X); + slopePull = Math.Abs(movement.X * steepness) / LevitationSpeedMultiplier; } if (Math.Abs(Collider.SimPosition.Y - targetY - slopePull) > 0.01f) @@ -1743,7 +1768,7 @@ namespace Barotrauma return closestFraction; }, rayStart, rayEnd, Physics.CollisionStairs | Physics.CollisionPlatform | Physics.CollisionWall | Physics.CollisionLevel); - if (standOnFloorFixture != null && !IsHanging) + if (standOnFloorFixture != null && !IsHangingWithRope) { floorFixture = standOnFloorFixture; standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction; @@ -1845,7 +1870,7 @@ namespace Barotrauma return (surfaceY, ceilingY); } - public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true) + public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool moveLatchers = true) { if (!MathUtils.IsValid(simPosition)) { @@ -1858,10 +1883,14 @@ namespace Barotrauma } if (MainLimb == null) { return; } + Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition; + // A Work-around for an issue with teleporting the characters: // Detach every latcher when either one of the latchers or the target is teleported, - // because otherwise all the characters are teleported to invalid positions. - if (Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached) + // because otherwise all the characters are teleported to invalid positions. + const float ForceDeattachThreshold = 10.0f; + if (limbMoveAmount.LengthSquared() > ForceDeattachThreshold * ForceDeattachThreshold && + Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached) { var target = enemyAI.LatchOntoAI.TargetCharacter; if (target != null) @@ -1874,7 +1903,6 @@ namespace Barotrauma Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true)); Character.Latchers.Clear(); - Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition; if (lerp) { Collider.TargetPosition = simPosition; @@ -1896,15 +1924,62 @@ namespace Barotrauma } } } + + /// + /// Is attached to something with a rope. + /// + public bool IsHoldingToRope { get; private set; } + protected bool shouldHoldToRope; - public bool IsHanging { get; protected set; } + /// + /// Is hanging to something with a rope, so that can reel towards it. Currently only possible in water. + /// + public bool IsHangingWithRope { get; private set; } + protected bool shouldHangWithRope; + + /// + /// Has someone attached to the character with a rope? + /// + public bool IsDraggedWithRope { get; private set; } + protected bool shouldBeDraggedWithRope; - public void Hang() + public void HangWithRope() { + shouldHangWithRope = true; + IsHangingWithRope = true; ResetPullJoints(); onGround = false; levitatingCollider = false; - IsHanging = true; + } + + public void HoldToRope() + { + shouldHoldToRope = true; + IsHoldingToRope = true; + } + + public void DragWithRope() + { + shouldBeDraggedWithRope = true; + IsDraggedWithRope = true; + } + + protected void StopHangingWithRope() + { + shouldHangWithRope = false; + IsHangingWithRope = false; + } + + protected void StopHoldingToRope() + { + shouldHoldToRope = false; + IsHoldingToRope = false; + } + + protected void StopGettingDraggedWithRope() + { + shouldBeDraggedWithRope = false; + IsDraggedWithRope = false; } protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 8bb5d51ff..92f4d9897 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -37,7 +37,9 @@ namespace Barotrauma FallBackUntilCanAttack, PursueIfCanAttack, Pursue, + Eat, FollowThrough, + FollowThroughWithoutObstacleAvoidance, FollowThroughUntilCanAttack, IdleUntilCanAttack, Reverse, @@ -104,6 +106,13 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes, description: "A delay before reacting after performing an attack."), Editable] public float AfterAttackDelay { get; set; } + [Serialize(AIBehaviorAfterAttack.FallBack, IsPropertySaveable.Yes, + description: "Secondary AI behavior after the attack. The character first executes the AfterAttack behavior, then after AfterAttackSecondaryDelay passes, switches to this one. Ignored if AfterAttackSecondaryDelay is 0 or less."), Editable] + public AIBehaviorAfterAttack AfterAttackSecondary { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How long the character executes the AfterAttack before switching to AfterAttackSecondary. The secondary behavior is ignored if this value is 0 or less."), Editable] + public float AfterAttackSecondaryDelay { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to turn around when aiming with this attack?"), Editable] public bool Reverse { get; private set; } @@ -135,10 +144,11 @@ namespace Barotrauma [Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] public float Duration { get; private set; } - [Serialize(5f, IsPropertySaveable.Yes, description: "How long the AI waits between the attacks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] + [Serialize(5f, IsPropertySaveable.Yes, description: "How long the AI must wait before it can use this attack again."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] public float CoolDown { get; set; } = 5; - [Serialize(0f, IsPropertySaveable.Yes, description: "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] + + [Serialize(0f, IsPropertySaveable.Yes, description: "When the attack cooldown is running and when there are other valid attacks possible for the character to use, the secondary cooldown is used instead of the regular cooldown. Does not have an effect, if set to 0 or less than the regular cooldown value."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] public float SecondaryCoolDown { get; set; } = 0; [Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] @@ -155,6 +165,9 @@ namespace Barotrauma set => _structureDamage = value; } + [Serialize(false, IsPropertySaveable.Yes, description: "If the attack causes an explosion of wall damage shrapnel, should some of the shrapnel be launched as projectiles that can go through walls?"), Editable] + public bool CreateWallDamageProjectiles { get; private set; } + [Serialize(true, IsPropertySaveable.Yes, description: "Whether or not damaging structures with the attack causes damage particles to emit."), Editable] public bool EmitStructureDamageParticles { get; private set; } @@ -443,6 +456,12 @@ namespace Barotrauma break; } } + + if (SecondaryCoolDown > CoolDown) + { + DebugConsole.AddWarning($"Potentially misconfigured attack in {parentDebugName}. Secondary cooldown should not be longer than the primary cooldown.", + contentPackage: element.ContentPackage); + } } partial void InitProjSpecific(ContentXElement element); @@ -461,11 +480,6 @@ namespace Barotrauma } affliction = afflictionPrefab.Instantiate(0.0f); affliction.Deserialize(subElement); - //backwards compatibility - if (subElement.GetAttribute("amount") != null && subElement.GetAttribute("strength") == null) - { - affliction.Strength = subElement.GetAttributeFloat("amount", 0.0f); - } // add the affliction anyway, so that it can be shown in the editor. Afflictions.Add(affliction, subElement); } @@ -708,6 +722,8 @@ namespace Barotrauma public float SecondaryCoolDownTimer { get; set; } public bool IsRunning { get; private set; } + public float AfterAttackTimer { get; set; } + public void UpdateCoolDown(float deltaTime) { CoolDownTimer -= deltaTime; @@ -729,6 +745,7 @@ namespace Barotrauma public void ResetAttackTimer() { + AfterAttackTimer = 0; AttackTimer = 0; IsRunning = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 88aa76a5f..374d89279 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -147,8 +147,8 @@ namespace Barotrauma /// /// Is the character player or does it have an active ship command manager (an AI controlled sub)? Bots in the player team are not treated as commanders. /// - public bool IsCommanding => IsPlayer || (AIController is HumanAIController humanAI && humanAI.ShipCommandManager != null && humanAI.ShipCommandManager.Active); - public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled; + public bool IsCommanding => IsPlayer || AIController is HumanAIController { ShipCommandManager.Active: true }; + public bool IsBot => !IsPlayer && AIController is HumanAIController { Enabled: true }; public bool IsEscorted { get; set; } public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty; @@ -349,7 +349,13 @@ namespace Barotrauma public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && teamID == CharacterTeamType.Team2; - public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator; + public bool IsInstigator => CombatAction is { IsInstigator: true }; + + /// + /// Set true only, if the character is turned hostile from an escort mission (See ). + /// + public bool IsHostileEscortee; + public CombatAction CombatAction; public readonly AnimController AnimController; @@ -393,8 +399,10 @@ namespace Barotrauma public readonly CharacterPrefab Prefab; public readonly CharacterParams Params; - + public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier(); + + public Identifier GetBaseCharacterSpeciesName() => Prefab.GetBaseCharacterSpeciesName(SpeciesName); public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group; @@ -444,6 +452,17 @@ namespace Barotrauma set => Params.Visibility = value; } + public float MaxPerceptionDistance + { + get => Params.AI?.MaxPerceptionDistance ?? 0; + set + { + if (Params.AI != null) + { + Params.AI.MaxPerceptionDistance = value; + } + } + } public bool IsTraitor { get; @@ -583,9 +602,6 @@ namespace Barotrauma public CharacterInventory Inventory { get; private set; } - private Color speechBubbleColor; - private float speechBubbleTimer; - /// /// Prevents the character from interacting with items or characters /// @@ -685,7 +701,8 @@ namespace Barotrauma if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance and > 0 } grabbedWallet }) { #if SERVER - if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings }) + var mpCampaign = GameMain.GameSession.Campaign as MultiPlayerCampaign; + if (mpCampaign != null && GameMain.Server is { ServerSettings: { } settings }) { switch (settings.LootedMoneyDestination) { @@ -698,16 +715,28 @@ namespace Barotrauma } } - + GameServer.Log($"{GameServer.CharacterLogName(this)} grabbed {value.Name}'s body and received {grabbedWallet.Balance} mk.", ServerLog.MessageType.Money); + + grabbedWallet.Deduct(balance); + //we need to save the grabbed character's wallet this at this point to ensure + //the client doesn't get to keep the money if they respawn + if (mpCampaign != null && selectedCharacter.Info != null) + { + var characterCampaignData = mpCampaign?.GetCharacterData(selectedCharacter.Info); + if (characterCampaignData!= null) + { + characterCampaignData.WalletData = grabbedWallet.Save(); + characterCampaignData?.ApplyWalletData(selectedCharacter); + } + } #elif CLIENT if (GameMain.GameSession.Campaign is SinglePlayerCampaign spCampaign) { spCampaign.Bank.Give(balance); } -#endif - grabbedWallet.Deduct(balance); +#endif } } } @@ -738,6 +767,25 @@ namespace Barotrauma if (item2 != null && item2 != item1) { yield return item2; } } } + + public bool IsDualWieldingRangedWeapons() + { + int rangedItemCount = 0; + foreach (var item in HeldItems) + { + if (item.GetComponent() != null) + { + rangedItemCount++; + } + + if (rangedItemCount > 1) + { + return true; + } + } + + return false; + } private float lowPassMultiplier; public float LowPassMultiplier @@ -844,7 +892,7 @@ namespace Barotrauma public float Stun { - get { return IsRagdolled && !AnimController.IsHanging ? 1.0f : CharacterHealth.Stun; } + get { return IsRagdolled && !AnimController.IsHangingWithRope ? 1.0f : CharacterHealth.Stun; } set { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } @@ -1093,7 +1141,7 @@ namespace Barotrauma { get { - return (SelectedItem == null || SelectedItem.GetComponent() is { AllowAiming: true }) && !IsIncapacitated && !IsRagdolled; + return (SelectedItem == null || SelectedItem.GetComponent() is { AllowAiming: true }) && !IsIncapacitated && (!IsRagdolled || AnimController.IsHoldingToRope); } } @@ -1350,7 +1398,7 @@ namespace Barotrauma } if (Params.VariantFile != null && Params.MainElement is ContentXElement paramsMainElement) { - var overrideElement = Params.VariantFile.Root.FromPackage(paramsMainElement.ContentPackage); + var overrideElement = Params.VariantFile.GetRootExcludingOverride().FromPackage(paramsMainElement.ContentPackage); // Only override if the override file contains matching elements if (overrideElement.GetChildElement("inventory") != null) { @@ -1432,7 +1480,7 @@ namespace Barotrauma if (ragdollParams == null && prefab.VariantOf == null) { Identifier name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName; - ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams(name) : RagdollParams.GetDefaultRagdollParams(name) as RagdollParams; + ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams(name, Params, Prefab.ContentPackage) : RagdollParams.GetDefaultRagdollParams(name, Params, Prefab.ContentPackage); } if (Params.HasInfo && info == null) { @@ -1818,9 +1866,18 @@ namespace Barotrauma // - dragging someone // - crouching // - moving backwards - public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged || HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) && - (!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) && - !AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk); + public bool CanRun => CanRunWhileDragging() && + AnimController is not HumanoidAnimController { Crouching: true } && + !AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk) && + !AnimController.IsHoldingToRope; + + public bool CanRunWhileDragging() + { + if (selectedCharacter == null || !selectedCharacter.CanBeDragged) { return true; } + //if the dragged character is conscious, don't allow running (the dragged character won't keep up, and the dragging gets interrupted) + if (!selectedCharacter.IsIncapacitated && selectedCharacter.Stun <= 0.0f) { return false; } + return HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging); + } public Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed) { @@ -2212,6 +2269,25 @@ namespace Barotrauma if (Inventory != null) { + if (IsKeyHit(InputType.DropItem)) + { + foreach (Item item in HeldItems) + { + if (!CanInteractWith(item)) { continue; } + + if (SelectedItem?.OwnInventory != null && SelectedItem.OwnInventory.CanBePut(item)) + { + SelectedItem.OwnInventory.TryPutItem(item, this); + } + else + { + item.Drop(this); + } + //only drop one held item per key hit + break; + } + } + bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected; if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem)) { @@ -2525,7 +2601,7 @@ namespace Barotrauma if (container != null) { if (!container.HasRequiredItems(this, addMessage: false)) { return false; } - if (!container.DrawInventory) { return false; } + if (!container.AllowAccess) { return false; } } } return true; @@ -2587,10 +2663,7 @@ namespace Barotrauma if (itemPriority <= 0) { continue; } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; Vector2 refPos = positionalReference != null ? positionalReference.WorldPosition : WorldPosition; - float yDist = Math.Abs(refPos.Y - itemPos.Y); - yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(refPos.X - itemPos.X) + yDist; - float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, maxItemDistance, dist)); + float distanceFactor = AIObjective.GetDistanceFactor(refPos, itemPos, verticalDistanceMultiplier: 5, maxDistance: maxItemDistance, factorAtMaxDistance: 0); itemPriority *= distanceFactor; if (itemPriority > _selectedItemPriority) { @@ -2881,11 +2954,22 @@ namespace Barotrauma //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes aimAssist = 0.0f; } - - focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null; - if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None) + + UpdateInteractablesInRange(); + + if (!ShowInteractionLabels) // show labels handles setting the focused item in CharacterHUD, so we can click on them boxes { - FocusedCharacter = null; + focusedItem = CanInteract ? FindClosestItem(interactablesInRange, mouseSimPos, aimAssist) : null; + } + + if (focusedItem != null) + { + if (focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None || + /*pets' "play" interaction can interfere with interacting with items, so let's remove focus from the pet if the cursor is closer to a highlighted item*/ + FocusedCharacter is { IsPet: true } && Vector2.DistanceSquared(focusedItem.SimPosition, mouseSimPos) < Vector2.DistanceSquared(FocusedCharacter.SimPosition, mouseSimPos)) + { + FocusedCharacter = null; + } } findFocusedTimer = 0.05f; } @@ -3068,7 +3152,7 @@ namespace Barotrauma { if (!c.Enabled || c.AnimController.Frozen) continue; - c.AnimController.UpdateAnim(deltaTime); + c.AnimController.UpdateAnimations(deltaTime); } } @@ -3078,7 +3162,7 @@ namespace Barotrauma { foreach (Character c in CharacterList) { - if (!(c is AICharacter) && !c.IsRemotePlayer) continue; + if (c is not AICharacter && !c.IsRemotePlayer) { continue; } if (c.IsPlayer || (c.IsBot && !c.IsDead)) { @@ -3136,8 +3220,14 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(character != null && !character.Removed); character.Update(deltaTime, cam); } + +#if CLIENT + UpdateSpeechBubbles(deltaTime); +#endif } + static partial void UpdateSpeechBubbles(float deltaTime); + public virtual void Update(float deltaTime, Camera cam) { UpdateProjSpecific(deltaTime, cam); @@ -3175,8 +3265,6 @@ namespace Barotrauma PreviousHull = CurrentHull; CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, useWorldCoordinates: true); - speechBubbleTimer = Math.Max(0.0f, speechBubbleTimer - deltaTime); - obstructVisionAmount = Math.Max(obstructVisionAmount - deltaTime, 0.0f); if (Inventory != null) @@ -3335,11 +3423,14 @@ namespace Barotrauma else if (!tooFastToUnragdoll) { IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves - if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; } + if (wasRagdolled != IsRagdolled && !AnimController.IsHangingWithRope) + { + ragdollingLockTimer = 0.2f; + } } SetInput(InputType.Ragdoll, false, IsRagdolled); } - if (!wasRagdolled && IsRagdolled) + if (!wasRagdolled && IsRagdolled && !AnimController.IsHangingWithRope) { CheckTalents(AbilityEffectType.OnRagdoll); } @@ -3599,7 +3690,7 @@ namespace Barotrauma Identifier despawnContainerId = IsHuman ? - "despawncontainer".ToIdentifier() : + Tags.DespawnContainer : Params.DespawnContainer; if (!despawnContainerId.IsEmpty) { @@ -3627,6 +3718,14 @@ namespace Barotrauma var itemContainer = item?.GetComponent(); if (itemContainer == null) { return; } List inventoryItems = new List(Inventory.AllItemsMod); + + //unequipping genetic materials normally destroys it in GeneticMaterial.Update, let's do that manually here + var geneticMaterials = Inventory.FindAllItems(it => it.GetComponent() != null, recursive: true); + foreach (var geneticMaterial in geneticMaterials) + { + geneticMaterial.ApplyStatusEffects(ActionType.OnSevered, 1.0f, this); + } + foreach (Item inventoryItem in inventoryItems) { if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null, createNetworkEvent: createNetworkEvents)) @@ -3677,13 +3776,12 @@ namespace Barotrauma float minRange = Math.Clamp((float)Math.Sqrt(Mass) * Visibility, 250, 1000); float massFactor = (float)Math.Sqrt(Mass / 20); float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange); + targetRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier); 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) @@ -3697,8 +3795,8 @@ namespace Barotrauma { float massFactor = (float)Math.Sqrt(Mass / 10); float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange); + targetRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier); float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed); - newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier); if (!float.IsNaN(newRange)) { aiTarget.SoundRange = newRange; @@ -3975,7 +4073,9 @@ namespace Barotrauma GameMain.Server.SendChatMessage(message.Message, message.MessageType.Value, null, this); } #endif - ShowSpeechBubble(2.0f, ChatMessage.MessageColor[(int)message.MessageType.Value]); +#if CLIENT + ShowSpeechBubble(ChatMessage.MessageColor[(int)message.MessageType.Value], message.Message); +#endif sentMessages.Add(message); } @@ -4006,13 +4106,6 @@ namespace Barotrauma } } - - public void ShowSpeechBubble(float duration, Color color) - { - speechBubbleTimer = Math.Max(speechBubbleTimer, duration); - speechBubbleColor = color; - } - public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount) { CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount); @@ -4321,8 +4414,8 @@ namespace Barotrauma { if (affliction.Prefab.IsBuff) { continue; } if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; } - if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || - affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + if (Params.Health.ImmunityIdentifiers.Contains(affliction.Identifier)) { continue; } + if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) { if (!Params.Health.PoisonImmunity) { @@ -4389,7 +4482,7 @@ namespace Barotrauma /// Is the character knocked down regardless whether the technical state is dead, unconcious, paralyzed, or stunned. /// With stunning, the parameter uses an one second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property. /// - public bool IsKnockedDown => IsRagdolled || CharacterHealth.StunTimer > 1.0f || IsIncapacitated; + public bool IsKnockedDown => (IsRagdolled && !AnimController.IsHangingWithRope) || CharacterHealth.StunTimer > 1.0f || IsIncapacitated; public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false) { @@ -4602,6 +4695,15 @@ namespace Barotrauma isDead = true; + // Save these resistances in the CharacterInfo object so that if they + // are needed for respawning, they will be available (because there + // will be no Character instance in the limbo/bardo state) + if (info != null) + { + info.LastResistanceMultiplierSkillLossDeath = GetAbilityResistance(Tags.SkillLossDeathResistance); + info.LastResistanceMultiplierSkillLossRespawn = GetAbilityResistance(Tags.SkillLossRespawnResistance); + } + ApplyStatusEffects(ActionType.OnDeath, 1.0f); AnimController.Frozen = false; @@ -4610,7 +4712,7 @@ namespace Barotrauma causeOfDeath, causeOfDeathAffliction?.Prefab, causeOfDeathAffliction?.Source, LastDamageSource); - if (GameAnalyticsManager.SendUserStatistics) + if (GameAnalyticsManager.SendUserStatistics && Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage) { string causeOfDeathStr = causeOfDeathAffliction == null ? causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Value.Replace(" ", ""); @@ -5096,7 +5198,7 @@ namespace Barotrauma public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure); - #region Talents +#region Talents private readonly List characterTalents = new List(); public IReadOnlyCollection CharacterTalents => characterTalents; @@ -5214,7 +5316,7 @@ namespace Barotrauma partial void OnTalentGiven(TalentPrefab talentPrefab); - #endregion +#endregion private readonly HashSet sameRoomHulls = new(); @@ -5426,6 +5528,24 @@ namespace Barotrauma private readonly Dictionary abilityResistances = new(); + public float GetAbilityResistance(Identifier resistanceId) + { + float resistance = 0f; + bool hadResistance = false; + + foreach (var (key, value) in abilityResistances) + { + if (key.ResistanceIdentifier == resistanceId) + { + resistance += value; + hadResistance = true; + } + } + + // NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance + return hadResistance ? resistance : 1f; + } + public float GetAbilityResistance(AfflictionPrefab affliction) { float resistance = 0f; @@ -5441,6 +5561,7 @@ namespace Barotrauma } } + // NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance return hadResistance ? resistance : 1f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs index 954809cb1..5392608ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Voronoi2; namespace Barotrauma { @@ -27,9 +28,10 @@ namespace Barotrauma UpdateMoney = 13, UpdatePermanentStats = 14, RemoveFromCrew = 15, + LatchOntoTarget = 16, MinValue = 0, - MaxValue = 15 + MaxValue = 16 } private interface IEventData : NetEntityEvent.IData @@ -135,7 +137,56 @@ namespace Barotrauma ObjectiveType = objectiveType; } } - + + public readonly struct LatchedOntoTargetEventData : IEventData + { + public EventType EventType => EventType.LatchOntoTarget; + public readonly bool IsLatched; + public readonly UInt16 TargetCharacterID = NullEntityID; + public readonly UInt16 TargetStructureID = NullEntityID; + public readonly int TargetLevelWallIndex = -1; + + public readonly Vector2 AttachSurfaceNormal = Vector2.Zero; + public readonly Vector2 AttachPos = Vector2.Zero; + + public readonly Vector2 CharacterSimPos; + + private LatchedOntoTargetEventData(Character character, Vector2 attachSurfaceNormal, Vector2 attachPos) + { + CharacterSimPos = character.SimPosition; + IsLatched = true; + AttachSurfaceNormal = attachSurfaceNormal; + AttachPos = attachPos; + } + + public LatchedOntoTargetEventData(Character character, Character targetCharacter, Vector2 attachSurfaceNormal, Vector2 attachPos) + : this(character, attachSurfaceNormal, attachPos) + { + TargetCharacterID = targetCharacter.ID; + } + + public LatchedOntoTargetEventData(Character character, Structure targetStructure, Vector2 attachSurfaceNormal, Vector2 attachPos) + : this(character, attachSurfaceNormal, attachPos) + { + TargetStructureID = targetStructure.ID; + } + + public LatchedOntoTargetEventData(Character character, VoronoiCell levelWall, Vector2 attachSurfaceNormal, Vector2 attachPos) + : this(character, attachSurfaceNormal, attachPos) + { + TargetLevelWallIndex = Level.Loaded.GetAllCells().IndexOf(levelWall); + } + + /// + /// Signifies detaching (not attached to any target) + /// + public LatchedOntoTargetEventData() + { + CharacterSimPos = Vector2.Zero; + IsLatched = false; + } + } + private struct TeamChangeEventData : IEventData { public EventType EventType => EventType.TeamChange; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index d733b8f92..1b673b092 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -490,8 +490,6 @@ namespace Barotrauma public ContentXElement CharacterConfigElement { get; set; } - public readonly string ragdollFileName = string.Empty; - public bool StartItemsGiven; public bool IsNewHire; @@ -553,12 +551,11 @@ namespace Barotrauma { if (ragdoll == null) { - // TODO: support for variants Identifier speciesName = SpeciesName; bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName == CharacterPrefab.HumanSpeciesName); ragdoll = isHumanoid - ? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName) - : RagdollParams.GetRagdollParams(speciesName, ragdollFileName) as RagdollParams; + ? RagdollParams.GetDefaultRagdollParams(SpeciesName, CharacterConfigElement, CharacterConfigElement.ContentPackage) + : RagdollParams.GetDefaultRagdollParams(SpeciesName, CharacterConfigElement, CharacterConfigElement.ContentPackage); } return ragdoll; } @@ -652,7 +649,6 @@ namespace Barotrauma string name = "", string originalName = "", Either jobOrJobPrefab = null, - string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, Identifier npcIdentifier = default) @@ -703,14 +699,10 @@ namespace Barotrauma Salary = CalculateSalary(); } OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name; - if (ragdollFileName != null) - { - this.ragdollFileName = ragdollFileName; - } } private void SetPersonalityTrait() - => PersonalityTrait = NPCPersonalityTrait.GetRandom(Name + string.Concat(Head.Preset.TagSet)); + => PersonalityTrait = NPCPersonalityTrait.GetRandom(Name + string.Concat(Head.Preset.TagSet.OrderBy(tag => tag))); public string GetRandomName(Rand.RandSync randSync) { @@ -832,7 +824,6 @@ namespace Barotrauma StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false); Identifier personalityName = infoElement.GetAttributeIdentifier("personality", ""); - ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty); if (personalityName != Identifier.Empty) { if (NPCPersonalityTrait.Traits.TryGet(personalityName, out var trait) || @@ -1086,7 +1077,26 @@ namespace Barotrauma partial void LoadHeadSpriteProjectSpecific(ContentXElement limbElement); + private bool spriteTagsLoaded; + public void VerifySpriteTagsLoaded() + { + if (!spriteTagsLoaded) + { + LoadSpriteTags(); + } + } + private void LoadHeadSprite() + { + LoadHeadElement(loadHeadSprite: true, loadHeadSpriteTags: true); + } + + private void LoadSpriteTags() + { + LoadHeadElement(loadHeadSprite: false, loadHeadSpriteTags: true); + } + + private void LoadHeadElement(bool loadHeadSprite, bool loadHeadSpriteTags) { if (Ragdoll?.MainElement == null) { return; } foreach (var limbElement in Ragdoll.MainElement.Elements()) @@ -1116,20 +1126,30 @@ namespace Barotrauma fileWithoutTags = fileWithoutTags.Split('[', ']').First(); if (fileWithoutTags != fileName) { continue; } - HeadSprite = new Sprite(spriteElement, "", file); - Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; - - //extract the tags out of the filename - SpriteTags = file.Split('[', ']').Skip(1).Select(id => id.ToIdentifier()).ToList(); - if (SpriteTags.Any()) + if (loadHeadSprite) { - SpriteTags.RemoveAt(SpriteTags.Count - 1); + HeadSprite = new Sprite(spriteElement, "", file); + Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; + } + + if (loadHeadSpriteTags) + { + //extract the tags out of the filename + SpriteTags = file.Split('[', ']').Skip(1).Select(id => id.ToIdentifier()).ToList(); + if (SpriteTags.Any()) + { + SpriteTags.RemoveAt(SpriteTags.Count - 1); + } + spriteTagsLoaded = true; } break; } - LoadHeadSpriteProjectSpecific(limbElement); + if (loadHeadSprite) + { + LoadHeadSpriteProjectSpecific(limbElement); + } break; } @@ -1446,9 +1466,7 @@ namespace Barotrauma new XAttribute("haircolor", XMLExtensions.ColorToString(Head.HairColor)), new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)), new XAttribute("startitemsgiven", StartItemsGiven), - new XAttribute("ragdoll", ragdollFileName), new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty)); - // TODO: animations? if (HumanPrefabIds != default) { @@ -1663,8 +1681,7 @@ namespace Barotrauma { Order order = null; string orderIdentifier = orderElement.GetAttributeString("id", ""); - var orderPrefab = OrderPrefab.Prefabs[orderIdentifier]; - if (orderPrefab == null) + if (!OrderPrefab.Prefabs.TryGet(orderIdentifier, out OrderPrefab orderPrefab)) { DebugConsole.ThrowError($"Error loading a previously saved order - can't find an order prefab with the identifier \"{orderIdentifier}\""); priorityIncrease++; @@ -1968,6 +1985,21 @@ namespace Barotrauma } if (changed) { OnPermanentStatChanged(statType); } } + + /// + /// Used to store the last known resistance against skill loss on death + /// when the character dies, so it can be correctly applied before + /// reinstantiating the Character object (if respawning). + /// NOTE: The resistances are handled as multipliers here, so 1.0 == 0% resistance + /// + public float LastResistanceMultiplierSkillLossDeath = 1.0f; + /// + /// Used to store the last known resistance against skill loss on respawn + /// when the character dies, so it can be correctly applied before + /// reinstantiating the Character object (if respawning). + /// NOTE: The resistances are handled as multipliers here, so 1.0 == 0% resistance + /// + public float LastResistanceMultiplierSkillLossRespawn = 1.0f; } internal sealed class SavedStatValue diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index acae26e4a..c1cea91c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -18,6 +18,19 @@ namespace Barotrauma public string Name => Identifier.Value; public Identifier VariantOf { get; } public CharacterPrefab ParentPrefab { get; set; } + + public Identifier GetBaseCharacterSpeciesName(Identifier speciesName) + { + if (!VariantOf.IsEmpty) + { + speciesName = VariantOf; + if (ParentPrefab is { VariantOf.IsEmpty: false } parentPrefab) + { + speciesName = parentPrefab.GetBaseCharacterSpeciesName(speciesName); + } + } + return speciesName; + } public void InheritFrom(CharacterPrefab parent) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index ffbf96858..baa030c31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -68,7 +68,7 @@ namespace Barotrauma public bool DivideByLimbCount { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Is the damage relative to the max vitality (percentage) or absolute (normal)"), Editable] - public bool MultiplyByMaxVitality { get; private set; } + public bool MultiplyByMaxVitality { get; set; } public float DamagePerSecond; public float DamagePerSecondTimer; @@ -130,11 +130,16 @@ namespace Barotrauma public void Deserialize(XElement element) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + //backwards compatibility + if (element.GetAttribute("amount") != null && element.GetAttribute("strength") == null) + { + Strength = element.GetAttributeFloat("amount", 0.0f); + } } public Affliction CreateMultiplied(float multiplier, Affliction affliction) { - var instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source); + Affliction instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source); instance.CopyProperties(affliction); return instance; } @@ -183,7 +188,7 @@ namespace Barotrauma if (currentEffect.MultiplyByMaxVitality) { - currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality; + currVitalityDecrease *= characterHealth?.MaxVitality ?? 100.0f; } return currVitalityDecrease; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 4136b6381..a4338ac70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -141,6 +141,9 @@ namespace Barotrauma if (prevDisplayedMessage.HasValue && prevDisplayedMessage.Value == State) { return; } if (highestStrength > Strength) { return; } + // Show initial husk warning by default, and disable it only if campaign difficulty settings explicitly disable it + bool showHuskWarning = GameMain.GameSession?.Campaign?.Settings.ShowHuskWarning ?? true; + switch (State) { case InfectionState.Dormant: @@ -148,15 +151,18 @@ namespace Barotrauma { return; } - if (character == Character.Controlled) + if (showHuskWarning) { + if (character == Character.Controlled) + { #if CLIENT - GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red); + GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red); #endif - } - else if (character.IsBot) - { - character.Speak(TextManager.Get("dialoghuskdormant").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskdormant".ToIdentifier()); + } + else if (character.IsBot) + { + character.Speak(TextManager.Get("dialoghuskdormant").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskdormant".ToIdentifier()); + } } break; case InfectionState.Transition: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index c5fa49f23..03a1f1e26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -173,6 +173,12 @@ namespace Barotrauma max += Character.Info.Job.Prefab.VitalityModifier; } max *= Character.HumanPrefabHealthMultiplier; + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + max *= Character.IsOnPlayerTeam + ? campaign.Settings.CrewVitalityMultiplier + : campaign.Settings.NonCrewVitalityMultiplier; + } max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier); return max * Character.HealthMultiplier; } @@ -270,9 +276,9 @@ namespace Barotrauma this.Character = character; InitIrremovableAfflictions(); - vitality = UnmodifiedMaxVitality; + vitality = UnmodifiedMaxVitality; - minVitality = character.IsHuman ? -100.0f : 0.0f; + minVitality = element.GetAttributeFloat(nameof(MinVitality), character.IsHuman ? -100.0f : 0.0f); limbHealths.Clear(); limbHealthElement ??= element; @@ -369,7 +375,7 @@ namespace Barotrauma public Limb GetAfflictionLimb(Affliction affliction) { - if (afflictions.TryGetValue(affliction, out LimbHealth limbHealth)) + if (affliction != null && afflictions.TryGetValue(affliction, out LimbHealth limbHealth)) { if (limbHealth == null) { return null; } int limbHealthIndex = limbHealths.IndexOf(limbHealth); @@ -466,13 +472,17 @@ namespace Barotrauma public float GetResistance(AfflictionPrefab afflictionPrefab) { + // This is a % resistance (0 to 1.0) float resistance = 0.0f; foreach (KeyValuePair kvp in afflictions) { var affliction = kvp.Key; resistance += affliction.GetResistance(afflictionPrefab.Identifier); } - return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); + // This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance + float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab); + // The returned value is calculated to be a % resistance again + return 1 - ((1 - resistance) * abilityResistanceMultiplier); } public float GetStatValue(StatTypes statType) @@ -506,7 +516,7 @@ namespace Barotrauma ReduceMatchingAfflictions(amount, treatmentAction); } - public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) + public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null) { if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } @@ -519,7 +529,7 @@ namespace Barotrauma } } - ReduceMatchingAfflictions(amount, treatmentAction); + ReduceMatchingAfflictions(amount, treatmentAction, attacker); } private IEnumerable GetAfflictionsForLimb(Limb targetLimb) @@ -531,11 +541,11 @@ namespace Barotrauma matchingAfflictions.Clear(); matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); - + ReduceMatchingAfflictions(amount, treatmentAction); } - - public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) + + public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null) { if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } @@ -550,14 +560,22 @@ namespace Barotrauma matchingAfflictions.Add(affliction.Key); } } - ReduceMatchingAfflictions(amount, treatmentAction); + ReduceMatchingAfflictions(amount, treatmentAction, attacker); } - private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction) + private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null) { if (matchingAfflictions.Count == 0) { return; } float reduceAmount = amount / matchingAfflictions.Count; + + if (reduceAmount > 0f) + { + var abilityReduceAffliction = new AbilityReduceAffliction(Character, reduceAmount); + attacker?.CheckTalents(AbilityEffectType.OnReduceAffliction, abilityReduceAffliction); + reduceAmount = abilityReduceAffliction.Value; + } + for (int i = matchingAfflictions.Count - 1; i >= 0; i--) { var matchingAffliction = matchingAfflictions[i]; @@ -737,18 +755,23 @@ namespace Barotrauma return; } } - if (Character.Params.Health.PoisonImmunity && - (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { return; } + if (Character.Params.Health.PoisonImmunity) + { + if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + { + return; + } + } if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; } if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; } + if (Character.Params.Health.ImmunityIdentifiers.Contains(newAffliction.Identifier)) { return; } Affliction existingAffliction = null; - foreach (KeyValuePair kvp in afflictions) + foreach ((Affliction affliction, LimbHealth value) in afflictions) { - var affliction = kvp.Key; - if (kvp.Value == limbHealth && kvp.Key.Prefab == newAffliction.Prefab) + if (value == limbHealth && affliction.Prefab == newAffliction.Prefab) { - existingAffliction = kvp.Key; + existingAffliction = affliction; break; } } @@ -974,10 +997,8 @@ namespace Barotrauma IsParalyzed = false; if (Unkillable || Character.GodMode) { return; } - foreach (KeyValuePair kvp in afflictions) + foreach (var (affliction, limbHealth) in afflictions) { - var affliction = kvp.Key; - var limbHealth = kvp.Value; float vitalityDecrease = affliction.GetVitalityDecrease(this); if (limbHealth != null) { @@ -1117,9 +1138,8 @@ namespace Barotrauma /// and negative treatment suitabilities (e.g. a medicine that causes oxygen loss may not be suitable if the character is already suffocating) /// /// A dictionary where the key is the identifier of the item and the value the suitability - /// If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable. /// If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds. - public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) + public void GetSuitableTreatments(Dictionary treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) { //key = item identifier //float = suitability @@ -1129,7 +1149,9 @@ namespace Barotrauma { var affliction = kvp.Key; var limbHealth = kvp.Value; - if (limb != null && affliction.Prefab.IndicatorLimb != limb.type) + if (limb != null && + affliction.Prefab.LimbSpecific && + GetMatchingLimbHealth(affliction) != GetMatchingLimbHealth(limb)) { if (limbHealth == null) { continue; } int healthIndex = limbHealths.IndexOf(limbHealth); @@ -1145,8 +1167,7 @@ namespace Barotrauma //other afflictions of the same type increase the "treatability" // e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false); - if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; } - + if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; } if (ignoreHiddenAfflictions) @@ -1164,6 +1185,13 @@ namespace Barotrauma foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitabilities) { float suitability = treatment.Value * strength; + if (suitability > 0) + { + //if this a suitable treatment, ignore it if the affliction isn't severe enough to treat + //if the suitability is negative though, we need to take it into account! + //otherwise we may end up e.g. giving too much opiates to someone already close to overdosing + if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; } + } if (treatment.Value > strength) { //avoid using very effective meds on small injuries @@ -1182,14 +1210,6 @@ namespace Barotrauma maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability); } } - //normalize the suitabilities to a range of 0 to 1 - if (normalize) - { - foreach (Identifier treatment in treatmentSuitability.Keys.ToList()) - { - treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability); - } - } } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 630e18265..10451960e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -102,6 +102,9 @@ namespace Barotrauma [Serialize(float.PositiveInfinity, IsPropertySaveable.No)] public float ReportRange { get; protected set; } + + [Serialize(float.PositiveInfinity, IsPropertySaveable.No)] + public float FindWeaponsRange { get; protected set; } public Identifier[] PreferredOutpostModuleTypes { get; protected set; } @@ -172,6 +175,7 @@ namespace Barotrauma } } humanAI.ReportRange = ReportRange; + humanAI.FindWeaponsRange = FindWeaponsRange; humanAI.AimSpeed = AimSpeed; humanAI.AimAccuracy = AimAccuracy; } @@ -182,6 +186,7 @@ namespace Barotrauma { humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200) { + FaceTargetOnCompleted = false, DebugLogWhenFails = false, IsWaitOrder = true, CloseEnough = 100 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 52d67296f..b6de960f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -419,6 +419,24 @@ namespace Barotrauma } } + + public Vector2 DrawPosition + { + get + { + if (Removed) + { +#if DEBUG + DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); +#endif + GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:DrawPosition", GameAnalyticsManager.ErrorSeverity.Error, + "Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); + return Vector2.Zero; + } + return body.DrawPosition; + } + } + public float Rotation { get @@ -488,6 +506,19 @@ namespace Barotrauma } } + private float _alpha = 1.0f; + /// + /// Can be used by status effects + /// + public float Alpha + { + get => _alpha; + set + { + _alpha = MathHelper.Clamp(value, 0.0f, 1.0f); + } + } + public int RefJointIndex => Params.RefJoint; public readonly List WearingItems = new List(); @@ -680,9 +711,9 @@ namespace Barotrauma } attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); } - if (character is { VariantOf: { IsEmpty: false } }) + if (character is { VariantOf.IsEmpty: false }) { - var attackElement = character.Params.VariantFile.Root.GetChildElement("attack"); + var attackElement = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("attack"); if (attackElement != null) { attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f); @@ -695,7 +726,7 @@ namespace Barotrauma DamageModifiers.Add(new DamageModifier(subElement, character.Name)); break; case "statuseffect": - var statusEffect = StatusEffect.Load(subElement, Name); + var statusEffect = StatusEffect.Load(subElement, character.Name + ", " + Name); if (statusEffect != null) { if (!statusEffects.ContainsKey(statusEffect.type)) @@ -793,10 +824,12 @@ namespace Barotrauma { finalDamageModifier *= character.EmpVulnerability; } - if (!character.Params.Health.PoisonImmunity && - (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) + if (!character.Params.Health.PoisonImmunity) { - finalDamageModifier *= character.PoisonVulnerability; + if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + { + finalDamageModifier *= character.PoisonVulnerability; + } } foreach (DamageModifier damageModifier in tempModifiers) { @@ -908,13 +941,13 @@ namespace Barotrauma { if (Params.BlinkFrequency > 0) { - if (blinkTimer > -TotalBlinkDurationOut) + if (BlinkTimer > -TotalBlinkDurationOut) { - blinkTimer -= deltaTime; + BlinkTimer -= deltaTime; } else { - blinkTimer = Params.BlinkFrequency; + BlinkTimer = Params.BlinkFrequency; } } if (reEnableTimer > 0) @@ -932,13 +965,14 @@ namespace Barotrauma private bool temporarilyDisabled; private float reEnableTimer = -1; + private bool originalIgnoreCollisions; public void HideAndDisable(float duration = 0, bool ignoreCollisions = true) { if (Hidden || Disabled) { return; } - if (ignoreCollisions && IgnoreCollisions) { return; } temporarilyDisabled = true; Hidden = true; Disabled = true; + originalIgnoreCollisions = IgnoreCollisions; IgnoreCollisions = ignoreCollisions; if (duration > 0) { @@ -957,7 +991,7 @@ namespace Barotrauma if (!temporarilyDisabled) { return; } Hidden = false; Disabled = false; - IgnoreCollisions = false; + IgnoreCollisions = originalIgnoreCollisions; reEnableTimer = -1; } @@ -1001,11 +1035,14 @@ namespace Barotrauma case HitDetection.Distance: if (dist < attack.DamageRange) { - structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true, customPredicate: - (Fixture f) => + Vector2 rayStart = simPos; + Vector2 rayEnd = attackSimPos; + if (Submarine == null && damageTarget is ISpatialEntity spatialEntity && spatialEntity.Submarine != null) { - return f?.Body?.UserData as string != "ruinroom"; - }); + rayStart -= spatialEntity.Submarine.SimPosition; + rayEnd -= spatialEntity.Submarine.SimPosition; + } + structureBody = Submarine.CheckVisibility(rayStart, rayEnd); if (damageTarget is Item i && i.GetComponent() != null) { // If the attack is aimed to an item and hits an item, it's successful. @@ -1228,6 +1265,8 @@ namespace Barotrauma if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; } foreach (StatusEffect statusEffect in statusEffectList) { + if (statusEffect.ShouldWaitForInterval(character, deltaTime)) { return; } + statusEffect.sourceBody = body; if (statusEffect.type == ActionType.OnDamaged) { @@ -1308,20 +1347,21 @@ namespace Barotrauma } } - private float blinkTimer; - public float BlinkPhase; + public float BlinkTimer { get; private set; } + public float BlinkPhase { get; set; } + public bool FreezeBlinkState; private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime; public void Blink() { - blinkTimer = -TotalBlinkDurationOut; + BlinkTimer = -TotalBlinkDurationOut; } public void UpdateBlink(float deltaTime, float referenceRotation) { - if (blinkTimer > -TotalBlinkDurationOut) + if (BlinkTimer > -TotalBlinkDurationOut) { if (!FreezeBlinkState) { @@ -1431,4 +1471,16 @@ namespace Barotrauma public Affliction Affliction { get; set; } } + class AbilityReduceAffliction : AbilityObject, IAbilityCharacter, IAbilityValue + { + public AbilityReduceAffliction(Character character, float value) + { + Character = character; + Value = value; + } + + public Character Character { get; set; } + public float Value { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index d88356d60..99e289b18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using Barotrauma.IO; using System; +using System.Diagnostics; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; @@ -65,22 +66,19 @@ namespace Barotrauma abstract class AnimationParams : EditableParams, IMemorizable { public Identifier SpeciesName { get; private set; } - public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run || AnimationType == AnimationType.Crouch; - public bool IsSwimAnimation => AnimationType == AnimationType.SwimSlow || AnimationType == AnimationType.SwimFast; + public bool IsGroundedAnimation => AnimationType is AnimationType.Walk or AnimationType.Run or AnimationType.Crouch; + public bool IsSwimAnimation => AnimationType is AnimationType.SwimSlow or AnimationType.SwimFast; - protected static Dictionary> allAnimations = new Dictionary>(); - /// allAnimations[speciesName][fileName] + /// + /// The cached animations of all the characters that have been loaded. + /// + private static readonly Dictionary> allAnimations = new Dictionary>(); - private float _movementSpeed; [Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)] - public float MovementSpeed - { - get => _movementSpeed; - set => _movementSpeed = value; - } - - [Serialize(1.0f, IsPropertySaveable.Yes, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"), - Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] + public float MovementSpeed { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"), + Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] public float CycleSpeed { get; set; } /// @@ -152,169 +150,214 @@ namespace Barotrauma private static string GetFolder(ContentXElement root, string filePath) { - var folder = root?.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value; + Debug.Assert(filePath != null); + Debug.Assert(root != null); + string folder = root.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value; if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Animations"); } - return folder.CleanUpPathCrossPlatform(true); + return folder.CleanUpPathCrossPlatform(correctFilenameCase: true); } /// - /// Selects a random filepath from multiple paths, matching the specified animation type. + /// Selects all file paths that match the specified animation type and filters them alphabetically. /// - public static string GetRandomFilePath(IReadOnlyList filePaths, AnimationType type) + public static IEnumerable FilterAndSortFiles(IEnumerable filePaths, AnimationType type) { - return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.ServerAndClient); - } - - /// - /// Selects all file paths that match the specified animation type. - /// - public static IEnumerable FilterFilesByType(IEnumerable filePaths, AnimationType type) - { - return filePaths.Where(f => AnimationPredicate(f, type)); - } - - private static bool AnimationPredicate(string filePath, AnimationType type) - { - var doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null) { return false; } - var typeString = doc.Root.GetAttributeString("animationtype", null); - if (string.IsNullOrWhiteSpace(typeString)) + return filePaths.Where(f => AnimationPredicate(f, type)).OrderBy(f => f, StringComparer.OrdinalIgnoreCase); + + static bool AnimationPredicate(string filePath, AnimationType type) { - typeString = doc.Root.GetAttributeString("AnimationType", "NotDefined"); + XDocument doc = XMLExtensions.TryLoadXml(filePath); + if (doc == null) { return false; } + return doc.GetRootExcludingOverride().GetAttributeEnum("animationtype", AnimationType.NotDefined) == type; } - return Enum.TryParse(typeString, out AnimationType fileType) && fileType == type; } - public static T GetDefaultAnimParams(Character character, AnimationType animType) where T : AnimationParams, new() + protected static T GetDefaultAnimParams(Character character, AnimationType animType) where T : AnimationParams, new() + { + // Using a null file definition means we are taking a first matching file from the folder. + return GetAnimParams(character, animType, file: null, throwErrors: true); + } + + protected static T GetAnimParams(Character character, AnimationType animType, Either file, bool throwErrors = true) where T : AnimationParams, new() { Identifier speciesName = character.SpeciesName; - if (!character.VariantOf.IsEmpty - && (character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeStringUnrestricted("folder", null)).IsNullOrEmpty()) + Identifier animSpecies = speciesName; + if (!character.VariantOf.IsEmpty) { - // Use the base animations defined in the base definition file. - speciesName = character.VariantOf; - } - return GetAnimParams(speciesName, animType, GetDefaultFileName(speciesName, animType)); - } - - /// - /// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path! - /// If a custom folder is used, it's defined in the character info file. - /// - public static T GetAnimParams(Identifier speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new() - { - if (!allAnimations.TryGetValue(speciesName, out Dictionary anims)) - { - anims = new Dictionary(); - allAnimations.Add(speciesName, anims); - } - if (fileName == null || !anims.TryGetValue(fileName, out AnimationParams anim)) - { - string selectedFile = null; - string folder = GetFolder(speciesName); - if (Directory.Exists(folder)) + string folder = character.Params.VariantFile?.GetRootExcludingOverride().GetChildElement("animations")?.GetAttributeContentPath("folder", character.Prefab.ContentPackage)?.Value; + if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { - var files = Directory.GetFiles(folder); - if (files.None()) + // Use the animations defined in the base definition file. + animSpecies = character.Prefab.GetBaseCharacterSpeciesName(speciesName); + } + } + return GetAnimParams(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors); + } + + private static readonly List errorMessages = new List(); + + private static T GetAnimParams(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies, AnimationType animType, Either file, bool throwErrors = true) where T : AnimationParams, new() + { + Debug.Assert(!speciesName.IsEmpty); + Debug.Assert(!animSpecies.IsEmpty); + ContentPath contentPath = null; + string fileName = null; + if (file != null) + { + if (!file.TryGet(out fileName)) + { + file.TryGet(out contentPath); + } + Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace()); + } + ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage; + Debug.Assert(contentPackage != null); + if (!allAnimations.TryGetValue(speciesName, out Dictionary animations)) + { + animations = new Dictionary(); + allAnimations.Add(speciesName, animations); + } + string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType); + if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType) + { + // Already cached. + return (T)anim; + } + if (!contentPath.IsNullOrEmpty()) + { + // Load the animation from path. + T animInstance = new T(); + if (animInstance.Load(contentPath, speciesName)) + { + if (animInstance.AnimationType == animType) { - DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation."); - selectedFile = GetDefaultFile(speciesName, animType); - } - var filteredFiles = FilterFilesByType(files, animType); - if (filteredFiles.None()) - { - DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation."); - selectedFile = GetDefaultFile(speciesName, animType); - } - else if (string.IsNullOrEmpty(fileName)) - { - // Files found, but none specified. - selectedFile = GetDefaultFile(speciesName, animType); + animations.TryAdd(contentPath.Value, animInstance); + return animInstance; } else { - selectedFile = filteredFiles.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); + errorMessages.Add($"[AnimationParams] Animation type mismatch. Expected: {animType}, Actual: {animInstance.AnimationType}. Using the default animation."); + } + } + else + { + errorMessages.Add($"[AnimationParams] Failed to load an animation {animInstance} of type {animType} from {contentPath.Value} for the character {speciesName}. Using the default animation."); + } + } + // Seek the correct animation from the character's animation folder. + string selectedFile = null; + string folder = GetFolder(animSpecies); + if (Directory.Exists(folder)) + { + string[] files = Directory.GetFiles(folder); + if (files.None()) + { + errorMessages.Add($"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation."); + } + else + { + var filteredFiles = FilterAndSortFiles(files, animType); + if (filteredFiles.None()) + { + errorMessages.Add($"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation."); + } + else if (string.IsNullOrEmpty(fileName)) + { + // Files found, but none specified -> Get a matching animation from the specified folder. + // First try to find a file that matches the default file name. If that fails, just take any file. + string defaultFileName = GetDefaultFileName(animSpecies, animType); + selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, defaultFileName)) ?? filteredFiles.First(); + } + else + { + selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, fileName)); if (selectedFile == null) { - DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations."); - selectedFile = GetDefaultFile(speciesName, animType); + errorMessages.Add($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations."); } - } + } } - else - { - DebugConsole.ThrowError($"[Animationparams] Invalid directory: {folder}. Using the default animation."); - selectedFile = GetDefaultFile(speciesName, animType); - } - if (selectedFile == null) - { - throw new Exception("[AnimationParams] Selected file null!"); - } - DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}."); - var characterPrefab = CharacterPrefab.Prefabs[speciesName]; - T a = new T(); - if (a.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName)) - { - fileName = IO.Path.GetFileNameWithoutExtension(selectedFile); - if (!anims.ContainsKey(fileName)) - { - anims.Add(fileName, a); - } - } - else - { - DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}", - contentPackage: characterPrefab.ContentPackage); - } - return a; } - return (T)anim; + else + { + errorMessages.Add($"[AnimationParams] Invalid directory: {folder}. Using the default animation."); + } + selectedFile ??= GetDefaultFile(fallbackSpecies, animType); + Debug.Assert(selectedFile != null); + if (errorMessages.None()) + { + DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}."); + } + T animationInstance = new T(); + if (animationInstance.Load(ContentPath.FromRaw(contentPackage, selectedFile), speciesName)) + { + animations.TryAdd(key, animationInstance); + } + else + { + errorMessages.Add($"[AnimationParams] Failed to load an animation {animationInstance} at {selectedFile} of type {animType} for the character {speciesName}"); + } + foreach (string errorMsg in errorMessages) + { + if (throwErrors) + { + DebugConsole.ThrowError(errorMsg, contentPackage: contentPackage); + } + else + { + DebugConsole.Log("Logging a supressed (potential) error: " + errorMsg); + } + } + errorMessages.Clear(); + return animationInstance; + + static bool PathMatchesFile(string p, string f) => IO.Path.GetFileNameWithoutExtension(p).Equals(f, StringComparison.OrdinalIgnoreCase); } public static void ClearCache() => allAnimations.Clear(); - public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type type) + public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type animationParamsType) { - if (type == typeof(HumanWalkParams)) + if (animationParamsType == typeof(HumanWalkParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(HumanRunParams)) + if (animationParamsType == typeof(HumanRunParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(HumanSwimSlowParams)) + if (animationParamsType == typeof(HumanSwimSlowParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(HumanSwimFastParams)) + if (animationParamsType == typeof(HumanSwimFastParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(HumanCrouchParams)) + if (animationParamsType == typeof(HumanCrouchParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(FishWalkParams)) + if (animationParamsType == typeof(FishWalkParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(FishRunParams)) + if (animationParamsType == typeof(FishRunParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(FishSwimSlowParams)) + if (animationParamsType == typeof(FishSwimSlowParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(FishSwimFastParams)) + if (animationParamsType == typeof(FishSwimFastParams)) { return Create(fullPath, speciesName, animationType); } - throw new NotImplementedException(type.ToString()); + throw new NotImplementedException(animationParamsType.ToString()); } /// @@ -331,7 +374,7 @@ namespace Barotrauma anims = new Dictionary(); allAnimations.Add(speciesName, anims); } - var fileName = IO.Path.GetFileNameWithoutExtension(fullPath); + string fileName = IO.Path.GetFileNameWithoutExtension(fullPath); if (anims.ContainsKey(fileName)) { DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red); @@ -340,7 +383,8 @@ namespace Barotrauma var instance = new T(); XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString())); instance.doc = new XDocument(animationElement); - var characterPrefab = CharacterPrefab.Prefabs[speciesName]; + var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); + Debug.Assert(characterPrefab != null); var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath); instance.UpdatePath(contentPath); instance.IsLoaded = instance.Deserialize(animationElement); @@ -373,16 +417,17 @@ namespace Barotrauma else { // Update the key by removing and re-adding the animation. + string fileName = FileNameWithoutExtension; if (allAnimations.TryGetValue(SpeciesName, out Dictionary animations)) { - animations.Remove(Name); + animations.Remove(fileName); } base.UpdatePath(newPath); if (animations != null) { - if (!animations.ContainsKey(Name)) + if (!animations.ContainsKey(fileName)) { - animations.Add(Name, this); + animations.Add(fileName, this); } } } @@ -421,37 +466,26 @@ namespace Barotrauma { if (isHumanoid) { - switch (type) + return type switch { - case AnimationType.Walk: - return typeof(HumanWalkParams); - case AnimationType.Run: - return typeof(HumanRunParams); - case AnimationType.Crouch: - return typeof(HumanCrouchParams); - case AnimationType.SwimSlow: - return typeof(HumanSwimSlowParams); - case AnimationType.SwimFast: - return typeof(HumanSwimFastParams); - default: - throw new NotImplementedException(type.ToString()); - } + AnimationType.Walk => typeof(HumanWalkParams), + AnimationType.Run => typeof(HumanRunParams), + AnimationType.Crouch => typeof(HumanCrouchParams), + AnimationType.SwimSlow => typeof(HumanSwimSlowParams), + AnimationType.SwimFast => typeof(HumanSwimFastParams), + _ => throw new NotImplementedException(type.ToString()) + }; } else { - switch (type) + return type switch { - case AnimationType.Walk: - return typeof(FishWalkParams); - case AnimationType.Run: - return typeof(FishRunParams); - case AnimationType.SwimSlow: - return typeof(FishSwimSlowParams); - case AnimationType.SwimFast: - return typeof(FishSwimFastParams); - default: - throw new NotImplementedException(type.ToString()); - } + AnimationType.Walk => typeof(FishWalkParams), + AnimationType.Run => typeof(FishRunParams), + AnimationType.SwimSlow => typeof(FishSwimSlowParams), + AnimationType.SwimFast => typeof(FishSwimFastParams), + _ => throw new NotImplementedException(type.ToString()) + }; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index 6a5c7aab6..4962b96dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -9,12 +9,12 @@ namespace Barotrauma { return Check(character) ? GetDefaultAnimParams(character, AnimationType.Walk) : Empty; } - public static FishWalkParams GetAnimParams(Character character, string fileName = null) + public static FishWalkParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return Check(character) ? GetAnimParams(character.SpeciesName, AnimationType.Walk, fileName) : Empty; + return Check(character) ? GetAnimParams(character, AnimationType.Walk, file, throwErrors) : null; } - protected static FishWalkParams Empty = new FishWalkParams(); + protected static readonly FishWalkParams Empty = new FishWalkParams(); public override void StoreSnapshot() => StoreSnapshot(); } @@ -25,12 +25,12 @@ namespace Barotrauma { return Check(character) ? GetDefaultAnimParams(character, AnimationType.Run) : Empty; } - public static FishRunParams GetAnimParams(Character character, string fileName = null) + public static FishRunParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return Check(character) ? GetAnimParams(character.SpeciesName, AnimationType.Run, fileName) : Empty; + return Check(character) ? GetAnimParams(character, AnimationType.Run, file, throwErrors) : null; } - protected static FishRunParams Empty = new FishRunParams(); + protected static readonly FishRunParams Empty = new FishRunParams(); public override void StoreSnapshot() => StoreSnapshot(); } @@ -38,9 +38,9 @@ namespace Barotrauma class FishSwimFastParams : FishSwimParams { public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimFast); - public static FishSwimFastParams GetAnimParams(Character character, string fileName = null) + public static FishSwimFastParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.SwimFast, fileName); + return GetAnimParams(character, AnimationType.SwimFast, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); @@ -49,9 +49,9 @@ namespace Barotrauma class FishSwimSlowParams : FishSwimParams { public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimSlow); - public static FishSwimSlowParams GetAnimParams(Character character, string fileName = null) + public static FishSwimSlowParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.SwimSlow, fileName); + return GetAnimParams(character, AnimationType.SwimSlow, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index a1920a820..48981573b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -5,9 +5,9 @@ namespace Barotrauma class HumanWalkParams : HumanGroundedParams { public static HumanWalkParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Walk); - public static HumanWalkParams GetAnimParams(Character character, string fileName = null) + public static HumanWalkParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.Walk, fileName); + return GetAnimParams(character, AnimationType.Walk, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); @@ -16,9 +16,9 @@ namespace Barotrauma class HumanRunParams : HumanGroundedParams { public static HumanRunParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Run); - public static HumanRunParams GetAnimParams(Character character, string fileName = null) + public static HumanRunParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.Run, fileName); + return GetAnimParams(character, AnimationType.Run, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); @@ -36,9 +36,9 @@ namespace Barotrauma public float ExtraTorsoAngleWhenStationary { get; set; } public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Crouch); - public static HumanCrouchParams GetAnimParams(Character character, string fileName = null) + public static HumanCrouchParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.Crouch, fileName); + return GetAnimParams(character, AnimationType.Crouch, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); @@ -47,9 +47,9 @@ namespace Barotrauma class HumanSwimFastParams: HumanSwimParams { public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimFast); - public static HumanSwimFastParams GetAnimParams(Character character, string fileName = null) + public static HumanSwimFastParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.SwimFast, fileName); + return GetAnimParams(character, AnimationType.SwimFast, file, throwErrors); } @@ -59,9 +59,9 @@ namespace Barotrauma class HumanSwimSlowParams : HumanSwimParams { public static HumanSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimSlow); - public static HumanSwimSlowParams GetAnimParams(Character character, string fileName = null) + public static HumanSwimSlowParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.SwimSlow, fileName); + return GetAnimParams(character, AnimationType.SwimSlow, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); @@ -125,6 +125,13 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes, description: "How much the horizontal difference of waist and the foot positions has an effect to lifting the foot."), Editable(DecimalCount = 2, ValueStep = 0.1f, MinValueFloat = 0f, MaxValueFloat = 1f)] public float FootLiftHorizontalFactor { get; set; } + [Serialize("0,0", IsPropertySaveable.Yes, description: "Normally the character's feet are positioned at a scaled-down version of it's normal step position - this can be used to override that value if you want to e.g. make the character to spread out it's feet more when standing."), Editable(DecimalCount = 2, ValueStep = 0.01f)] + public Vector2 StepSizeWhenStanding + { + get; + set; + } + /// /// In degrees. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 7da09bc6b..7a8544a75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -20,31 +20,31 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes), Editable] public Identifier SpeciesName { get; private set; } - [Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "References to another species. Define only if the creature is a variant that needs to use a pre-existing translation."), Editable] public Identifier SpeciesTranslationOverride { get; private set; } - [Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "Overrides the name of the character, shown to the player. If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] public string DisplayName { get; private set; } - [Serialize("", IsPropertySaveable.Yes, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "If defined, different species of the same group consider each other friendly and do not attack each other."), Editable] public Identifier Group { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)] + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the character is a humanoid and has different animation constraints relative to non-humanoid characters."), Editable(ReadOnly = true)] public bool Humanoid { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)] + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, jobs can be assigned to characters of this species. Should be true for the player characters."), Editable(ReadOnly = true)] public bool HasInfo { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature interact with items?"), Editable] public bool CanInteract { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should this character be treated as a husk?"), Editable] public bool Husk { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description:"Should this character use a special husk appendage, attached to the ragdoll, when it turns into a husk?"), Editable] public bool UseHuskAppendage { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Does this character need oxygen to survive? Enabling this also makes the character vulnerable to high pressure when swimming outside of the submarine."), Editable] public bool NeedsAir { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable] @@ -56,13 +56,13 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Is this creature an artificial creature, like robot or machine that shouldn't be affected by afflictions that affect only organic creatures? Overrides DoesBleed."), Editable] public bool IsMachine { get; set; } - [Serialize(false, IsPropertySaveable.No), Editable] + [Serialize(false, IsPropertySaveable.No, description:"Is the character able to send messages in the chat?"), Editable] public bool CanSpeak { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description:"Is there a health bar shown above the character when it takes damage? Defaults to true."), Editable] public bool ShowHealthBar { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Is this character's health shown at the top of the player's screen when they are in an active encounter?"), Editable] public bool UseBossHealthBar { get; private set; } [Serialize(100f, IsPropertySaveable.Yes, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)] @@ -80,7 +80,7 @@ namespace Barotrauma [Serialize("waterblood", IsPropertySaveable.Yes), Editable] public string BleedParticleWater { get; private set; } - [Serialize(1f, IsPropertySaveable.Yes), Editable] + [Serialize(1f, IsPropertySaveable.Yes, description: "A multiplier to increase or decrease the number of bleeding particles to create."), Editable] public float BleedParticleMultiplier { get; private set; } [Serialize(true, IsPropertySaveable.Yes, description: "Can the creature eat bodies? Used by player controlled creatures to allow them to eat. Currently applicable only to non-humanoids. To allow an AI controller to eat, just add an ai target with the state \"eat\""), Editable] @@ -89,22 +89,22 @@ namespace Barotrauma [Serialize(10f, IsPropertySaveable.Yes, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)] public float EatingSpeed { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the character AI use waypoints defined in the level to find a path to its targets?"), Editable] public bool UsePathFinding { get; set; } [Serialize(1f, IsPropertySaveable.Yes, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)] public float PathFinderPriority { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the character be hidden in the sonar?"), Editable] public bool HideInSonar { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the character be hidden when using thermal goggles?"), Editable] public bool HideInThermalGoggles { get; set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "If set to a value greater than zero, this character creates disrupting noise on the sonar when within range."), Editable] public float SonarDisruption { get; set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "Range at which \"long distance\" blips for this character will appear on the sonar (used on some of the Abyss monsters)."), Editable] public float DistantSonarRange { get; set; } [Serialize(25000f, IsPropertySaveable.Yes, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)] @@ -113,7 +113,7 @@ namespace Barotrauma [Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] public float SoundInterval { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the character be drawn on top of characters that do not have this set? This currently has no effect if the character has no deformable sprites."), Editable] public bool DrawLast { get; set; } [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons. Defaults to 1. Set 0 to tell the bots not to target this character at all. Distance to the target affects the decision making."), Editable] @@ -543,7 +543,7 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool PoisonImmunity { get; set; } - + [Serialize(1f, IsPropertySaveable.Yes, description: "1 = default, 0 = immune."), Editable(MinValueFloat = 0f, MaxValueFloat = 1000, DecimalCount = 1)] public float PoisonVulnerability { get; set; } @@ -552,6 +552,19 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable] public bool ApplyAfflictionColors { get; private set; } + + [Serialize("", IsPropertySaveable.Yes, description:"A comma-separated list of identifiers of afflictions that the creature is immune to."), Editable] + public string Immunities { get; private set; } + + private ImmutableHashSet _immunityIdentifiers; + public IEnumerable ImmunityIdentifiers + { + get + { + _immunityIdentifiers ??= Element.GetAttributeIdentifierArray("immunities", Array.Empty()).ToImmutableHashSet(); + return _immunityIdentifiers; + } + } // TODO: limbhealths, sprite? @@ -634,6 +647,9 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)] public float Hearing { get; private set; } + [Serialize(-1.0f, IsPropertySaveable.Yes, description: "Hard limit to how far the character can spot targets from, regardless of the sight/hearing or how visible or how much noise the target is making. Not used if set to negative."), Editable] + public float MaxPerceptionDistance { get; set; } + [Serialize(100f, IsPropertySaveable.Yes, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)] public float AggressionHurt { get; private set; } @@ -673,7 +689,7 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] public bool CanOpenDoors { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description:"Unlike human AI, monsters normally only use pathfinding when they are inside the submarine. When this is enabled, the monsters can also use pathfinding to get inside the sub. In practice, via doors and hatches."), Editable] public bool UsePathFindingToGetInside { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable] @@ -690,17 +706,18 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable] public bool PatrolDry { get; set; } - - [Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable] + + [Serialize(0f, IsPropertySaveable.Yes, description: "Initial aggression used in the circle attack pattern (0-100). The aggression affects how close and how fast to the target the monster circles."), Editable] public float StartAggression { get; private set; } - [Serialize(100f, IsPropertySaveable.Yes, description: ""), Editable] + [Serialize(100f, IsPropertySaveable.Yes, description: "Maximum aggression used in the circle attack pattern (0-100). The aggression affects how close and how fast to the target the monster circles."), Editable] public float MaxAggression { get; private set; } - [Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "How quickly the aggression level increases from StartAggression to MaxAggression when using the circle attack pattern. Artificial amount, applied once per attack cycle."), Editable] + public float AggressionCumulation { get; private set; } - [Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: ""), Editable] + [Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: "Defines the method of checking whether there's a blocking (submarine) wall."), Editable] public WallTargetingMethod WallTargetingMethod { get; private set; } public IEnumerable Targets => targets; @@ -851,6 +868,12 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable] public bool IgnoreOutside { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside. Doesn't matter where the creature itself is."), Editable] + public bool IgnoreTargetInside { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's outside. Doesn't matter where the creature itself is."), Editable] + public bool IgnoreTargetOutside { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable] public bool IgnoreIfNotInSameSub { get; set; } @@ -866,10 +889,16 @@ namespace Barotrauma [Serialize(-1f, IsPropertySaveable.Yes, description: "A generic max threshold. Not used if set to negative."), Editable] public float ThresholdMax { get; private set; } - [Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Can be used to make the monster perceive the target further than it normally can."), Editable] + public float PerceptionDistanceMultiplier { get; private set; } + + [Serialize(-1.0f, IsPropertySaveable.Yes, description: "Maximum distance at which the monster can perceive the target, regardless of the sight/hearing or how visible or how much noise the target is making. Not used if set to negative."), Editable] + public float MaxPerceptionDistance { get; private set; } + + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "A generic offset. Used for example for offsetting the react distance (vector length) and for offsetting the target position when a guardian flees to a pod."), Editable] public Vector2 Offset { get; private set; } - [Serialize(AttackPattern.Straight, IsPropertySaveable.Yes), Editable] + [Serialize(AttackPattern.Straight, IsPropertySaveable.Yes, description: "Defines the movement pattern of the character when approaching a target."), Editable] public AttackPattern AttackPattern { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the AI will give more priority to targets close to the horizontal middle of the sub. Only applies to walls, hulls, and items like sonar. Circle and Sweep always does this regardless of this property."), Editable] @@ -887,31 +916,48 @@ namespace Barotrauma #endregion #region Circle - [Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] + [Serialize(5000f, IsPropertySaveable.Yes, description:"How close to the target the character should be, before they start using the circle pattern instead of directional approaching."), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] public float CircleStartDistance { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description:"Normally the target size is taken into account when calculating the distance to the target. Set this true to skip that.")] public bool IgnoreTargetSize { get; private set; } - [Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)] + [Serialize(1f, IsPropertySaveable.Yes, description:"Determines the rate how quickly the target movement position is rotated towards the attack target. The actual rotation is calculated once per each attack cycle, based on the current aggression level."), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)] public float CircleRotationSpeed { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description:"When enabled, the circle rotation speed can change when the target is far. When this setting is disabled (default), the character will head directly towards the target when it's too far."), Editable] public bool DynamicCircleRotationSpeed { get; private set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)] + [Serialize(0f, IsPropertySaveable.Yes, description:"How much the turn speed can differ between attack cycles (stays constant during the cycle)"), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)] public float CircleRandomRotationFactor { get; private set; } - [Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)] + [Serialize(5f, IsPropertySaveable.Yes, description:"Affects how close to the target the character has to be before the strike phase of the circle behavior triggers. In the strike phase, the creature moves directly towards the target."), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)] public float CircleStrikeDistanceMultiplier { get; private set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] + [Serialize(0f, IsPropertySaveable.Yes, description:"How much the target position is offset at maximum. Low values make the character hit the target earlier/always, higher values make it miss the target when the aggression intensity is low (early in the encounter)."), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] public float CircleMaxRandomOffset { get; private set; } #endregion - public TargetParams(ContentXElement element, CharacterParams character) : base(element, character) { } + /// + /// Conditionals that must be met for the character to be able to use these targeting parameters. + /// + public List Conditionals { get; private set; } = new List(); - public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(character, tag, state, priority), character) { } + public TargetParams(string tag, AIState state, float priority, CharacterParams character) : + this(CreateNewElement(character, tag, state, priority), character) { } + + public TargetParams(ContentXElement element, CharacterParams character) : base(element, character) + { + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "conditional": + Conditionals.AddRange(PropertyConditional.FromXElement(subElement)); + break; + } + } + } public static ContentXElement CreateNewElement(CharacterParams character, Identifier tag, AIState state, float priority) => CreateNewElement(character, tag.Value, state, priority); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs index 72ad781a8..dca3b5dc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs @@ -16,6 +16,7 @@ namespace Barotrauma public bool IsLoaded { get; protected set; } public string Name { get; private set; } public string FileName { get; private set; } + public string FileNameWithoutExtension { get; private set; } public string Folder { get; private set; } public ContentPath Path { get; protected set; } = ContentPath.Empty; public Dictionary SerializableProperties { get; protected set; } @@ -103,8 +104,9 @@ namespace Barotrauma { Path = fullPath; Name = GetName(); - FileName = System.IO.Path.GetFileName(Path.Value); - Folder = System.IO.Path.GetDirectoryName(Path.Value); + FileName = Barotrauma.IO.Path.GetFileName(Path.Value); + FileNameWithoutExtension = Barotrauma.IO.Path.GetFileNameWithoutExtension(Path.Value); + Folder = Barotrauma.IO.Path.GetDirectoryName(Path.Value); } public virtual bool Save(string fileNameWithoutExtension = null, System.Xml.XmlWriterSettings settings = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 26e8ccfec..f4bf452ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Xml.Linq; using System.Linq; using Barotrauma.IO; @@ -13,15 +14,31 @@ using Barotrauma.SpriteDeformations; namespace Barotrauma { + public enum CanEnterSubmarine + { + /// + /// No part of the ragdoll can go inside a submarine + /// + False, + /// + /// Can fully enter a submarine. Make sure to only allow this on small/medium sized creatures that can reasonably fit inside rooms. + /// + True, + /// + /// The ragdoll's limbs can enter the sub, but the collider can't. + /// Can be used to e.g. allow the monster's head to poke into the sub to bite characters, even if the whole monster can't fit in the sub. + /// + Partial + } + class HumanRagdollParams : RagdollParams { - public static HumanRagdollParams GetRagdollParams(Identifier speciesName, string fileName = null) => GetRagdollParams(speciesName, fileName); - public static HumanRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams(speciesName); + public static HumanRagdollParams GetDefaultRagdollParams(Character character) => GetDefaultRagdollParams(character); } class FishRagdollParams : RagdollParams { - public static FishRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams(speciesName); + public static FishRagdollParams GetDefaultRagdollParams(Character character) => GetDefaultRagdollParams(character); } class RagdollParams : EditableParams, IMemorizable @@ -37,8 +54,11 @@ namespace Barotrauma [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()] public Color Color { get; set; } - - [Serialize(0.0f, IsPropertySaveable.Yes, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)] + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "General orientation of the sprites as drawn on the spritesheet. " + + "Defines the \"forward direction\" of the sprites. Should be configured as the direction pointing outwards from the main limb. " + + "Incorrectly defined orientations may lead to limbs being rotated incorrectly when e.g. when the character aims or flips to face a different direction. " + + "Can be overridden per sprite by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)] public float SpritesheetOrientation { get; set; } public bool IsSpritesheetOrientationHorizontal @@ -53,11 +73,19 @@ namespace Barotrauma private float limbScale; [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] - public float LimbScale { get { return limbScale; } set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } } + public float LimbScale + { + get { return limbScale; } + set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } + } private float jointScale; [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] - public float JointScale { get { return jointScale; } set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } } + public float JointScale + { + get { return jointScale; } + set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } + } // Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize? [Serialize(1f, IsPropertySaveable.No)] @@ -69,8 +97,8 @@ namespace Barotrauma [Serialize(50f, IsPropertySaveable.Yes, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float ImpactTolerance { get; set; } - [Serialize(true, IsPropertySaveable.Yes, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()] - public bool CanEnterSubmarine { get; set; } + [Serialize(CanEnterSubmarine.True, IsPropertySaveable.Yes, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()] + public CanEnterSubmarine CanEnterSubmarine { get; set; } [Serialize(true, IsPropertySaveable.Yes), Editable] public bool CanWalk { get; set; } @@ -86,7 +114,7 @@ namespace Barotrauma /// key2: File path /// value: Ragdoll parameters /// - private readonly static Dictionary> allRagdolls = new Dictionary>(); + private static readonly Dictionary> allRagdolls = new Dictionary>(); public List Colliders { get; private set; } = new List(); public List Limbs { get; private set; } = new List(); @@ -106,8 +134,7 @@ 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}'", - contentPackage: contentPackage); + DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", contentPackage: contentPackage); return string.Empty; } return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value); @@ -115,99 +142,151 @@ namespace Barotrauma private static string GetFolder(ContentXElement root, string filePath) { - var folder = root?.GetChildElement("ragdolls")?.GetAttributeContentPath("folder")?.Value; + Debug.Assert(filePath != null); + Debug.Assert(root != null); + string folder = (root.GetChildElement("ragdolls") ?? root.GetChildElement("ragdoll"))?.GetAttributeContentPath("folder")?.Value; if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Ragdolls") + IO.Path.DirectorySeparatorChar; } return folder.CleanUpPathCrossPlatform(correctFilenameCase: true); } - - public static T GetDefaultRagdollParams(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams(speciesName); - - /// - /// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path! - /// If a custom folder is used, it's defined in the character info file. - /// - public static T GetRagdollParams(Identifier speciesName, string fileName = null) where T : RagdollParams, new() + + public static T GetDefaultRagdollParams(Character character) where T : RagdollParams, new() => GetDefaultRagdollParams(character.SpeciesName, character.Params, character.Prefab.ContentPackage); + + public static T GetDefaultRagdollParams(Identifier speciesName, CharacterParams characterParams, ContentPackage contentPackage) where T : RagdollParams, new() { - if (speciesName.IsEmpty) + XElement mainElement = characterParams.VariantFile?.Root ?? characterParams.MainElement; + return GetDefaultRagdollParams(speciesName, mainElement, contentPackage); + } + + public static T GetDefaultRagdollParams(Identifier speciesName, XElement characterRootElement, ContentPackage contentPackage) where T : RagdollParams, new() + { + Debug.Assert(contentPackage != null); + if (characterRootElement.IsOverride()) { - throw new Exception($"Species name null or empty!"); + characterRootElement = characterRootElement.FirstElement(); } + Identifier ragdollSpecies = speciesName; + Identifier variantOf = characterRootElement.VariantOf(); + if (characterRootElement != null && (characterRootElement.GetChildElement("ragdolls") ?? characterRootElement.GetChildElement("ragdoll")) is XElement ragdollElement) + { + if ((ragdollElement.GetAttributeContentPath("path", contentPackage) ?? ragdollElement.GetAttributeContentPath("file", contentPackage)) is ContentPath path) + { + return GetRagdollParams(speciesName, ragdollSpecies, file: path, contentPackage); + } + else if (!variantOf.IsEmpty) + { + string folder = ragdollElement.GetAttributeContentPath("folder", contentPackage)?.Value; + if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) + { + // Folder attribute not defined or set to default -> use the ragdoll defined in the base definition file. + if (CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab prefab) + { + ragdollSpecies = prefab.GetBaseCharacterSpeciesName(variantOf); + } + } + } + } + else if (!variantOf.IsEmpty && CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab prefab) + { + // Ragdoll element not defined -> use the ragdoll defined in the base definition file. + ragdollSpecies = prefab.GetBaseCharacterSpeciesName(variantOf); + } + // Using a null file definition means we use the default animations found in the Ragdolls folder. + return GetRagdollParams(speciesName, ragdollSpecies, file: null, contentPackage); + } + + public static T GetRagdollParams(Identifier speciesName, Identifier ragdollSpecies, Either file, ContentPackage contentPackage) where T : RagdollParams, new() + { + Debug.Assert(!speciesName.IsEmpty); + Debug.Assert(!ragdollSpecies.IsEmpty); + ContentPath contentPath = null; + string fileName = null; + if (file != null) + { + if (!file.TryGet(out fileName)) + { + file.TryGet(out contentPath); + } + Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace()); + } + Debug.Assert(contentPackage != null); if (!allRagdolls.TryGetValue(speciesName, out Dictionary ragdolls)) { ragdolls = new Dictionary(); allRagdolls.Add(speciesName, ragdolls); } - if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll)) + string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies); + if (ragdolls.TryGetValue(key, out RagdollParams ragdoll)) { + // Already cached. return (T)ragdoll; } - string selectedFile = null; - Identifier ragdollSpecies = speciesName; - if (CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab)) + if (!contentPath.IsNullOrEmpty()) { - if (!prefab.VariantOf.IsEmpty) + // Load the ragdoll from path. + T ragdollInstance = new T(); + if (ragdollInstance.Load(contentPath, ragdollSpecies)) { - ragdollSpecies = prefab.VariantOf; + ragdolls.TryAdd(contentPath.Value, ragdollInstance); + return ragdollInstance; } - string error = null; - string folder = GetFolder(ragdollSpecies); - if (!Directory.Exists(folder)) + else { - error = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll."; + DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {ragdollInstance} from {contentPath.Value} for the character {speciesName}. Using the default ragdoll.", contentPackage: contentPackage); + } + } + // Seek the default ragdoll from the character's ragdoll folder. + string selectedFile; + string folder = GetFolder(ragdollSpecies); + if (Directory.Exists(folder)) + { + var files = Directory.GetFiles(folder).OrderBy(f => f, StringComparer.OrdinalIgnoreCase); + if (files.None()) + { + DebugConsole.ThrowError($"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.", contentPackage: contentPackage); selectedFile = GetDefaultFile(ragdollSpecies); } else { - string[] files = Directory.GetFiles(folder); - if (files.None()) + if (string.IsNullOrEmpty(fileName)) { - error = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll."; - selectedFile = GetDefaultFile(ragdollSpecies); - } - else if (string.IsNullOrEmpty(fileName)) - { - // Files found, but none specified - selectedFile = GetDefaultFile(ragdollSpecies); + // Files found, but none specified -> Get a matching ragdoll from the specified folder. + // First try to find a file that matches the default file name. If that fails, just take any file. + string defaultFileName = GetDefaultFileName(ragdollSpecies); + selectedFile = files.FirstOrDefault(f => f.Contains(defaultFileName, StringComparison.OrdinalIgnoreCase)) ?? files.First(); } else { selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); if (selectedFile == null) { - error = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."; + DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.", contentPackage: contentPackage); selectedFile = GetDefaultFile(ragdollSpecies); } - } + } } - if (error != null) - { - DebugConsole.ThrowError(error, - contentPackage: prefab?.ContentPackage); - } - } - if (selectedFile == null) - { - throw new Exception("[RagdollParams] Selected file null!"); - } - DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}."); - var characterPrefab = CharacterPrefab.Prefabs[speciesName]; - T r = new T(); - if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), ragdollSpecies)) - { - if (!ragdolls.ContainsKey(r.Name)) - { - ragdolls.Add(r.Name, r); - } - return r; } else { - // Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harded to debug. It's better to fail early. + DebugConsole.ThrowError($"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.", contentPackage: contentPackage); + selectedFile = GetDefaultFile(ragdollSpecies); + } + + Debug.Assert(selectedFile != null); + DebugConsole.Log($"[RagdollParams] Loading the ragdoll from {selectedFile}."); + T r = new T(); + if (r.Load(ContentPath.FromRaw(contentPackage, selectedFile), speciesName)) + { + ragdolls.TryAdd(key, r); + } + else + { + // Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harder to debug. It's better to fail early. throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}."); } + return r; } /// @@ -234,9 +313,9 @@ namespace Barotrauma instance.IsLoaded = instance.Deserialize(mainElement); instance.Save(); instance.Load(contentPath, speciesName); - ragdolls.Add(instance.Name, instance); + ragdolls.Add(instance.FileNameWithoutExtension, instance); DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite); - return instance as T; + return instance; } public static void ClearCache() => allRagdolls.Clear(); @@ -250,16 +329,17 @@ namespace Barotrauma else { // Update the key by removing and re-adding the ragdoll. + string fileName = FileNameWithoutExtension; if (allRagdolls.TryGetValue(SpeciesName, out Dictionary ragdolls)) { - ragdolls.Remove(Name); + ragdolls.Remove(fileName); } base.UpdatePath(fullPath); if (ragdolls != null) { - if (!ragdolls.ContainsKey(Name)) + if (!ragdolls.ContainsKey(fileName)) { - ragdolls.Add(Name, this); + ragdolls.Add(fileName, this); } } } @@ -282,6 +362,7 @@ namespace Barotrauma { if (Load(file)) { + isVariantScaleApplied = false; SpeciesName = speciesName; CreateColliders(); CreateLimbs(); @@ -398,18 +479,21 @@ namespace Barotrauma } #endif - private bool variantScaleApplied; - public void ApplyVariantScale(XDocument variantFile) + private bool isVariantScaleApplied; + public void TryApplyVariantScale(XDocument variantFile) { - if (variantScaleApplied) { return; } + if (isVariantScaleApplied) { return; } if (variantFile == null) { return; } - var scaleMultiplier = variantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("scalemultiplier", 1f); - if (scaleMultiplier.HasValue) + if (variantFile.GetRootExcludingOverride() is XElement root) { - JointScale *= scaleMultiplier.Value; - LimbScale *= scaleMultiplier.Value; + if ((root.GetChildElement("ragdoll") ?? root.GetChildElement("ragdolls")) is XElement ragdollElement) + { + float scaleMultiplier = ragdollElement.GetAttributeFloat("scalemultiplier", 1f); + JointScale *= scaleMultiplier; + LimbScale *= scaleMultiplier; + } } - variantScaleApplied = true; + isVariantScaleApplied = true; } #endregion @@ -623,8 +707,11 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Disable drawing for this limb."), Editable()] public bool Hide { get; set; } - - [Serialize(float.NaN, IsPropertySaveable.Yes, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] + + [Serialize(float.NaN, IsPropertySaveable.Yes, description: "Orientation of the sprite as drawn on the spritesheet. " + + "Defines the \"forward direction\" of the sprite. Should be configured as the direction pointing outwards from the main limb." + + "Incorrectly defined orientations may lead to limbs being rotated incorrectly when e.g. when the character aims or flips to face a different direction. " + + "Overrides the value of 'Spritesheet Orientation' for this limb."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] public float SpriteOrientation { get; set; } [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "If set, the limb sprite will use the same sprite depth as the specified limb. Generally only useful for limbs that get added on the ragdoll on the fly (e.g. extra limbs added via gene splicing).")] @@ -744,6 +831,9 @@ namespace Barotrauma [Serialize(0.05f, IsPropertySaveable.Yes)] public float Restitution { get; set; } + [Serialize(true, IsPropertySaveable.Yes, description: "Can the limb enter submarines? Only valid if the ragdoll's CanEnterSubmarine is set to Partial, otherwise the limb can enter if the ragdoll can."), Editable] + public bool CanEnterSubmarine { get; private set; } + public LimbParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { var spriteElement = element.GetChildElement("sprite"); 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 961cfcf71..ac37854de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs @@ -12,9 +12,9 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { - if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction) + if (abilityObject is IAbilityAffliction { Affliction: Affliction affliction }) { - return afflictions.Any(a => a == affliction.Identifier); + return afflictions.Any(a => a == affliction.Identifier || a == affliction.Prefab.AfflictionType); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionInSubmarine.cs similarity index 57% rename from Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs rename to Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionInSubmarine.cs index f237deea3..a4bbb17f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionInSubmarine.cs @@ -1,16 +1,15 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { - class AbilityConditionItemInSubmarine : AbilityConditionData + [TypePreviouslyKnownAs("AbilityConditionItemInSubmarine")] + class AbilityConditionInSubmarine : AbilityConditionData { private readonly SubmarineType? submarineType; - public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { if (conditionElement.GetAttribute("submarinetype") != null) { - submarineType = conditionElement.GetAttributeEnum("submarinetype", SubmarineType.Player); + submarineType = conditionElement.GetAttributeEnum("submarinetype", SubmarineType.Player); } } @@ -30,9 +29,15 @@ namespace Barotrauma.Abilities } else { - LogAbilityConditionError(abilityObject, typeof(IAbilityItem)); - return false; + return MatchesCondition(); } } + + public override bool MatchesCondition() + { + if (character.Submarine is null) { return false; } + + return character.Submarine?.Info?.Type == submarineType; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs index f14552583..512ef0c01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs @@ -8,6 +8,7 @@ namespace Barotrauma.Abilities { private readonly Option matchedLevel; private readonly Option minLevel; + private readonly Option maxLevel; public AbilityConditionHasLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { @@ -18,23 +19,33 @@ namespace Barotrauma.Abilities minLevel = conditionElement.GetAttributeInt("minlevel", 0) is var min and not 0 ? Option.Some(min) : Option.None(); + + maxLevel = conditionElement.GetAttributeInt("maxlevel", 0) is var max and not 0 + ? Option.Some(max) + : Option.None(); - if (matchedLevel.IsNone() && minLevel.IsNone()) + if (matchedLevel.IsNone() && minLevel.IsNone() && maxLevel.IsNone()) { - throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\" or \"minlevel\" attribute."); + throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\", \"minlevel\" or \"maxlevel\" attribute."); } } protected override bool MatchesConditionSpecific() { + var currentLevel = character.Info.GetCurrentLevel(); if (matchedLevel.TryUnwrap(out int match)) { - return character.Info.GetCurrentLevel() == match; + return currentLevel == match; } if (minLevel.TryUnwrap(out int min)) { - return character.Info.GetCurrentLevel() >= min; + return currentLevel >= min; + } + + if (maxLevel.TryUnwrap(out int max)) + { + return currentLevel <= max; } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs index 3cc7c0eac..231b01487 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs @@ -12,7 +12,9 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific() { - return character.IsRagdolled || character.Stun > 0f || character.IsIncapacitated; + // TODO: Should we only check whether the target is ragdolling here? + // Or should we use character.IsKnockedDown instead? + return (character.IsRagdolled && !character.AnimController.IsHangingWithRope) || character.Stun > 0f || character.IsIncapacitated; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 7ccc4e036..9d2ab1c13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -100,7 +100,7 @@ namespace Barotrauma.Abilities string type = abilityElement.Name.ToString().ToLowerInvariant(); try { - abilityType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); + abilityType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true); if (abilityType == null) { if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs index 3b9653393..1eba6a358 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs @@ -21,24 +21,42 @@ } } + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } + } + + protected override void ApplyEffect() + { + ApplyAfflictionToCharacter(Character); + } + protected override void ApplyEffect(AbilityObject abilityObject) { if (abilityObject is IAbilityCharacter character) { - 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}\".", - contentPackage: CharacterTalent.Prefab.ContentPackage); - return; - } - float strength = this.strength; - if (!string.IsNullOrEmpty(multiplyStrengthBySkill)) - { - strength *= Character.GetSkillLevel(multiplyStrengthBySkill); - } - character.Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), allowStacking: !setValue); + ApplyAfflictionToCharacter(character.Character); } } + + private void ApplyAfflictionToCharacter(Character character) + { + 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}\".", + contentPackage: CharacterTalent.Prefab.ContentPackage); + return; + } + float strength = this.strength; + if (!string.IsNullOrEmpty(multiplyStrengthBySkill)) + { + strength *= Character.GetSkillLevel(multiplyStrengthBySkill); + } + character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), allowStacking: !setValue); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs index 062032ca7..98269e784 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs @@ -15,12 +15,13 @@ DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.", contentPackage: abilityElement.ContentPackage); } + + // NOTE: The resistance value is a multiplier here, so 1.0 == 0% resistance if (MathUtils.NearlyEqual(multiplier, 1)) { DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.", contentPackage: abilityElement.ContentPackage); } - } public override void InitializeAbility(bool addingFirstTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs index 4f64d8bfd..c519be61a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Abilities protected override void ApplyEffect(AbilityObject abilityObject) { if (abilityObject is not IAbilityCharacter character) { return; } - character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount); + character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount, attacker: Character); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUpgradeSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUpgradeSubmarine.cs new file mode 100644 index 000000000..a7387e414 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUpgradeSubmarine.cs @@ -0,0 +1,57 @@ +#nullable enable +namespace Barotrauma.Abilities; + +internal class CharacterAbilityUpgradeSubmarine : CharacterAbility +{ + private readonly UpgradePrefab? upgradePrefab; + private readonly UpgradeCategory? upgradeCategory; + public readonly int level; + + public override bool AllowClientSimulation => true; + + public CharacterAbilityUpgradeSubmarine(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + var prefabIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradePrefab), Identifier.Empty); + var categoryIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradeCategory), Identifier.Empty); + + if (UpgradePrefab.Find(prefabIdentifier) is not { } foundUpgradePrefab) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityUpgradeSubmarine)} - {nameof(upgradePrefab)} not found.", + contentPackage: abilityElement.ContentPackage); + } + else + { + upgradePrefab = foundUpgradePrefab; + } + + if (UpgradeCategory.Find(categoryIdentifier) is not { } foundUpgradeCategory) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityUpgradeSubmarine)} - {nameof(upgradeCategory)} not found.", + contentPackage: abilityElement.ContentPackage); + } + else + { + upgradeCategory = foundUpgradeCategory; + } + + level = abilityElement.GetAttributeInt(nameof(level), 1); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffectSpecific(); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(); + } + + private void ApplyEffectSpecific() + { + if (upgradePrefab == null || upgradeCategory == null) { return; } + if (GameMain.GameSession?.Campaign?.UpgradeManager is not { } upgradeManager) { return; } + + upgradeManager.AddUpgradeExternally(upgradePrefab, upgradeCategory, level); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityWarStories.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityWarStories.cs new file mode 100644 index 000000000..7348428ae --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityWarStories.cs @@ -0,0 +1,63 @@ +namespace Barotrauma.Abilities; + +/// +/// Hardcoded ability for the "War Stories" talent. +/// Spawns an item and sets the health multiplier to the target stat value. +/// +/// The item spawned should have a default health of 1 because we set the multiplier. +/// This is because we already had existing Item.HealthMultiplier that gets synced and +/// everything but not one for setting the max health directly to some value and I didn't +/// want to add a new one just for this. +/// +internal class CharacterAbilityWarStories : CharacterAbility +{ + private readonly Identifier targetStat; + private readonly float minCondition; + + private readonly ItemPrefab prefab; + + public CharacterAbilityWarStories(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + targetStat = abilityElement.GetAttributeIdentifier("target", Identifier.Empty); + minCondition = abilityElement.GetAttributeFloat("mincondition", 1); + + if (targetStat.IsEmpty) + { + DebugConsole.ThrowError($"{nameof(CharacterAbilityWarStories)}: target stat is not defined", contentPackage: abilityElement.ContentPackage); + } + + Identifier spawnedItem = abilityElement.GetAttributeIdentifier("item", Identifier.Empty); + if (!ItemPrefab.Prefabs.TryGet(spawnedItem, out prefab)) + { + DebugConsole.ThrowError($"{nameof(CharacterAbilityWarStories)}: spawned item \"{spawnedItem}\" could not be found.", contentPackage: abilityElement.ContentPackage); + } + } + + protected override void ApplyEffect() + { + if (prefab is null || Character is null) { return; } + + float condition = Character.Info?.GetSavedStatValue(StatTypes.None, targetStat) ?? 0; + if (condition < minCondition) { return; } + + if (GameMain.GameSession?.RoundEnding ?? true) + { + Item item = new(prefab, Character.WorldPosition, Character.Submarine) + { + Condition = condition, + HealthMultiplier = condition + }; + Character.Inventory.TryPutItem(item, Character, item.AllowedSlots); + } + else + { + Entity.Spawner?.AddItemToSpawnQueue(prefab, Character.Inventory, condition: condition, onSpawned: item => + { + item.HealthMultiplier = condition; + }); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + => ApplyEffect(); +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 4d177dd69..aa8b70120 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -140,7 +140,7 @@ namespace Barotrauma.Abilities string type = conditionElement.Name.ToString().ToLowerInvariant(); try { - conditionType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); + conditionType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true); if (conditionType == null) { if (errorMessages) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index f640dee46..736421c36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -22,6 +22,12 @@ namespace Barotrauma public readonly Sprite Icon; + /// + /// When set to a value the talent tooltip will display a text showing the current value of the stat and the max value. + /// For example "Progress: 37/100". + /// + public readonly Option<(Identifier PermanentStatIdentifier, int Max)> TrackedStat; + #if CLIENT public readonly Option ColorOverride; #endif @@ -44,6 +50,12 @@ namespace Barotrauma AbilityEffectsStackWithSameTalent = element.GetAttributeBool("abilityeffectsstackwithsametalent", true); + var trackedStat = element.GetAttributeIdentifier("trackedstat", Identifier.Empty); + var trackedMax = element.GetAttributeInt("trackedmax", 100); + TrackedStat = !trackedStat.IsEmpty + ? Option.Some((trackedStat, trackedMax)) + : Option.None; + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); if (!nameIdentifier.IsEmpty) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 04ac83453..bb06f8717 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -82,18 +82,17 @@ namespace Barotrauma TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); if (subTree is null) { return TalentStages.Invalid; } - if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents)) - { - return TalentStages.Locked; - } - TalentOption targetTalentOption = subTree.TalentOptionStages[index]; - if (targetTalentOption.HasEnoughTalents(character.Info)) { return TalentStages.Unlocked; } + if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents)) + { + return TalentStages.Locked; + } + if (targetTalentOption.HasSelectedTalent(selectedTalents)) { return TalentStages.Highlighted; diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxLabelNode.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxLabelNode.cs new file mode 100644 index 000000000..058018ceb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxLabelNode.cs @@ -0,0 +1,83 @@ +#nullable enable + +using System.Xml.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal sealed partial class CircuitBoxLabelNode : CircuitBoxNode, ICircuitBoxIdentifiable + { + public Color Color; + public ushort ID { get; } + + public override bool IsResizable => true; + + public static NetLimitedString DefaultHeaderText => new("label"); + + public NetLimitedString BodyText = NetLimitedString.Empty; + public NetLimitedString HeaderText = DefaultHeaderText; + + public static Vector2 MinSize = new(128, 8); + + public CircuitBoxLabelNode(ushort id, Color color, Vector2 pos, CircuitBox circuitBox) : base(circuitBox) + { + Size = new Vector2(256); + Position = pos; + ID = id; + Color = color; + UpdatePositions(); +#if CLIENT + bodyLabel = new GUITextBlock(new RectTransform(Point.Zero), text: string.Empty, font: GUIStyle.Font, textAlignment: Alignment.TopLeft, wrap: true); + headerLabel = new CircuitBoxLabel(HeaderText.Value, GUIStyle.LargeFont); + UpdateDrawRects(); + UpdateTextSizes(DrawRect); +#endif + } + + public void EditText(NetLimitedString header, NetLimitedString body) + { + HeaderText = header; + BodyText = body; +#if CLIENT + UpdateTextSizes(DrawRect); +#endif + } + + public XElement Save() + { + var element = new XElement("Label", + new XAttribute("id", ID), + new XAttribute("color", Color.ToStringHex()), + new XAttribute("position", XMLExtensions.Vector2ToString(Position)), + new XAttribute("size", XMLExtensions.Vector2ToString(Size)), + new XAttribute("header", HeaderText), + new XAttribute("body", BodyText)); + return element; + } + + public static CircuitBoxLabelNode LoadFromXML(ContentXElement element, CircuitBox circuitBox) + { + ushort id = element.GetAttributeUInt16("id", ICircuitBoxIdentifiable.NullComponentID); + Vector2 position = element.GetAttributeVector2("position", Vector2.Zero); + Vector2 size = element.GetAttributeVector2("size", Vector2.Zero); + Color color = element.GetAttributeColor("color", Color.White); + string header = element.GetAttributeString("header", string.Empty); + string body = element.GetAttributeString("body", string.Empty); + + var labelNode = new CircuitBoxLabelNode(id, color, position, circuitBox) + { + Size = size, + HeaderText = new NetLimitedString(header), + BodyText = new NetLimitedString(body) + }; + // proc a edit to force the sizes to be updated + labelNode.EditText(new NetLimitedString(header), new NetLimitedString(body)); + labelNode.UpdatePositions(); +#if CLIENT + labelNode.UpdateTextSizes(labelNode.Rect); +#endif + return labelNode; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs index cab96ebee..2d7ad4f36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs @@ -8,6 +8,19 @@ using Microsoft.Xna.Framework; namespace Barotrauma { + [Flags] + internal enum CircuitBoxResizeDirection + { + None = 0, + Down = 1, + Right = 2, + Left = 4 + } + + // TODO this needs to be refactored at some point for reasons: + // 1. We need to send 4 different ImmutableArray for some network packets + // 2. We have 3 identical remove events that are identical in signature + // 3. We have 3 different events for selecting. nodes, wires, and server broadcast public enum CircuitBoxOpcode { Error, @@ -20,6 +33,10 @@ namespace Barotrauma SelectWires, UpdateSelection, DeleteComponent, + RenameLabel, + AddLabel, + RemoveLabel, + ResizeLabel, ServerInitialize } @@ -88,6 +105,18 @@ namespace Barotrauma => $"{{Name: {SignalConnection}, ID: {(TargetId.TryUnwrap(out var value) ? value.ToString() : "N/A")}}}"; } + [NetworkSerialize] + internal readonly record struct CircuitBoxAddLabelEvent(Vector2 Position, Color Color, NetLimitedString Header, NetLimitedString Body) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxServerAddLabelEvent(ushort ID, Vector2 Position, Vector2 Size, Color Color, NetLimitedString Header, NetLimitedString Body) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxResizeLabelEvent(ushort ID, Vector2 Position, Vector2 Size) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxRemoveLabelEvent(ImmutableArray TargetIDs) : INetSerializableStruct; + [NetworkSerialize] internal readonly record struct CircuitBoxAddComponentEvent(UInt32 PrefabIdentifier, Vector2 Position) : INetSerializableStruct; @@ -98,13 +127,13 @@ namespace Barotrauma internal readonly record struct CircuitBoxRemoveComponentEvent(ImmutableArray TargetIDs) : INetSerializableStruct; [NetworkSerialize] - internal readonly record struct CircuitBoxMoveComponentEvent(ImmutableArray TargetIDs, ImmutableArray IOs, Vector2 MoveAmount) : INetSerializableStruct; + internal readonly record struct CircuitBoxMoveComponentEvent(ImmutableArray TargetIDs, ImmutableArray IOs, ImmutableArray LabelIDs, Vector2 MoveAmount) : INetSerializableStruct; [NetworkSerialize] - internal readonly record struct CircuitBoxSelectNodesEvent(ImmutableArray TargetIDs, ImmutableArray IOs, bool Overwrite, ushort CharacterID) : INetSerializableStruct; + internal readonly record struct CircuitBoxSelectNodesEvent(ImmutableArray TargetIDs, ImmutableArray IOs, ImmutableArray LabelIDs, bool Overwrite, ushort CharacterID) : INetSerializableStruct; [NetworkSerialize] - internal readonly record struct CircuitBoxServerUpdateSelection(ImmutableArray ComponentIds, ImmutableArray WireIds, ImmutableArray InputOutputs) : INetSerializableStruct; + internal readonly record struct CircuitBoxServerUpdateSelection(ImmutableArray ComponentIds, ImmutableArray WireIds, ImmutableArray InputOutputs, ImmutableArray LabelIds) : INetSerializableStruct; [NetworkSerialize] internal readonly record struct CircuitBoxIdSelectionPair(ushort ID, Option SelectedBy) : INetSerializableStruct; @@ -124,6 +153,9 @@ namespace Barotrauma [NetworkSerialize] internal readonly record struct CircuitBoxRemoveWireEvent(ImmutableArray TargetIDs) : INetSerializableStruct; + [NetworkSerialize] + internal readonly record struct CircuitBoxRenameLabelEvent(ushort LabelId, Color Color, NetLimitedString NewHeader, NetLimitedString NewBody) : INetSerializableStruct; + [NetworkSerialize] internal readonly record struct CircuitBoxErrorEvent(string Message) : INetSerializableStruct; @@ -131,6 +163,7 @@ namespace Barotrauma internal readonly record struct CircuitBoxInitializeStateFromServerEvent( ImmutableArray Components, ImmutableArray Wires, + ImmutableArray Labels, Vector2 InputPos, Vector2 OutputPos) : INetSerializableStruct; @@ -157,6 +190,14 @@ namespace Barotrauma => CircuitBoxOpcode.RemoveWire, CircuitBoxInitializeStateFromServerEvent => CircuitBoxOpcode.ServerInitialize, + CircuitBoxRenameLabelEvent + => CircuitBoxOpcode.RenameLabel, + (CircuitBoxAddLabelEvent or CircuitBoxServerAddLabelEvent) + => CircuitBoxOpcode.AddLabel, + CircuitBoxRemoveLabelEvent + => CircuitBoxOpcode.RemoveLabel, + CircuitBoxResizeLabelEvent + => CircuitBoxOpcode.ResizeLabel, _ => throw new ArgumentOutOfRangeException(nameof(Data)) }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs index 0dbb018f8..3608618ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs @@ -14,6 +14,8 @@ namespace Barotrauma public RectangleF Rect; private Vector2 position; + public virtual bool IsResizable => false; + public Vector2 Position { get => position; @@ -22,12 +24,12 @@ namespace Barotrauma const float clampSize = CircuitBoxSizes.PlayableAreaSize / 2f; position = new Vector2(Math.Clamp(value.X, -clampSize, clampSize), - Math.Clamp(value.Y, -clampSize, clampSize)); + Math.Clamp(value.Y, -clampSize, clampSize)); UpdatePositions(); } } - public ImmutableArray Connectors; + public ImmutableArray Connectors = ImmutableArray.Empty; public static float Opacity = 0.8f; @@ -38,6 +40,47 @@ namespace Barotrauma CircuitBox = circuitBox; } + public (Vector2 Size, Vector2 Pos) ResizeBy(CircuitBoxResizeDirection directions, Vector2 amount) + { + Vector2 newSize = Size; + Vector2 newPos = Position; + amount.Y = -amount.Y; + + if (directions.HasFlag(CircuitBoxResizeDirection.Down)) + { + newSize.Y += amount.Y; + newSize.Y = Math.Max(newSize.Y, CircuitBoxLabelNode.MinSize.Y); + newPos = new Vector2(newPos.X, newPos.Y - (newSize.Y - Size.Y) / 2f); + } + + if (directions.HasFlag(CircuitBoxResizeDirection.Right)) + { + newSize.X += amount.X; + newSize.X = Math.Max(newSize.X, CircuitBoxLabelNode.MinSize.X); + newPos = new Vector2(newPos.X + (newSize.X - Size.X) / 2f, newPos.Y); + } + + if (directions.HasFlag(CircuitBoxResizeDirection.Left)) + { + newSize.X -= amount.X; + newSize.X = Math.Max(newSize.X, CircuitBoxLabelNode.MinSize.X); + newPos = new Vector2(newPos.X + (Size.X - newSize.X) / 2f, newPos.Y); + } + + return (newSize, newPos); + } + + public void ApplyResize(Vector2 newSize, Vector2 newPos) + { + if (!MathUtils.IsValid(newSize)) { return; } + Size = newSize; + Position = newPos; + UpdatePositions(); +#if CLIENT + OnResized(DrawRect); +#endif + } + public static Vector2 CalculateSize(IReadOnlyList conns) { Vector2 leftSize = Vector2.Zero, diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs index 5f7f211b8..b21649561 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs @@ -11,6 +11,7 @@ namespace Barotrauma public const int WireWidth = 10; public const int WireKnobLength = 16; public const int NodeHeaderTextPadding = 8; + public const int NodeBodyTextPadding = 8; public const float PlayableAreaSize = 8192f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs index 843b873fb..ba2e8e480 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs @@ -148,8 +148,14 @@ namespace Barotrauma } case CircuitBoxNodeConnection node when two is CircuitBoxInputConnection input: { - if (node.ExternallyConnectedFrom.Contains(input)) { break; } - node.ExternallyConnectedFrom.Add(input); + if (!node.Connection.CircuitBoxConnections.Contains(input)) + { + node.Connection.CircuitBoxConnections.Add(input); + } + if (!node.ExternallyConnectedFrom.Contains(input)) + { + node.ExternallyConnectedFrom.Add(input); + } break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs index d8269dbd0..96dc4ca72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -14,6 +14,7 @@ namespace Barotrauma public override void LoadFile() { + ClearCaches(); XDocument doc = XMLExtensions.TryLoadXml(Path); if (doc == null) { @@ -36,6 +37,15 @@ namespace Barotrauma public override void UnloadFile() { CharacterPrefab.Prefabs.RemoveByFile(this); + ClearCaches(); + } + + private static void ClearCaches() + { + // Clear the caches to get rid of any overrides. + // Variants should have their own params instances, but let's keep it simple and play safe. + RagdollParams.ClearCache(); + AnimationParams.ClearCache(); } public override void Sort() @@ -60,11 +70,11 @@ namespace Barotrauma { if (humanoid) { - ragdollParams = RagdollParams.GetRagdollParams(speciesName); + ragdollParams = RagdollParams.GetDefaultRagdollParams(speciesName, mainElement, ContentPackage); } else { - ragdollParams = RagdollParams.GetRagdollParams(speciesName); + ragdollParams = RagdollParams.GetDefaultRagdollParams(speciesName, mainElement, ContentPackage); } } catch (Exception e) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContainerTagFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContainerTagFile.cs new file mode 100644 index 000000000..b121a2147 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContainerTagFile.cs @@ -0,0 +1,16 @@ +#nullable enable + +namespace Barotrauma +{ + internal sealed class ContainerTagFile : GenericPrefabFile + { + public ContainerTagFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "containertag"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "containertags"; + protected override PrefabCollection Prefabs => ContainerTagPrefab.Prefabs; + + protected override ContainerTagPrefab CreatePrefab(ContentXElement element) + => new(element, this); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs index ff4493c9c..978429cbb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs @@ -1,4 +1,4 @@ -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -37,7 +37,8 @@ namespace Barotrauma if (newList.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newList); } } TextManager.IncrementLanguageVersion(); - if (!TextManager.TextPacks.ContainsKey(GameSettings.CurrentConfig.Language)) + if (!TextManager.TextPacks.ContainsKey(GameSettings.CurrentConfig.Language) && + GameSettings.CurrentConfig.Language != TextManager.DefaultLanguage) { DebugConsole.AddWarning($"The language {GameSettings.CurrentConfig.Language} is no longer available. Switching to {TextManager.DefaultLanguage}..."); var config = GameSettings.CurrentConfig; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index b853d9705..29c26ee54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -65,8 +65,13 @@ namespace Barotrauma public static void ReloadCore() { if (Core == null) { return; } - Core.UnloadContent(); - Core.LoadContent(); + ReloadPackage(Core); + } + + public static void ReloadPackage(ContentPackage p) + { + p.UnloadContent(); + p.LoadContent(); SortContent(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index 149395221..0d9a4a112 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -22,7 +22,7 @@ namespace Barotrauma private string? cachedValue; private string? cachedFullPath; - + public string Value { get @@ -107,11 +107,6 @@ namespace Barotrauma prevCreatedRaw = newRaw; return newRaw; } - - public static ContentPath FromEvaluated(ContentPackage? contentPackage, string? evaluatedValue) - { - throw new NotImplementedException(); - } private static bool StringEquality(string? a, string? b) { @@ -119,8 +114,8 @@ namespace Barotrauma { return a.IsNullOrEmpty() == b.IsNullOrEmpty(); } - return string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""), - Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""), StringComparison.OrdinalIgnoreCase); + return string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(correctFilenameCase: false) ?? ""), + Path.GetFullPath(b.CleanUpPathCrossPlatform(correctFilenameCase: false) ?? ""), StringComparison.OrdinalIgnoreCase); } public static bool operator==(ContentPath a, ContentPath b) @@ -145,9 +140,9 @@ namespace Barotrauma public override bool Equals(object? obj) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; + if (ReferenceEquals(null, obj)) { return false; } + if (ReferenceEquals(this, obj)) { return true; } + if (obj.GetType() != this.GetType()) { return false; } return Equals((ContentPath)obj); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index c30424c23..316408b52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -739,6 +739,35 @@ namespace Barotrauma }; }, isCheat: true)); + + commands.Add(new Command("listsuitabletreatments", "listsuitabletreatments [character name]: List which items are the most suitable for treating the specified character. Useful for debugging medic AI.", (string[] args) => + { + Character character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); + if (character != null) + { + Dictionary treatments = new Dictionary(); + character.CharacterHealth.GetSuitableTreatments(treatments, user: null); + foreach (var treatment in treatments.OrderByDescending(t => t.Value)) + { + Color color = Color.White; +#if CLIENT + color = ToolBox.GradientLerp( + MathUtils.InverseLerp(-1000, 1000, treatment.Value), + Color.Red, Color.Yellow, Color.White, Color.LightGreen); +#endif + NewMessage((int)treatment.Value + ": " + treatment.Key, color); + + } + } + }, + () => + { + return new string[][] + { + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() + }; + }, isCheat: true)); + commands.Add(new Command("revive", "revive [character name]: Bring the specified character back from the dead. If the name parameter is omitted, the controlled character will be revived.", (string[] args) => { Character revivedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); @@ -827,7 +856,7 @@ namespace Barotrauma } else if (eventPrefab != null) { - var newEvent = eventPrefab.CreateInstance(); + var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); if (newEvent == null) { NewMessage($"Could not initialize event {args[0]} because level did not meet requirements"); @@ -2296,6 +2325,7 @@ namespace Barotrauma #endif } spawnedCharacter.GiveJobItems(spawnPoint); + spawnedCharacter.GiveIdCardTags(spawnPoint); spawnedCharacter.Info.StartItemsGiven = true; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 9fcaed105..2f71320e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -152,6 +152,7 @@ namespace Barotrauma OnAllyGainMissionExperience, OnGainMissionExperience, OnGainMissionMoney, + OnCrewGainMissionReputation, OnLocationDiscovered, OnItemDeconstructed, OnItemDeconstructedByAlly, @@ -329,6 +330,11 @@ namespace Barotrauma /// Increases the repair speed of the character when repairing mechanical items by a percentage. /// MechanicalRepairSpeed, + + /// + /// Increases the repair speed of the character when repairing electrical items by a percentage. + /// + ElectricalRepairSpeed, /// /// Increase deconstruction speed of deconstructor operated by the character by a percentage. @@ -572,7 +578,12 @@ namespace Barotrauma /// /// 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 + SightRangeMultiplier, + + /// + /// Reduces the dual wielding penalty by a percentage. + /// + DualWieldingPenaltyReduction } internal enum ItemTalentStats @@ -672,18 +683,26 @@ namespace Barotrauma Both = Bot | Player } - public enum StartingBalanceAmount + public enum StartingBalanceAmountOption { Low, Medium, High, } - public enum GameDifficulty + public enum PatdownProbabilityOption { - Easy, + Off, + Low, Medium, - Hard, + High, + } + + public enum WorldHostilityOption + { + Low, + Medium, + High, Hellish } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index da3967687..e178bd9f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -29,8 +29,8 @@ namespace Barotrauma return $"ArtifactEvent ({(itemPrefab == null ? "null" : itemPrefab.Name)})"; } - public ArtifactEvent(EventPrefab prefab) - : base(prefab) + public ArtifactEvent(EventPrefab prefab, int seed) + : base(prefab, seed) { if (prefab.ConfigElement.GetAttribute("itemname") != null) { @@ -55,9 +55,8 @@ namespace Barotrauma } } - public override void Init(EventSet parentSet) + protected override void InitEventSpecific(EventSet parentSet) { - base.Init(parentSet); spawnPos = Level.Loaded.GetRandomItemPos( (Rand.Value(Rand.RandSync.ServerAndClient) < 0.5f) ? Level.PositionType.MainPath | Level.PositionType.SidePath : diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 0e4358efb..08abda9ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -9,7 +9,7 @@ namespace Barotrauma public event Action Finished; protected bool isFinished; - public int RandomSeed; + public readonly int RandomSeed; protected readonly EventPrefab prefab; @@ -17,6 +17,8 @@ namespace Barotrauma public EventSet ParentSet { get; private set; } + public bool Initialized { get; private set; } + public Func SpawnPosFilter; public bool IsFinished @@ -37,8 +39,9 @@ namespace Barotrauma } } - public Event(EventPrefab prefab) + public Event(EventPrefab prefab, int seed) { + RandomSeed = seed; this.prefab = prefab ?? throw new ArgumentNullException(nameof(prefab)); } @@ -47,9 +50,15 @@ namespace Barotrauma yield break; } - public virtual void Init(EventSet parentSet = null) + public void Init(EventSet parentSet = null) { + Initialized = true; ParentSet = parentSet; + InitEventSpecific(parentSet); + } + + protected virtual void InitEventSpecific(EventSet parentSet = null) + { } public virtual string GetDebugInfo() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs index a5b62d869..58e25ea73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs @@ -1,22 +1,35 @@ -using System.Linq; +using System.Linq; namespace Barotrauma { + /// + /// Gives an affliction to a specific character. + /// class AfflictionAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the affliction.")] public Identifier Affliction { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Strength of the affliction.")] public float Strength { get; set; } - [Serialize(LimbType.None, IsPropertySaveable.Yes)] + [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "Type of the limb(s) to apply the affliction on. Only valid if the affliction is limb-specific.")] public LimbType LimbType { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to apply the affliction on.")] public Identifier TargetTag { get; set; } - public AfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the strength be multiplied by the maximum vitality of the target?")] + public bool MultiplyByMaxVitality { get; set; } + + public AfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (Affliction.IsEmpty) + { + DebugConsole.ThrowError($"Error in {nameof(AfflictionAction)}: affliction not defined (use the attribute \"{nameof(Affliction)}\").", + contentPackage: element.ContentPackage); + } + } private bool isFinished = false; @@ -40,27 +53,32 @@ namespace Barotrauma { if (target != null && target is Character character) { + float strength = Strength; + if (MultiplyByMaxVitality) + { + strength *= character.MaxVitality; + } if (LimbType != LimbType.None) { var limb = character.AnimController.GetLimb(LimbType); - if (Strength > 0.0f) + if (strength > 0.0f) { - character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true); + character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(strength), ignoreUnkillability: true); } - else if (Strength < 0.0f) + else if (strength < 0.0f) { - character.CharacterHealth.ReduceAfflictionOnLimb(limb, Affliction, -Strength); + character.CharacterHealth.ReduceAfflictionOnLimb(limb, Affliction, -strength); } } else { - if (Strength > 0.0f) + if (strength > 0.0f) { - character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true); + character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), ignoreUnkillability: true); } - else if (Strength < 0.0f) + else if (strength < 0.0f) { - character.CharacterHealth.ReduceAfflictionOnAllLimbs(Affliction, -Strength); + character.CharacterHealth.ReduceAfflictionOnAllLimbs(Affliction, -strength); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs index fb75303ee..5977105bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs @@ -4,24 +4,27 @@ using System.Linq; namespace Barotrauma { + /// + /// Check whether a target has a specific affliction. + /// internal class CheckAfflictionAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the affliction.")] public Identifier Identifier { get; set; } = Identifier.Empty; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")] public Identifier TargetTag { get; set; } = Identifier.Empty; - [Serialize("", IsPropertySaveable.Yes, description: "Tag referring to the character who caused the affliction.")] + [Serialize("", IsPropertySaveable.Yes, description: "Tag referring to the character who caused the affliction. Can be used to require the affliction to be caused by a specific character.")] public Identifier SourceCharacter { get; set; } = Identifier.Empty; - [Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type")] + [Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type.")] public LimbType TargetLimb { get; set; } - [Serialize(true, IsPropertySaveable.Yes, "When set to false when TargetLimb is not specified prevent checking limb-specific afflictions")] + [Serialize(true, IsPropertySaveable.Yes, "When set to false, limb-specific afflictions are ignored when not checking a specific limb.")] public bool AllowLimbAfflictions { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes, "Minimum strength of the affliction")] + [Serialize(0.0f, IsPropertySaveable.Yes, "Minimum strength of the affliction.")] public float MinStrength { get; set; } public CheckAfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index 8c11d1829..d3124db34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -6,20 +6,24 @@ using System.Xml.Linq; namespace Barotrauma { + + /// + /// Checks whether an arbitrary condition is met. The conditionals work the same way as they do in StatusEffects. + /// class CheckConditionalAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the target to check.")] public Identifier TargetTag { get; set; } - [Serialize(PropertyConditional.LogicalOperatorType.Or, IsPropertySaveable.Yes)] + [Serialize(PropertyConditional.LogicalOperatorType.Or, IsPropertySaveable.Yes, description: "Do all of the conditions need to be met, or is it enough if at least one is? Only valid if there are multiple conditionals.")] public PropertyConditional.LogicalOperatorType LogicalOperator { get; set; } private ImmutableArray Conditionals { get; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "A tag to apply to the hull the target is currently in when the check succeeds, as well as all the hulls linked to it.")] 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.")] + [Serialize("", IsPropertySaveable.Yes, description: "A tag to apply to the hull the target is currently in when the check succeeds.")] public Identifier ApplyTagToHull { get; set; } public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs index 56f6b82e2..2ec488f30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs @@ -4,21 +4,24 @@ using System.Linq; namespace Barotrauma; +/// +/// Check whether a specific connection of an item is wired to a specific kind of connection. +/// class CheckConnectionAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item to check.")] public Identifier ItemTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The name of the connection to check on the target item.")] public Identifier ConnectionName { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item the connection must be wired to. If omitted, it doesn't matter what the connection is wired to.")] public Identifier ConnectedItemTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The name of the other connection the connection must be wired to. If omitted, it doesn't matter what the connection is wired to.")] public Identifier OtherConnectionName { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Minimum number of matching connections for the check to succeed.")] public int MinAmount { get; set; } public CheckConnectionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -37,7 +40,7 @@ class CheckConnectionAction : BinaryOptionAction if (!IsCorrectConnection(connection, ConnectionName)) { continue; } if (ConnectedItemTag.IsEmpty && OtherConnectionName.IsEmpty) { - amount += connection.Wires.Count(); + amount += connection.Wires.Count; if (amount >= MinAmount) { return true; } continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index f56d50e29..91f328b5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -1,21 +1,24 @@ #nullable enable using System; -using System.Linq; namespace Barotrauma { + + /// + /// Can be used to check arbitrary campaign metadata set using . + /// class CheckDataAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the data to check.")] public Identifier Identifier { get; set; } = Identifier.Empty; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The condition that must be met for the check to succeed. Uses the same formatting as conditionals (for example, \"gt 5.2\", \"true\", \"lt 10\".)")] public string Condition { get; set; } = ""; - [Serialize(false, IsPropertySaveable.Yes, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first")] + [Serialize(false, IsPropertySaveable.Yes, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first. Use this if you know the value is a string.")] public bool ForceString { get; set; } - [Serialize(false, IsPropertySaveable.Yes, "Performs the comparison against a metadata by identifier instead of a constant value")] + [Serialize(false, IsPropertySaveable.Yes, "Performs the comparison against a metadata by identifier instead of a constant value. Meaning that you could for example check whether the value of \"progress_of_some_event\" is larger than \"progress_of_some_other_event\".")] public bool CheckAgainstMetadata { get; set; } protected object? value2; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index 727d627dd..779f9b4f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -6,18 +6,22 @@ using System.Linq; namespace Barotrauma { + /// + /// Can be used to do various kinds of checks on items: whether a specific kind of item exists, + /// if it's in a specific character's inventory or in a container, or whether some conditions are met on the item. + /// class CheckItemAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Either the tag of the item(s) we want to check, or a character/container the items are inside.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The target item must have one of these identifiers.")] public string ItemIdentifiers { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The target item must have at least one of these tags.")] public string ItemTags { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of matching items for the check to succeed.")] public int Amount { get; set; } [Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")] @@ -29,31 +33,27 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the found item(s) when the check succeeds.")] public Identifier ApplyTagToItem { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Does the item need to be equipped for the check to succeed?")] public bool RequireEquipped { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "If enabled, the doesn't need to be directly inside the container/character we're checking, but can be nested inside multiple containers (e.g. in a toolbelt in a character's inventory).")] public bool Recursive { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "Can be used to require the item to be in a specific ItemContainer of the target container. For example, the input slots of a fabricator (the first ItemContainer of the fabricator, with an index of 0).")] public int ItemContainerIndex { get; set; } private readonly bool checkPercentage; private float requiredConditionalMatchPercentage; - [Serialize(100.0f, IsPropertySaveable.Yes)] - - /// - /// What percentage of targets do the conditionals need to match for the check to succeed? - /// + [Serialize(100.0f, IsPropertySaveable.Yes, description: "What percentage of targets do the conditionals need to match for the check to succeed?")] public float RequiredConditionalMatchPercentage { get { return requiredConditionalMatchPercentage; } set { requiredConditionalMatchPercentage = MathHelper.Clamp(value, 0.0f, 100.0f); } } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "When enabled, the number of matching items is compared to the number of matching items there were at the start of the round. Only valid if RequiredConditionalMatchPercentage is set.")] public bool CompareToInitialAmount { get; set; } private readonly IReadOnlyList conditionals; @@ -94,12 +94,12 @@ namespace Barotrauma private bool EnoughTargets(int totalTargets, int targetsWithConditionalsMatched) { - if (CompareToInitialAmount) - { - totalTargets = ParentEvent.GetInitialTargetCount(TargetTag); - } if (checkPercentage) { + if (CompareToInitialAmount) + { + totalTargets = ParentEvent.GetInitialTargetCount(TargetTag); + } return MathUtils.Percentage(targetsWithConditionalsMatched, totalTargets) >= RequiredConditionalMatchPercentage; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs index 530a63429..96df67378 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs @@ -3,6 +3,9 @@ using System.Linq; namespace Barotrauma; +/// +/// Check whether a specific mission is currently active, selected for the next round or available. +/// class CheckMissionAction : BinaryOptionAction { public enum MissionType @@ -12,16 +15,16 @@ class CheckMissionAction : BinaryOptionAction Available } - [Serialize(MissionType.Current, IsPropertySaveable.Yes)] + [Serialize(MissionType.Current, IsPropertySaveable.Yes, description: "Does the mission need to be currently active, selected for the next round or available.")] public MissionType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the mission.")] public Identifier MissionIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the mission. Ignored if MissionIdentifier is set.")] public Identifier MissionTag { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Minimum number of matching missions for the check to succeed.")] public int MissionCount { get; set; } public CheckMissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs index ac6eef6b2..2a3b45db1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs @@ -1,16 +1,18 @@ +using Barotrauma.Networking; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Networking; namespace Barotrauma { + /// + /// Check whether the crew or a specific player has enough money. + /// class CheckMoneyAction : BinaryOptionAction { - [Serialize(0, IsPropertySaveable.Yes)] + [Serialize(0, IsPropertySaveable.Yes, description: "Minimum amount of money the crew or the player must have.")] public int Amount { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the player to check. If omitted, the crew's shared wallet is checked instead.")] public Identifier TargetTag { get; set; } public CheckMoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs index 15e5e90a9..cc0023ad8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs @@ -1,5 +1,8 @@ namespace Barotrauma; +/// +/// Checks the state of an Objective created using . +/// partial class CheckObjectiveAction : BinaryOptionAction { public CheckObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs index 39b427ba4..9cfd45006 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs @@ -2,6 +2,9 @@ using Barotrauma.Extensions; namespace Barotrauma { + /// + /// Check whether a specific character has been given a specific order. + /// class CheckOrderAction : BinaryOptionAction { public enum OrderPriority @@ -10,19 +13,19 @@ namespace Barotrauma Any } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the order the target character must have.")] public Identifier OrderIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The option that must be selected for the order. If the order has multiple options (such as turning on or turning off a reactor).")] public Identifier OrderOption { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity the order must be targeting. Only valid for orders that can target a specific entity (such as orders to operate a specific turret).")] public Identifier OrderTargetTag { get; set; } - [Serialize(OrderPriority.Any, IsPropertySaveable.Yes)] + [Serialize(OrderPriority.Any, IsPropertySaveable.Yes, description: "Does the order need to have top priority, or is any priority fine?")] public OrderPriority Priority { get; set; } public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs index b0ac35616..17ce5fa12 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs @@ -3,6 +3,9 @@ using System.Linq; namespace Barotrauma; +/// +/// Check whether specific kinds of items have been purchased or sold during the round. +/// class CheckPurchasedItemsAction : BinaryOptionAction { public enum TransactionType @@ -11,16 +14,16 @@ class CheckPurchasedItemsAction : BinaryOptionAction Sold } - [Serialize(TransactionType.Purchased, IsPropertySaveable.Yes)] + [Serialize(TransactionType.Purchased, IsPropertySaveable.Yes, description: "Do the items need to have been purchased or sold?")] public TransactionType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item that must have been purchased or sold.")] public Identifier ItemIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item that must have been purchased or sold.")] public Identifier ItemTag { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Minimum number of matching items that must have been purchased or sold.")] public int MinCount { get; set; } public CheckPurchasedItemsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs index 478fb872d..dc447ff92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs @@ -3,9 +3,12 @@ using System.Diagnostics; namespace Barotrauma { + /// + /// Check whether the reputation of the crew for a specific faction meets some criteria (e.g. equal to, larger than or less than some value). + /// class CheckReputationAction : CheckDataAction { - [Serialize(ReputationAction.ReputationType.None, IsPropertySaveable.Yes)] + [Serialize(ReputationAction.ReputationType.None, IsPropertySaveable.Yes, description: "Should the action check the reputation for a given faction, or whichever faction owns the current location.")] public ReputationAction.ReputationType TargetType { get; set; } public CheckReputationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs index 0e673e047..ca3b4795c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs @@ -3,17 +3,20 @@ using System.Collections.Generic; namespace Barotrauma { + /// + /// Check whether a specific character has selected a specific kind of item. + /// class CheckSelectedAction : BinaryOptionAction { public enum SelectedItemType { Primary, Secondary, Any }; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")] public Identifier CharacterTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If specified, only items that have been given this tag using TagAction are considered valid.")] public Identifier TargetTag { get; set; } - [Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)] + [Serialize(SelectedItemType.Any, IsPropertySaveable.Yes, description: "How does the item need to be selected? Primary item (i.e. any device you're interacting with), secondary item (such as ladders or chairs which allow interacting with a primary item at the same time), or either?")] public SelectedItemType ItemType { get; set; } public CheckSelectedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs index b974c6f83..667cf2d76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs @@ -2,12 +2,15 @@ namespace Barotrauma { + /// + /// Check whether a specific character has a specific talent. + /// internal sealed class CheckTalentAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the talent to check for.")] public Identifier TalentIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")] public Identifier TargetTag { get; set; } public CheckTalentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs index ab2b9fde6..e4e11c9f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs @@ -2,9 +2,12 @@ namespace Barotrauma { + /// + /// Check the state of the traitor event the action is defined in. Only valid for traitor events. + /// class CheckTraitorEventStateAction : BinaryOptionAction { - [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)] + [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes, description: "What does the state of the event need to be for the check to succeed?")] public TraitorEvent.State State { get; set; } private readonly TraitorEvent? traitorEvent; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs index 9ab84cd3e..203503e02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs @@ -9,7 +9,7 @@ namespace Barotrauma /// class CheckTraitorVoteAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")] public Identifier Target { get; set; } public CheckTraitorVoteAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs index 119fa0649..13a217c0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs @@ -4,6 +4,9 @@ using System.Linq; namespace Barotrauma { + /// + /// Check whether a specific entity is visible from the perspective of another entity. + /// class CheckVisibilityAction : BinaryOptionAction { [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs index 4de2f7c4f..ecba9a6b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs @@ -1,10 +1,12 @@ -using System.Xml.Linq; - namespace Barotrauma { + + /// + /// Clears the specific tag from the event (i.e. untagging all the entities that have been previously given the tag). + /// class ClearTagAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The tag to clear.")] public Identifier Tag { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs index 9f1242e13..a9cc76a69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs @@ -1,31 +1,33 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Makes an NPC switch to a combat state (with options for different kinds of behaviors, such as offensive, arresting or retreating). + /// class CombatAction : EventAction { - [Serialize(AIObjectiveCombat.CombatMode.Offensive, IsPropertySaveable.Yes)] + [Serialize(AIObjectiveCombat.CombatMode.Offensive, IsPropertySaveable.Yes, description: $"What kind of combat mode should the NPC switch to (Defensive, Offensive, Arrest, Retreat, None)?")] public AIObjectiveCombat.CombatMode CombatMode { get; set; } - [Serialize(false, IsPropertySaveable.Yes, description: "Did this NPC start the fight (as an aggressor)?")] + [Serialize(false, IsPropertySaveable.Yes, description: "Did this NPC start the fight (as an aggressor)? Attacking instigators doesn't reduce reputation or trigger outpost security.")] public bool IsInstigator { get; set; } - [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes)] + [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes, description: "How do guards react to this character attacking others?")] public AIObjectiveCombat.CombatMode GuardReaction { get; set; } - [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes)] + [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes, description: "How do other NPCs react to this character attacking others?")] public AIObjectiveCombat.CombatMode WitnessReaction { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The tag of the NPC to switch to combat mode.")] public Identifier NPCTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character the NPC should attack.")] public Identifier EnemyTag { get; set; } - [Serialize(120.0f, IsPropertySaveable.Yes)] + [Serialize(120.0f, IsPropertySaveable.Yes, description: "How long it takes for the NPC to \"cool down\" (stop attacking).")] public float CoolDown { get; set; } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 7817c012c..93511edb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -8,6 +8,10 @@ using System.Xml.Linq; namespace Barotrauma { + + /// + /// Triggers a "conversation popup" with text and support for different branching options. + /// partial class ConversationAction : EventAction { @@ -26,43 +30,40 @@ namespace Barotrauma /// const float BlockOtherConversationsDuration = 5.0f; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The text to display in the prompt. Can be the text as-is, or a tag referring to a line in a text file.")] public string Text { get; set; } - [Serialize(0, IsPropertySaveable.Yes)] - public int DefaultOption { get; set; } - - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character who's speaking. Makes a speech bubble icon appear above the character to indicate you can speak with them, and stops the character in place when the conversation triggers. Also allows the conversation to be interrupted if the speaker dies or becomes incapacitated mid-conversation.")] public Identifier SpeakerTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the player the conversation is shown to. If empty, the conversation is shown to everyone. If SpeakerTag is defined, the conversation is always only shown to the player who interacts with the speaker.")] public Identifier TargetTag { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, "Should someone interact with the speaker for the conversation to trigger?")] public bool WaitForInteraction { get; set; } - [Serialize("", IsPropertySaveable.Yes, "Tag to assign to whoever invokes the conversation")] + [Serialize("", IsPropertySaveable.Yes, "Tag to assign to whoever invokes the conversation.")] public Identifier InvokerTag { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the screen fade to black when the conversation is active?")] public bool FadeToBlack { get; set; } [Serialize(true, IsPropertySaveable.Yes, "Should the event end if the conversations is interrupted (e.g. if the speaker dies or falls unconscious mid-conversation). Defaults to true.")] public bool EndEventIfInterrupted { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of an event sprite to display in the corner of the conversation prompt.")] public string EventSprite { get; set; } - [Serialize(DialogTypes.Regular, IsPropertySaveable.Yes)] + [Serialize(DialogTypes.Regular, IsPropertySaveable.Yes, description: "Type of the dialog prompt.")] public DialogTypes DialogType { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Does this conversation continue after this ConversationAction? If you have multiple successive ConversationActions, perhaps with some actions happening in between, you can enable this to prevent the dialog prompt from closing between the actions. Not necessary if the ConversationActions are nested inside each other: those are always considered parts of the same conversation, and shown in the same prompt.")] public bool ContinueConversation { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the event will not stop to wait for the conversation to be dismissed.")] public bool ContinueAutomatically { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "If SpeakerTag is defined, the conversation is interrupted by default if the speaker and the target end up too far from each other. This can be used to disable that behavior, keeping the dialog prompt open regardless of the distance.")] public bool IgnoreInterruptDistance { get; set; } public Character Speaker @@ -72,6 +73,7 @@ namespace Barotrauma } private AIObjective prevIdleObjective, prevGotoObjective; + private AIObjective npcWaitObjective; public List Options { get; private set; } @@ -275,6 +277,10 @@ namespace Barotrauma if (!SpeakerTag.IsEmpty) { + if (npcWaitObjective != null) + { + npcWaitObjective.ForceHighestPriority = true; + } if (Speaker != null && !Speaker.Removed && Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } Speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character; if (Speaker == null || Speaker.Removed) @@ -386,11 +392,11 @@ namespace Barotrauma { prevIdleObjective = humanAI.ObjectiveManager.GetObjective(); prevGotoObjective = humanAI.ObjectiveManager.GetObjective(); - humanAI.SetForcedOrder( + npcWaitObjective = humanAI.SetForcedOrder( new Order(OrderPrefab.Prefabs["wait"], Barotrauma.Identifier.Empty, null, orderGiver: null)); - if (targets.Any()) + if (targets.Any() || targetCharacter != null) { - Entity closestTarget = null; + Entity closestTarget = targetCharacter; float closestDist = float.MaxValue; foreach (Entity entity in targets) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs index 70b6f74b9..13d3b6859 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs @@ -5,9 +5,12 @@ using System.Linq; namespace Barotrauma { + /// + /// Check whether there's at least / at most some number of entities matching some specific criteria. + /// class CountTargetsAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entities to check.")] public Identifier TargetTag { get; set; } [Serialize("", IsPropertySaveable.Yes, description: "Optional second tag. Can be used if the target must have two different tags.")] @@ -16,29 +19,19 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")] public Identifier HullTag { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "Minimum number of matching entities for the check to succeed. If omitted or negative, there is no minimum amount.")] public int MinAmount { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of matching entities for the check to succeed. If omitted or negative, there is no maximum amount.")] public int MaxAmount { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of some other entities to compare the number of targets to. E.g. you could compare the number of entities tagged as \"discoveredhull\" to entities tagged as \"anyhull\". The minimum/maximum amount of entities there must be relative to the other entities is configured using MinPercentageRelativeToTarget and MaxPercentageRelativeToTarget.")] public Identifier CompareToTarget { get; set; } - [Serialize(-1.0f, IsPropertySaveable.Yes)] - - /// - /// Minimum amount of targets, as a percentage of the number of entities tagged with CompareToTarget - /// E.g. you could compare the number of entities tagged as "discoveredhull" to entities tagged as "anyhull" to require 50% of hulls to be discovered. - /// + [Serialize(-1.0f, IsPropertySaveable.Yes, description: "Minimum amount of targets, as a percentage of the number of entities tagged with CompareToTarget. E.g. you could compare the number of entities tagged as \"discoveredhull\" to entities tagged as \"anyhull\" to require 50% of hulls to be discovered.")] public float MinPercentageRelativeToTarget { get; set; } - [Serialize(-1.0f, IsPropertySaveable.Yes)] - - /// - /// Maximum amount of targets, as a percentage of the number of entities tagged with CompareToTarget - /// E.g. you could compare the number of entities tagged as "floodedhull" to entities tagged as "anyhull" to require less than 50% of hulls to be flooded. - /// + [Serialize(-1.0f, IsPropertySaveable.Yes, description: "Maximum amount of targets, as a percentage of the number of entities tagged with CompareToTarget. E.g. you could compare the number of entities tagged as \"floodedhull\" to entities tagged as \"anyhull\" to require less than 50% of hulls to be flooded.")] public float MaxPercentageRelativeToTarget { get; set; } private readonly IReadOnlyList conditionals; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs index c6f24003d..5bbd67774 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs @@ -5,15 +5,19 @@ using System.Xml.Linq; namespace Barotrauma { + + /// + /// Adds an entry to the "event log" displayed in the mission tab of the tab menu. + /// partial class EventLogAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the entry. If there's already an entry with the same id, it gets overwritten.")] public Identifier Id { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Text to add to the event log. Can be the text as-is, or a tag referring to a line in a text file.")] public string Text { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) who should see the entry. If empty, the entry is shown to everyone.")] public Identifier TargetTag { get; set; } public bool ShowInServerLog { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs index 2faa398bf..dceb01b73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs @@ -1,40 +1,81 @@ +using System; + namespace Barotrauma { + + /// + /// Displays an objective in the top-right corner of the screen, or modifies an existing objective in some way. + /// partial class EventObjectiveAction : EventAction { - public enum SegmentActionType { Trigger, Add, AddIfNotFound, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove }; + public enum SegmentActionType + { + /// + /// Legacy support. Triggers an info box segment, with optional support for video clips. + /// + [Obsolete] + Trigger, + /// + /// Adds a new objective to the list. + /// + Add, + /// + /// Adds a new objective to the list if there are no existing objectives with the same identifier. + /// + AddIfNotFound, + /// + /// Marks the objective as completed. + /// + Complete, + /// + /// Marks the objective as completed and removes it from the list. + /// + CompleteAndRemove, + /// + /// Removes the objective from the list. + /// + Remove, + /// + /// Marks the objective as failed. + /// + Fail, + /// + /// Marks the objective as failed and removes it from the list. + /// + FailAndRemove + }; - [Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)] + [Serialize(SegmentActionType.Add, IsPropertySaveable.Yes, description: "Should the action add a new objective, or do something to an existing objective?")] public SegmentActionType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Arbitrary identifier given to the objective. Can be used to complete/remove/fail the objective later. Also used to fetch the text from the text files.")] public Identifier Identifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Obsolete, Serialize("", IsPropertySaveable.Yes, description: "Legacy support. Tag of the text to display as an objective in info box segments.")] public Identifier ObjectiveTag { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Obsolete, Serialize(true, IsPropertySaveable.Yes, description: "Legacy support. Is this objective possible to complete if it's used in an info box segment.")] public bool CanBeCompleted { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of a parent objective. If set, this objective is displayed as a subobjective under the parent objective.")] public Identifier ParentObjectiveId { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Obsolete, Serialize(false, IsPropertySaveable.Yes, description: "Legacy support. Should the video defined by VideoFile play automatically, or wait for the user to play it.")] public bool AutoPlayVideo { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Obsolete, Serialize("", IsPropertySaveable.Yes, description: "Legacy support. Tag of the main text to display in info box segments.")] public Identifier TextTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Obsolete, Serialize("", IsPropertySaveable.Yes, description: "Legacy support. Path of a video file to display in info box segments.")] public string VideoFile { get; set; } - [Serialize(450, IsPropertySaveable.Yes)] + [Obsolete, Serialize(450, IsPropertySaveable.Yes, description: "Legacy support. Width of the info box segment.")] public int Width { get; set; } - [Serialize(80, IsPropertySaveable.Yes)] + [Obsolete, Serialize(80, IsPropertySaveable.Yes, description: "Legacy support. Height of the info box segment.")] public int Height { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) to show the objective to.")] public Identifier TargetTag { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs index 0adf73e69..87ab4de2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs @@ -1,17 +1,16 @@ using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Starts a fire at the position of a specific target. + /// class FireAction : EventAction { - [Serialize(10.0f, IsPropertySaveable.Yes)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "Size of the fire (width in pixels).")] public float Size { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to start the fire at.")] public Identifier TargetTag { get; set; } public FireAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs index f2899ff8a..dad761ed0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs @@ -2,12 +2,15 @@ using System.Linq; namespace Barotrauma { + /// + /// Gives experience to a specific character. + /// class GiveExpAction : EventAction { - [Serialize(0, IsPropertySaveable.Yes)] + [Serialize(0, IsPropertySaveable.Yes, description: "The amount of experience to give. Cannot be negative.")] public int Amount { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) to give the experience to.")] public Identifier TargetTag { get; set; } public GiveExpAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index 8f8cc7d0b..92f878f9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs @@ -2,15 +2,18 @@ using System.Linq; namespace Barotrauma { + /// + /// Increases the skill level of a specific character. + /// class GiveSkillExpAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the skill to increase.")] public Identifier Skill { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much the skill should increase.")] public float Amount { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) whose skill to increase.")] public Identifier TargetTag { get; set; } public GiveSkillExpAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs index d2cb4011f..bc83b8afd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs @@ -1,11 +1,14 @@ namespace Barotrauma { + /// + /// Makes the event jump to a somewhere else in the event. + /// class GoTo : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Name of the label to jump to.")] public string Name { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "How many times can this GoTo action be repeated? Can be used to make some parts of an event repeat a limited number of times. If negative or zero, there's no limit.")] public int MaxTimes { get; set; } private int counter; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs index e9995190f..f029d9b1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs @@ -1,14 +1,17 @@ namespace Barotrauma { + /// + /// Makes a specific character invulnerable to damage and unable to die. + /// class GodModeAction : EventAction { - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the godmode be enabled or disabled?")] public bool Enabled { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Should the character's active afflictions be updated (e.g. applying visual effects of the afflictions)")] public bool UpdateAfflictions { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character whose godmode to enable/disable.")] public Identifier TargetTag { get; set; } public GodModeAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs index 8900299ea..688212ddb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs @@ -5,17 +5,20 @@ using System.Linq; namespace Barotrauma; +/// +/// Highlights a specific entity. +/// partial class HighlightAction : EventAction { private static readonly Color highlightColor = Color.Orange; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to highlight.")] 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)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the highlight be turned on or off?")] public bool State { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/InventoryHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/InventoryHighlightAction.cs index 7eecbbbb2..272bd6c5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/InventoryHighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/InventoryHighlightAction.cs @@ -1,17 +1,20 @@ namespace Barotrauma; +/// +/// Highlights specific items in a specific inventory. +/// partial class InventoryHighlightAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity or entities whose inventory the item should be highlighted in. Must be a character or an item with an inventory.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item(s) to highlight.")] public Identifier ItemIdentifier { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "If the target is an item with multiple ItemContainer components (i.e. multiple inventories), such as a fabricator, this determines which inventory to highlight the item in (0 = first, 1 = second). If negative, it doesn't matter which inventory the item is in.")] public int ItemContainerIndex { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the action will go look through all the containers in the target inventory (e.g. highlighting a tank in a welding tool in the target inventory).")] public bool Recursive { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs index a95a7dea7..0dd3e3c70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs @@ -1,7 +1,8 @@ -using System.Xml.Linq; - namespace Barotrauma { + /// + /// Defines a point in the event that actions can jump to. + /// class Label : EventAction { [Serialize("", IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/LayerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/LayerAction.cs new file mode 100644 index 000000000..5012cde09 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/LayerAction.cs @@ -0,0 +1,61 @@ +namespace Barotrauma; + +/// +/// Enable or disable a specific layer in a specific submarine. +/// +class LayerAction : EventAction +{ + [Serialize("", IsPropertySaveable.Yes, description: "Which layer to enable/disable. Use \"All\" to apply it to all layers.")] + public Identifier Layer { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Whether to enable or disable the layer.")] + public bool Enabled { get; set; } + + [Serialize(TagAction.SubType.Any, IsPropertySaveable.Yes, description: "The type of submatine to enable or disable the layer in.")] + public TagAction.SubType SubmarineType { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Should the action continue if it can't find the specified layer in the specified submarine(s).")] + public bool ContinueIfNotFound { get; set; } + + public LayerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + private bool isFinished; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + bool layerFound = false; + foreach (var submarine in Submarine.Loaded) + { + if (!TagAction.SubmarineTypeMatches(submarine, SubmarineType)) { continue; } + if (submarine.LayerExists(Layer)) + { + submarine.SetLayerEnabled(Layer, Enabled, sendNetworkEvent: true); + layerFound = true; + } + } + if (ContinueIfNotFound) + { + isFinished = true; + } + else + { + if (layerFound) { isFinished = true; } + } + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(LayerAction)} -> ({(Enabled ? "Enable" : "Disable")} {Layer.ColorizeObject()})"; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs index 6ac082e66..8c34bcd8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs @@ -1,52 +1,55 @@ namespace Barotrauma { + /// + /// Displays a message box, or modifies an existing one. + /// partial class MessageBoxAction : EventAction { public enum ActionType { Create, ConnectObjective, Close, Clear } - [Serialize(ActionType.Create, IsPropertySaveable.Yes)] + [Serialize(ActionType.Create, IsPropertySaveable.Yes, description: "What do you want to do with the message box (Create, ConnectObjective, Close, Clear)?")] public ActionType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Optional identifier of the tutorial \"segment\" that can be referenced by other event actions.")] public Identifier Identifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "An arbitrary tag given to the message box. Only required if you're intending to close or clear the box with another MessageBoxAction later.")] public string Tag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Text displayed in the header of the message box. Can be either the text as-is, or a tag referring to a line in a text file.")] public Identifier Header { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Text displayed in the body of the message box. Can be either the text as-is, or a tag referring to a line in a text file.")] public Identifier Text { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Style of the icon displayed in the corner of the message box (optional). The style must be defined in a UIStyle file.")] public string IconStyle { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the button that closes the box be hidden? If it is hidden, you must close the box manually using another MessageBoxAction.")] public bool HideCloseButton { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) to show the message box to.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed on some input (e.g. Select, Use, CrewOrders).")] public string CloseOnInput { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed when the user selects an item that has this tag.")] public Identifier CloseOnSelectTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed when the user picks up an item that has this tag.")] public Identifier CloseOnPickUpTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed when the user equips an item that has this tag.")] public Identifier CloseOnEquipTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed when the user exits a room with this name.")] public Identifier CloseOnExitRoomName { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed when the user is in a room with this name.")] public Identifier CloseOnInRoomName { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag that will be used to get the text for the objective that is displayed on the screen.")] public Identifier ObjectiveTag { get; set; } [Serialize(true, IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 4592e6751..a423ce459 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -7,15 +7,18 @@ using System.Linq; namespace Barotrauma { + /// + /// Unlocks a mission in a nearby level or location. + /// partial class MissionAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the mission to unlock.")] public Identifier MissionIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the mission to unlock. If there are multiple missions with the tag, one is chosen randomly.")] public Identifier MissionTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The mission can only be unlocked in a location that's occupied by this faction.")] public Identifier RequiredFaction { get; set; } public ImmutableArray LocationTypes { get; } @@ -46,7 +49,14 @@ namespace Barotrauma contentPackage: element.ContentPackage); } LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); - random = new MTRandom(parentEvent.RandomSeed); + //the action chooses the same mission if + // 1. event seed is the same (based on level seed, changes when events are completed) + // 2. event is the same (two different events shouldn't choose the same mission) + // 3. the MissionAction is the same (two different actions in the same event shouldn't choose the same mission) + random = new MTRandom( + parentEvent.RandomSeed + + ToolBox.StringToInt(ParentEvent.Prefab.Identifier.Value) + + ParentEvent.Actions.Count); } public override bool IsFinished(ref string goTo) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs index 02b726aa8..98dcfd8f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs @@ -1,8 +1,12 @@ namespace Barotrauma { + + /// + /// Changes the state of a specific active mission. The way the states are used depends on the type of mission. + /// class MissionStateAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the mission whose state to change.")] public Identifier MissionIdentifier { get; set; } public enum OperationType @@ -11,10 +15,10 @@ namespace Barotrauma Add } - [Serialize(OperationType.Set, IsPropertySaveable.Yes)] + [Serialize(OperationType.Set, IsPropertySaveable.Yes, description: "Should the value be added to the state of the mission, or should the state be set to the specified value.")] public OperationType Operation { get; set; } - [Serialize(0, IsPropertySaveable.Yes)] + [Serialize(0, IsPropertySaveable.Yes, description: "The state to set the mission to, or how much to add to the state of the mission.")] public int State { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs index a21925c94..4b35c6c46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs @@ -1,17 +1,20 @@ namespace Barotrauma { + /// + /// Modifies the current location in some way (e.g. adjusting the faction, type of name). + /// class ModifyLocationAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the faction to set as the location's primary faction (optional).")] public Identifier Faction { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the faction to set as the location's secondary faction (optional).")] public Identifier SecondaryFaction { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the location type to set as the location's new type (optional)")] public Identifier Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "New name to give to the location (optional). Can either be the name as-is, or a tag referring to a line in a text file.")] public Identifier Name { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs index c3030d6df..b6d4331a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs @@ -1,20 +1,21 @@ -using System; +using Barotrauma.Networking; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Networking; namespace Barotrauma { + /// + /// Give or remove money from the crew or a specific character. + /// class MoneyAction : EventAction { public MoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(0, IsPropertySaveable.Yes)] + [Serialize(0, IsPropertySaveable.Yes, description: "Amount of money to give or remove.")] public int Amount { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If set, the money is removed from character(s) with this tag.")] public Identifier TargetTag { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index 239b95f60..7269d169f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -4,20 +4,21 @@ using System.Linq; namespace Barotrauma { + /// + /// Changes the team of an NPC. Most common use cases are adding a character to the crew, or turning an NPC hostile to the crew by changing their team to a hostile one. + /// class NPCChangeTeamAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the NPC(s) whose team to change.")] public Identifier NPCTag { get; set; } - [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes)] + [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes, description: "The team to move the NPC to. None = unspecified, Team1 = player crew, Team2 = the team opposing Team1 (= hostile to player crew), FriendlyNPC = friendly to all other teams.")] public CharacterTeamType TeamID { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the NPC be added to the player crew?")] public bool AddToCrew { get; set; } - - - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the NPC be removed from the player crew?")] public bool RemoveFromCrew { get; set; } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index 94111f0fd..2b01611ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -3,21 +3,24 @@ using System.Linq; namespace Barotrauma { + /// + /// Makes an NPC follow or stop following a specific target. + /// class NPCFollowAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the NPC(s) that should follow the target.")] public Identifier NPCTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the target. Can be any type of entity: if it's a static one like a device or a hull, the NPC will just stay at the position of that target.")] public Identifier TargetTag { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop following the target?")] public bool Follow { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs to target (e.g. you could choose to only make a specific number of security officers follow the player.)")] public int MaxTargets { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop following the target when the event resets?")] public bool AbandonOnReset { get; set; } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs index e3e8c9ade..d19cd7c52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs @@ -1,39 +1,43 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; namespace Barotrauma { + /// + /// Makes an NPC select an item, and operate it if it's something AI characters can operate. + /// class NPCOperateItemAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the NPC(s) that should operate the item.")] public Identifier NPCTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item to operate. If it's not something AI characters can or know how to operate, such as a cabinet or an engine, the NPC will just select it.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("Controller", IsPropertySaveable.Yes, description: "Name of the component to operate. For example, the Controller component of a periscope or the Reactor component of a nuclear reactor.")] public Identifier ItemComponentName { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the option, if there are several ways the item can be operated. For example, \"powerup\" or \"shutdown\" when operating a reactor.")] public Identifier OrderOption { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the character equip the item before attempting to operate it (only valid if the item is equippable).")] public bool RequireEquip { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the character start or stop operating the item.")] public bool Operate { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs the action can target. For example, you could only make a specific number of security officers man a periscope.")] public int MaxTargets { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop operating the item when the event resets?")] public bool AbandonOnReset { get; set; } private bool isFinished = false; - + public NPCOperateItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - private List affectedNpcs = null; private Item target = null; @@ -41,7 +45,13 @@ namespace Barotrauma { if (isFinished) { return; } - target = ParentEvent.GetTargets(TargetTag).FirstOrDefault() as Item; + var potentialTargets = ParentEvent.GetTargets(TargetTag).OfType(); + var nonSelectedItems = potentialTargets.Where(it => it.GetComponent()?.User == null); + + target = + nonSelectedItems.Any() ? + nonSelectedItems.GetRandomUnsynced() : + potentialTargets.GetRandomUnsynced(); if (target == null) { return; } int targetCount = 0; @@ -53,7 +63,6 @@ namespace Barotrauma if (Operate) { - ItemComponentName = "Controller".ToIdentifier(); var itemComponent = target.Components.FirstOrDefault(ic => ItemComponentName == ic.Name); if (itemComponent == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index 28db501f7..8a12868f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -1,15 +1,17 @@ using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Makes an NPC stop and wait. + /// class NPCWaitAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the NPC(s) that should wait.")] public Identifier NPCTag { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop waiting?")] public bool Wait { get; set; } private bool isFinished = false; @@ -35,6 +37,7 @@ namespace Barotrauma var gotoObjective = new AIObjectiveGoTo( AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true) { + FaceTargetOnCompleted = false, OverridePriority = 100.0f, SourceEventAction = this, IsWaitOrder = true, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs index 23bb3b63a..baf4fba0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs @@ -2,6 +2,9 @@ namespace Barotrauma { + /// + /// Executes all the child actions when the round ends. + /// class OnRoundEndAction : EventAction { private readonly SubactionGroup subActions; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs index a9e4ffea3..163f6f65c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs @@ -1,12 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; - namespace Barotrauma { + /// + /// Randomly executes either of the child actions (Success or Failure). + /// class RNGAction : BinaryOptionAction { - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The probability of executing the Success actions. A value between 0-1.")] public float Chance { get; set; } public RNGAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index 348e856e8..75c446b46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -3,15 +3,18 @@ using System.Collections.Immutable; namespace Barotrauma { + /// + /// Removes (deletes) a specific item or items. + /// class RemoveItemAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item(s) to remove.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Optional list of identifiers the item(s) must have. You might for example want to go through all tagged items inside a cabinet, but only remove specific types of items.")] public string ItemIdentifiers { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Maximum number of items to remove.")] public int Amount { get; set; } private readonly ImmutableHashSet itemIdentifierSplit; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs index f161a63fa..3298f8312 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma +namespace Barotrauma { + /// + /// Adjusts the crew's reputation by some value. + /// class ReputationAction : EventAction { public enum ReputationType @@ -17,13 +14,13 @@ namespace Barotrauma public ReputationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Amount of reputation to add or remove.")] public float Increase { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the faction you want to adjust the reputation for. Ignored if TargetType is set to Location.")] public Identifier Identifier { get; set; } - [Serialize(ReputationType.None, IsPropertySaveable.Yes)] + [Serialize(ReputationType.None, IsPropertySaveable.Yes, description: "Do you want to adjust the reputation for a specific faction, or whichever faction controls the current location?")] public ReputationType TargetType { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs index 7e33a2373..4257430d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs @@ -1,8 +1,10 @@ using System; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Sets a campaign metadata value. The metadata can be any arbitrary data you want to save: for example, whether some event has been completed, the number of times something has been done during the campaign, or at what stage of some multi-part event chain the crew is at. + /// class SetDataAction : EventAction { public enum OperationType @@ -14,13 +16,13 @@ namespace Barotrauma public SetDataAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(OperationType.Set, IsPropertySaveable.Yes)] + [Serialize(OperationType.Set, IsPropertySaveable.Yes, description: "Do you want to set the metadata to a specific value, multiply it, or add to it.")] public OperationType Operation { get; set; } - [Serialize(null, IsPropertySaveable.Yes)] + [Serialize(null, IsPropertySaveable.Yes, description: "Depending on the operation, the value you want to set the metadata to, multiply it with, or add to it.")] public string Value { get; set; } = null!; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the metadata to set. Can be any arbitrary identifier, e.g. itemscollected, my_custom_event_state, specialnpckilled...")] public Identifier Identifier { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs index bdded765e..5b153da11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs @@ -1,8 +1,10 @@ using System; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Adjusts the price multiplier for stores or mechanical repairs in the current location. + /// class SetPriceMultiplierAction : EventAction { public enum OperationType @@ -19,13 +21,13 @@ namespace Barotrauma Mechanical } - [Serialize(1.0f, IsPropertySaveable.Yes)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Value to set as the multiplier, or to multiply, min or max the current multiplier with.")] public float Multiplier { get; set; } - [Serialize(OperationType.Set, IsPropertySaveable.Yes)] + [Serialize(OperationType.Set, IsPropertySaveable.Yes, description: "Do you want to set the value as the multiplier, multiply the existing multiplier with it, or take the smaller or larger of the values.")] public OperationType Operation { get; set; } - [Serialize(PriceMultiplierType.Store, IsPropertySaveable.Yes)] + [Serialize(PriceMultiplierType.Store, IsPropertySaveable.Yes, description: "Do you want to set the price multiplier for stores or for mechanical services (hull and item repairs and restoring lost shuttles)?")] public PriceMultiplierType TargetMultiplier { get; set; } public SetPriceMultiplierAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs index af798fefc..f4b402ca4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs @@ -2,6 +2,9 @@ namespace Barotrauma { + /// + /// Sets the state of the traitor event. Only valid in traitor events. + /// class SetTraitorEventStateAction : EventAction { private readonly TraitorEvent? traitorEvent; @@ -19,7 +22,7 @@ namespace Barotrauma } } - [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)] + [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes, description: "The state to set the traitor event to (Incomplete, Completed or Failed).")] public TraitorEvent.State State { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs index b3376e4a3..10d7f7dff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs @@ -1,22 +1,22 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Performs a skill check and executes either the Success or Failure child actions depending on whether the check succeeds. + /// class SkillCheckAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The identifier of the skill to check.")] public Identifier RequiredSkill { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The required skill level for the check to succeed.")] public float RequiredLevel { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the skill check be probability-based (i.e. if you have half the required skill level, the chance of success is 50%), or should the check always fail when under the required level and always succeed when above? ")] public bool ProbabilityBased { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) whose skill to check. If there are multiple targets, the action succeeds if any of their skill checks succeeds.")] public Identifier TargetTag { get; set; } public SkillCheckAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 15a58b8c0..507d547bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -6,6 +6,9 @@ using System.Linq; namespace Barotrauma { + /// + /// Spawns an entity (e.g. item, NPC, monster). + /// class SpawnAction : EventAction { public enum SpawnLocationType @@ -41,16 +44,16 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag of an entity with an inventory to spawn the item into.")] public Identifier TargetInventory { get; set; } - [Serialize(SpawnLocationType.Any, IsPropertySaveable.Yes)] + [Serialize(SpawnLocationType.Any, IsPropertySaveable.Yes, description: "Where should the entity spawn? This can be restricted further with the other spawn point options.")] public SpawnLocationType SpawnLocation { get; set; } - [Serialize(SpawnType.Human, IsPropertySaveable.Yes)] + [Serialize(SpawnType.Human, IsPropertySaveable.Yes, description: "Type of spawnpoint to spawn the entity at. Ignored if SpawnPointTag is set.")] public SpawnType SpawnPointType { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of a spawnpoint to spawn the entity at.")] public Identifier SpawnPointTag { get; set; } - [Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes)] + [Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes, description: "Team of the NPC to spawn. Only valid when spawning a character.")] public CharacterTeamType TeamID { get; protected set; } [Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")] @@ -61,10 +64,10 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, description: "If false, we won't spawn another character if one with the same identifier has already been spawned.")] public bool AllowDuplicates { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Number of entities to spawn.")] public int Amount { get; set; } - [Serialize(100.0f, IsPropertySaveable.Yes)] + [Serialize(100.0f, IsPropertySaveable.Yes, description: "Random offset to add to the spawn position.")] public float Offset { get; set; } [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the entity prefer to spawn in.")] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs index f07c316cd..2035c53c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs @@ -2,13 +2,16 @@ using System.Collections.Generic; namespace Barotrauma { + /// + /// Executes all the StatusEffects defined as child elements of the action. + /// partial class StatusEffectAction : EventAction { private readonly List effects = new List(); private readonly int actionIndex; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity or entities the status effect should target.")] public Identifier TargetTag { get; set; } public StatusEffectAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 8a1e389fe..a2046bd0e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -6,26 +6,32 @@ using System.Linq; namespace Barotrauma { + /// + /// Tags a specific entity. Tags are used by other actions to refer to specific entities. The tags are event-specific, i.e. you cannot use a tag that was added by another event to refer to an entity. + /// class TagAction : EventAction { public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "What criteria to use to select the entities to target. Valid values are players, player, traitor, nontraitor, nontraitorplayer, bot, crew, humanprefabidentifier:[id], jobidentifier:[id], structureidentifier:[id], structurespecialtag:[tag], itemidentifier:[id], itemtag:[tag], hull, hullname:[name], submarine:[type], eventtag:[tag].")] public string Criteria { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The tag to apply to the target.")] public Identifier Tag { get; set; } - [Serialize(SubType.Any, IsPropertySaveable.Yes)] + [Serialize(SubType.Any, IsPropertySaveable.Yes, description: "The type of submarine the target needs to be in.")] public SubType SubmarineType { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, "If set, the target must be in an outpost module that has this tag.")] + public Identifier RequiredModuleTag { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Should incapacitated (e.g. dead, paralyzed, unconscious) characters be ignored, i.e. not considered valid targets?")] public bool IgnoreIncapacitatedCharacters { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Can items that have been set to be hidden in-game be tagged?")] public bool AllowHiddenItems { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "If there are multiple matching targets, should all of them be tagged or one chosen randomly?")] public bool ChooseRandom { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Should the event continue if the TagAction can't find any valid targets?")] @@ -85,7 +91,7 @@ namespace Barotrauma private void TagByEventTag(Identifier eventTag) { - AddTarget(Tag, ParentEvent.GetTargets(eventTag).Where(t => SubmarineTypeMatches(t.Submarine))); + AddTarget(Tag, ParentEvent.GetTargets(eventTag).Where(t => MatchesRequirements(t))); } private void TagPlayers() @@ -157,7 +163,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Structure, - e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); + e => e is Structure s && MatchesRequirements(s) && s.Prefab.Identifier == identifier); } private void TagStructuresBySpecialTag(Identifier tag) @@ -165,7 +171,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Structure, - e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag); + e => e is Structure s && MatchesRequirements(s) && s.SpecialTag.ToIdentifier() == tag); } private void TagItemsByIdentifier(Identifier identifier) @@ -173,7 +179,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Item, - e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); + e => e is Item it && it.Prefab.Identifier == identifier && IsValidItem(it)); } private void TagItemsByTag(Identifier tag) @@ -181,7 +187,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Item, - e => e is Item it && IsValidItem(it) && it.HasTag(tag)); + e => e is Item it && it.HasTag(tag) && IsValidItem(it)); } private void TagHulls() @@ -189,7 +195,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Hull, - e => e is Hull h && SubmarineTypeMatches(h.Submarine)); + e => e is Hull h && MatchesRequirements(h)); } private void TagHullsByName(Identifier name) @@ -197,7 +203,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Hull, - e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); + e => e is Hull h && MatchesRequirements(h) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); } private void TagSubmarinesByType(Identifier type) @@ -205,33 +211,76 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Submarine, - e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); + e => e is Submarine s && MatchesRequirements(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); } private bool IsValidItem(Item it) { return (!it.HiddenInGame || AllowHiddenItems) && + ModuleTagMatches(it) && //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 MatchesRequirements(Entity e) + { + return ModuleTagMatches(e) && SubmarineTypeMatches(e.Submarine); + } + + private bool ModuleTagMatches(Entity e) + { + if (RequiredModuleTag.IsEmpty) { return true; } + if (e?.Submarine == null) { return false; } + + Hull hull; + if (e is Character character) + { + hull = character.CurrentHull; + } + else if (e is Item item) + { + hull = item.CurrentHull; + } + else if (e is WayPoint wp) + { + hull = wp.CurrentHull; + } + else if (e is Hull h) + { + hull = h; + } + else + { + DebugConsole.AddWarning($"Potential error in event \"{ParentEvent.Prefab.Identifier}\": {nameof(TagAction)} cannot check the module tags of an entity of the type {e.GetType()}."); + return false; + } + + return hull != null && hull.OutpostModuleTags.Contains(RequiredModuleTag); + } + + private bool SubmarineTypeMatches(Submarine sub) { - if (SubmarineType == SubType.Any) { return true; } + return SubmarineTypeMatches(sub, SubmarineType); + } + + public static bool SubmarineTypeMatches(Submarine sub, SubType submarineType) + { + if (submarineType == SubType.Any) { return true; } if (sub == null) { return false; } switch (sub.Info.Type) { case Barotrauma.SubmarineType.Player: - return SubmarineType.HasFlag(SubType.Player) && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle; + return submarineType.HasFlag(SubType.Player) && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle; case Barotrauma.SubmarineType.Outpost: case Barotrauma.SubmarineType.OutpostModule: - return SubmarineType.HasFlag(SubType.Outpost); + return submarineType.HasFlag(SubType.Outpost); case Barotrauma.SubmarineType.Wreck: - return SubmarineType.HasFlag(SubType.Wreck); + return submarineType.HasFlag(SubType.Wreck); case Barotrauma.SubmarineType.BeaconStation: - return SubmarineType.HasFlag(SubType.BeaconStation); + return submarineType.HasFlag(SubType.BeaconStation); default: return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs index 1aacece60..407de994d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs @@ -1,19 +1,22 @@ namespace Barotrauma; +/// +/// Teleports a specific entity to a specific spawn point. +/// class TeleportAction : EventAction { public enum TeleportPosition { MainSub, Outpost } - [Serialize(TeleportPosition.MainSub, IsPropertySaveable.Yes)] + [Serialize(TeleportPosition.MainSub, IsPropertySaveable.Yes, description: "Should the entity be teleported to the main submarine or the outpost?")] public TeleportPosition Position { get; set; } - [Serialize(SpawnType.Human, IsPropertySaveable.Yes)] + [Serialize(SpawnType.Human, IsPropertySaveable.Yes, description: "The type of the spawnpoint to teleport the character to.")] public SpawnType SpawnType { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag of the spawnpoint.")] public string SpawnPointTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the target(s) to teleport.")] public Identifier TargetTag { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 5c8bce3d0..69b1b1a21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -1,10 +1,12 @@ -using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; namespace Barotrauma { + /// + /// Waits for a player to trigger the action before continuing. Triggering can mean entering a specific trigger area, or interacting with a specific entity. + /// class TriggerAction : EventAction { public enum TriggerType diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs index ae6a8c75c..e75d563c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs @@ -1,11 +1,14 @@ namespace Barotrauma { + /// + /// Triggers another scripted event. + /// class TriggerEventAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the event to trigger.")] public Identifier Identifier { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the event will trigger at the beginning of the next round. Useful for e.g. triggering some scripted event in the outpost after you finish a mission.")] public bool NextRound { get; set; } private bool isFinished; @@ -41,7 +44,7 @@ } else { - var ev = eventPrefab.CreateInstance(); + var ev = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); if (ev != null) { GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialCompleteAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialCompleteAction.cs index 9544f9371..33ec26657 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialCompleteAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialCompleteAction.cs @@ -1,5 +1,8 @@ namespace Barotrauma { + /// + /// Completes the tutorial. Only valid in tutorial events. + /// class TutorialCompleteAction : EventAction { private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs index a47d4e932..cf0316278 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs @@ -2,17 +2,20 @@ using System.Linq; namespace Barotrauma; +/// +/// Displays a tutorial icon next to a specific target. +/// class TutorialIconAction : EventAction { public enum ActionType { Add, Remove, RemoveTarget, RemoveIcon, Clear }; - [Serialize(ActionType.Add, IsPropertySaveable.Yes)] + [Serialize(ActionType.Add, IsPropertySaveable.Yes, description: "What to do with the icon. Add = add an icon, Remove = remove the icon that has the specific target and style, RemoveTarget = remove all icons assigned to the specific target, RemoveIcon = remove all icons with the specific style, Remove = remove all icons.")] public ActionType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the target to assign the icon to.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Style of the icon.")] public Identifier IconStyle { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs index ac2beeebb..eeb4c1b72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs @@ -1,5 +1,8 @@ namespace Barotrauma; +/// +/// Highlights an UI element of some kind. Generally used in tutorials. +/// partial class UIHighlightAction : EventAction { public enum ElementId @@ -24,28 +27,28 @@ partial class UIHighlightAction : EventAction MessageBoxCloseButton } - [Serialize(ElementId.None, IsPropertySaveable.Yes)] + [Serialize(ElementId.None, IsPropertySaveable.Yes, description: "An arbitrary identifier that must match the userdata of the UI element. The userdatas of the element are hard-coded, so this option is generally intended for the developers' use.")] public ElementId Id { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If the element's userdata is an entity or an entity prefab, it's identifier must match this value.")] public Identifier EntityIdentifier { get; set; } - [Serialize(OrderCategory.Emergency, IsPropertySaveable.Yes)] + [Serialize(OrderCategory.Emergency, IsPropertySaveable.Yes, description: "If the element's userdata is an order category, it must match this.")] public OrderCategory OrderCategory { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If the element's userdata is an order, it must match this identifier.")] public Identifier OrderIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If the element's userdata is an order with options, it must match this.")] public Identifier OrderOption { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If the element's userdata is an order, the order must target an entity with this tag.")] public Identifier OrderTargetTag { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the element bounce up an down in addition to being highlighted.")] public bool Bounce { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the action highlight the first matching element it finds, or all of them?")] public bool HighlightMultiple { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs index a69ee509a..eaefc7169 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs @@ -1,12 +1,12 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Unlocks a "locked" pathways between locations, if there are any such paths adjacent to the current location. + /// class UnlockPathAction : EventAction { public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs index 202a8734d..dcbefe305 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs @@ -1,8 +1,11 @@ namespace Barotrauma { + /// + /// Waits for a specific amount of time before continuing the execution of the event. + /// class WaitAction : EventAction { - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How long to wait (in seconds).")] public float Time { get; set; } private float timeRemaining; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs index 89106db36..981ce8bd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs @@ -1,22 +1,25 @@ -#nullable enable +#nullable enable using Barotrauma.Items.Components; using System.Linq; namespace Barotrauma { + /// + /// Waits for some item(s) to be fabricated before continuing the execution of the event. + /// class WaitForItemFabricatedAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character who must fabricate the item. If empty, it doesn't matter who fabricates it.")] public Identifier CharacterTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item that must be fabricated. Optional if ItemTag is set.")] public Identifier ItemIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item that must be fabricated. Optional if ItemIdentifier is set.")] public Identifier ItemTag { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Number of items that need to be fabricated.")] public int Amount { get; set; } [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the fabricated item(s).")] @@ -48,7 +51,8 @@ namespace Barotrauma { if (!ParentEvent.GetTargets(CharacterTag).Contains(character)) { return; } } - if (item.ContainerIdentifier == ItemTag || item.HasTag(ItemTag)) + if ((!ItemIdentifier.IsEmpty && item.Prefab.Identifier == ItemIdentifier) || + (!ItemTag.IsEmpty && item.HasTag(ItemTag))) { if (!ApplyTagToItem.IsEmpty) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs index dce9b2f62..dd3375657 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs @@ -7,30 +7,33 @@ using System.Linq; namespace Barotrauma { + /// + /// Waits for some item(s) to be used before continuing the execution of the event. + /// class WaitForItemUsedAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item that must be used. Note that the item needs to have been tagged by the event - this does not refer to the tags that can be set per-item in the sub editor.")] public Identifier ItemTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character that must use the item. If there's multiple matching characters, it's enough if any of them use the item. If empty, it doesn't matter who uses the item.")] public Identifier UserTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Name of the ItemComponent that the character must use. If empty, the character attempts to use all of them.")] public Identifier TargetItemComponent { get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the target item when it's used.")] + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag to apply to the target item when it's used.")] public Identifier ApplyTagToItem { get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the user when the target item is used.")] + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag to apply to the user when the target item is used.")] public Identifier ApplyTagToUser{ get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside when the item is used.")] + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag to apply to the hull the target item is inside when the item is used.")] public Identifier ApplyTagToHull { get; set; } - [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.")] + [Serialize("", IsPropertySaveable.Yes, description: "Optional 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)] + [Serialize(1, IsPropertySaveable.Yes, description: "How many times does the item need to be used. Defaults to 1.")] public int RequiredUseCount { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 72f17db3c..183777f8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -144,7 +144,7 @@ namespace Barotrauma public bool Enabled = true; private MTRandom random; - private int randomSeed; + public int RandomSeed { get; private set; } public void StartRound(Level level) { @@ -171,13 +171,13 @@ namespace Barotrauma if (level != null) { - randomSeed = ToolBox.StringToInt(level.Seed); + RandomSeed = ToolBox.StringToInt(level.Seed); foreach (var previousEvent in level.LevelData.EventHistory) { - randomSeed ^= ToolBox.IdentifierToInt(previousEvent); + RandomSeed ^= ToolBox.IdentifierToInt(previousEvent); } } - random = new MTRandom(randomSeed); + random = new MTRandom(RandomSeed); bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode; EventSet initialEventSet = SelectRandomEvents( @@ -214,7 +214,7 @@ namespace Barotrauma var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction); if (unlockPathEventPrefab != null) { - var newEvent = unlockPathEventPrefab.CreateInstance(); + var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed); activeEvents.Add(newEvent); } else @@ -250,7 +250,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}."); continue; } - var ev = eventPrefab.CreateInstance(); + var ev = eventPrefab.CreateInstance(RandomSeed); if (ev != null) { QueuedEvents.Enqueue(ev); @@ -543,9 +543,8 @@ namespace Barotrauma if (eventPrefabs != null && random.NextDouble() <= probability) { var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random); - var newEvent = eventPrefab.CreateInstance(); + var newEvent = eventPrefab.CreateInstance(RandomSeed); if (newEvent == null) { continue; } - newEvent.RandomSeed = randomSeed; if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) @@ -588,8 +587,9 @@ namespace Barotrauma if (random.NextDouble() > probability) { continue; } var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random); - var newEvent = eventPrefab.CreateInstance(); + var newEvent = eventPrefab.CreateInstance(RandomSeed); if (newEvent == null) { continue; } + if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } if (!selectedEvents.ContainsKey(eventSet)) { selectedEvents.Add(eventSet, new List()); @@ -730,11 +730,9 @@ namespace Barotrauma float distFromStart = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.StartExitPosition.ToPoint(), level.StartPosition.ToPoint(), refEntity.WorldPosition.ToPoint())); float distFromEnd = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.EndExitPosition.ToPoint(), level.EndPosition.ToPoint(), refEntity.WorldPosition.ToPoint())); - //don't create new events if within 50 meters of the start/end of the level if (!eventSet.AllowAtStart) { - if (distanceTraveled <= 0.0f || - distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || + if (distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f) { return false; @@ -767,7 +765,7 @@ namespace Barotrauma public void Update(float deltaTime) { - if (!Enabled || level == null) { return; } + if (!Enabled) { return; } if (GameMain.GameSession.Campaign?.DisableEvents ?? false) { return; } if (!eventsInitialized) @@ -871,6 +869,10 @@ namespace Barotrauma { pendingEventSets.Add(eventSet); CreateEvents(eventSet); + foreach (Event newEvent in selectedEvents[eventSet]) + { + if (!newEvent.Initialized) { newEvent.Init(eventSet); } + } }; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index 721f1bcb0..f6a8847ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -99,19 +99,19 @@ namespace Barotrauma UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0); } - public bool TryCreateInstance(out T instance) where T : Event + public bool TryCreateInstance(int seed, out T instance) where T : Event { - instance = CreateInstance() as T; + instance = CreateInstance(seed) as T; return instance is not null; } - public Event CreateInstance() + public Event CreateInstance(int seed) { - ConstructorInfo constructor = EventType.GetConstructor(new[] { GetType() }); + ConstructorInfo constructor = EventType.GetConstructor(new[] { GetType(), typeof(int) }); Event instance = null; try { - instance = constructor.Invoke(new object[] { this }) as Event; + instance = constructor.Invoke(new object[] { this, seed }) as Event; } catch (Exception ex) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index a21bc4cf2..6aed0f365 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -372,19 +372,19 @@ namespace Barotrauma MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f); MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f); - AllowAtStart = element.GetAttributeBool("allowatstart", false); + AllowAtStart = element.GetAttributeBool("allowatstart", parentSet?.AllowAtStart ?? false); PerRuin = element.GetAttributeBool("perruin", false); PerCave = element.GetAttributeBool("percave", false); PerWreck = element.GetAttributeBool("perwreck", false); - DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false); + DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", parentSet?.DisableInHuntingGrounds ?? false); IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck)); IgnoreIntensity = element.GetAttributeBool("ignoreintensity", parentSet?.IgnoreIntensity ?? false); - DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); - OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false)); - TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); + DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", parentSet?.DelayWhenCrewAway ?? (!PerRuin && !PerCave && !PerWreck)); + OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", parentSet?.OncePerLevel ?? false)); + TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", parentSet?.TriggerEventCooldown ?? true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); ResetTime = element.GetAttributeFloat(nameof(ResetTime), parentSet?.ResetTime ?? 0); - CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), false); + CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), parentSet?.CampaignTutorialOnly ?? false); ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1); ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1); @@ -449,6 +449,11 @@ namespace Barotrauma EventPrefabs = eventPrefabs.ToImmutableArray(); ChildSets = childSets.ToImmutableArray(); OverrideCommonness = overrideCommonness.ToImmutableDictionary(); + + if ((PerRuin && PerCave) || (PerWreck && PerCave) || (PerRuin && PerWreck)) + { + DebugConsole.AddWarning($"Error in event set \"{Identifier}\". Only one of the settings {nameof(PerRuin)}, {nameof(PerCave)} or {nameof(PerWreck)} can be enabled at the time."); + } } public void CheckLocationTypeErrors() @@ -560,7 +565,8 @@ namespace Barotrauma static void AddEvent(EventDebugStats stats, EventPrefab eventPrefab, Func filter = null) { - if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent)) + if (eventPrefab.EventType == typeof(MonsterEvent) && + eventPrefab.TryCreateInstance(GameMain.GameSession?.EventManager?.RandomSeed ?? 0, out MonsterEvent monsterEvent)) { if (filter != null && !filter(monsterEvent)) { return; } float spawnProbability = monsterEvent.Prefab?.Probability ?? 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs index 47c73a3eb..306a0f8a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs @@ -25,8 +25,8 @@ namespace Barotrauma return "MalfunctionEvent (" + string.Join(", ", targetItemIdentifiers) + ")"; } - public MalfunctionEvent(EventPrefab prefab) - : base(prefab) + public MalfunctionEvent(EventPrefab prefab, int seed) + : base(prefab, seed) { targetItems = new List(); @@ -39,9 +39,8 @@ namespace Barotrauma targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("itemidentifiers", Array.Empty()); } - public override void Init(EventSet parentSet) + protected override void InitEventSpecific(EventSet parentSet) { - base.Init(parentSet); var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)); int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient); for (int i = 0; i < itemAmount; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 3f22e082b..81e579c80 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -1,4 +1,4 @@ -using Barotrauma.Items.Components; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -137,18 +137,21 @@ namespace Barotrauma } } - var monsterSet = ToolBox.SelectWeightedRandom(monsterSets, m => m.Commonness, Rand.RandSync.Unsynced); - foreach ((CharacterPrefab monsterSpecies, Point monsterCountRange) in monsterSet.MonsterPrefabs) + if (monsterSets.Any()) { - int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1); - for (int i = 0; i < amount; i++) + var monsterSet = ToolBox.SelectWeightedRandom(monsterSets, m => m.Commonness, Rand.RandSync.Unsynced); + foreach ((CharacterPrefab monsterSpecies, Point monsterCountRange) in monsterSet.MonsterPrefabs) { - CoroutineManager.Invoke(() => + int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1); + for (int i = 0; i < amount; i++) { - //round ended before the coroutine finished - if (GameMain.GameSession == null || Level.Loaded == null) { return; } - Entity.Spawner.AddCharacterToSpawnQueue(monsterSpecies.Identifier, spawnPos); - }, Rand.Range(0f, amount)); + CoroutineManager.Invoke(() => + { + //round ended before the coroutine finished + if (GameMain.GameSession == null || Level.Loaded == null) { return; } + Entity.Spawner.AddCharacterToSpawnQueue(monsterSpecies.Identifier, spawnPos); + }, Rand.Range(0f, amount)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 2d40a2fc5..47b0092de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -15,6 +15,9 @@ namespace Barotrauma private readonly Dictionary inventorySlotIndices = new Dictionary(); private readonly Dictionary parentItemContainerIndices = new Dictionary(); + /// + /// Percentage of items (0.0 - 1.0) needed to be delivered to complete the mission. + /// private float requiredDeliveryAmount; private readonly List<(ContentXElement element, ItemContainer container)> itemsToSpawn = new List<(ContentXElement element, ItemContainer container)>(); @@ -86,7 +89,7 @@ namespace Barotrauma bool isPriorMission = true; foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) { - if (!(mission is CargoMission otherMission)) { continue; } + if (mission is not CargoMission otherMission) { continue; } if (mission == this) { isPriorMission = false; } previouslySelectedMissions.Add(otherMission); if (!isPriorMission) { continue; } @@ -99,7 +102,8 @@ namespace Barotrauma { int maxCount = subElement.GetAttributeInt("maxcount", 10); if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } - ItemPrefab itemPrefab = FindItemPrefab(subElement); + // For logging purposes + FindItemPrefab(subElement); while (itemsToSpawn.Count < maxItemCount) { itemsToSpawn.Add((subElement, null)); @@ -121,7 +125,7 @@ namespace Barotrauma bool isPriorMission = true; foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) { - if (!(mission is CargoMission otherMission)) { continue; } + if (mission is not CargoMission otherMission) { continue; } if (mission == this) { isPriorMission = false; } previouslySelectedMissions.Add(otherMission); if (!isPriorMission) { continue; } @@ -161,27 +165,53 @@ namespace Barotrauma itemsToSpawn.Add((itemConfig.Elements().First(), null)); } + // Calculate the current total reward, since it might differ from the + // prefab total reward depending on the current actual crate count. calculatedReward = 0; + bool crateValuesUniform = true; + int? prevCrateReward = null; foreach (var (element, container) in itemsToSpawn) { - int price = element.GetAttributeInt("reward", Prefab.Reward / itemsToSpawn.Count); - if (rewardPerCrate.HasValue) + int currentCrateReward = element.GetAttributeInt("reward", 0); + calculatedReward += currentCrateReward; + + // Apparently crates can have varying values, so we need to check + // here if that is the case, stopping checks on the first discrepancy + if (crateValuesUniform) { - if (price != rewardPerCrate.Value) { rewardPerCrate = -1; } + if (prevCrateReward.HasValue) + { + if (prevCrateReward.Value != currentCrateReward) + { + crateValuesUniform = false; + } + } + prevCrateReward = currentCrateReward; } - else - { - rewardPerCrate = price; - } - calculatedReward += price; } - if (rewardPerCrate.HasValue && rewardPerCrate < 0) { rewardPerCrate = null; } + + if (crateValuesUniform) + { + // If rewardPerCrate is set, it will be displayed in the client UI as eg. "123 mk x 5" + rewardPerCrate = calculatedReward / itemsToSpawn.Count; + } + else + { + // If rewardPerCrate is null, the client UI will display just the total reward + rewardPerCrate = null; + } + + // Apply the mission reward campaign setting multiplier to the per-crate price, too + if (GameMain.GameSession?.Campaign is CampaignMode campaign && rewardPerCrate is int confirmedRewardPerCrate) + { + rewardPerCrate = (int)Math.Round(confirmedRewardPerCrate * campaign.Settings.MissionRewardMultiplier); + } string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(currentSub))}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } } - public override int GetReward(Submarine sub) + public override int GetBaseReward(Submarine sub) { // If we are not at the location of the mission, skip the calculation of the reward if (GameMain.GameSession?.StartLocation != Locations[0]) @@ -272,7 +302,7 @@ namespace Barotrauma item.FindHull(); items.Add(item); - if (parent != null && parent.GetComponent() != null) + if (parent?.GetComponent() != null) { parentInventoryIDs.Add(item, parent.ID); parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(parent.GetComponent())); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 9c900601f..39838a1d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -61,7 +61,7 @@ namespace Barotrauma if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } } - public override int GetReward(Submarine sub) + public override int GetBaseReward(Submarine sub) { if (sub != missionSub) { @@ -160,14 +160,13 @@ namespace Barotrauma if (terroristChance > 0f) { - int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count); + int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count); terroristCount = Math.Clamp(terroristCount, 1, characters.Count); terroristCharacters.Clear(); characters.GetRange(0, terroristCount).ForEach(c => terroristCharacters.Add(c)); - + terroristCharacters.ForEach(c => c.IsHostileEscortee = true); terroristDistanceSquared = Vector2.DistanceSquared(Level.Loaded.StartPosition, Level.Loaded.EndPosition) * Rand.Range(0.35f, 0.65f); - #if DEBUG DebugConsole.AddWarning("Terrorists will trigger at range " + Math.Sqrt(terroristDistanceSquared)); foreach (Character character in terroristCharacters) @@ -251,6 +250,7 @@ namespace Barotrauma // decoupled from range check to prevent from weirdness if players handcuff a terrorist and move backwards foreach (Character character in terroristCharacters) { + character.IsHostileEscortee = true; if (character.HasTeamChange(TerroristTeamChangeIdentifier)) { // already triggered diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 561fed7d5..10d174170 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -259,7 +259,7 @@ namespace Barotrauma } else if (owner is Character c) { - return c.Info != null && GameMain.GameSession.CrewManager.CharacterInfos.Contains(c.Info); + return c.Info != null && GameMain.GameSession.CrewManager.GetCharacterInfos().Contains(c.Info); } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index be2130aac..060d47af6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -183,33 +183,32 @@ namespace Barotrauma completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})"); } - for (int n = 0; n < 2; n++) - { - 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); } - for (int m = 0; m < messages.Length; m++) - { - messages[m] = messages[m].Replace("[location" + (n + 1) + "]", locationName); - } - } - string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖"; - if (description != null) - { - descriptionWithoutReward = description; - description = description.Replace("[reward]", rewardText); - } - if (successMessage != null) { successMessage = successMessage.Replace("[reward]", rewardText); } - if (failureMessage != null) { failureMessage = failureMessage.Replace("[reward]", rewardText); } + descriptionWithoutReward = ReplaceVariablesInMissionMessage(description, sub, replaceReward: false); + description = ReplaceVariablesInMissionMessage(description, sub); + successMessage = ReplaceVariablesInMissionMessage(successMessage, sub); + failureMessage = ReplaceVariablesInMissionMessage(failureMessage, sub); for (int m = 0; m < messages.Length; m++) { - messages[m] = messages[m].Replace("[reward]", rewardText); + messages[m] = ReplaceVariablesInMissionMessage(messages[m], sub); } - Messages = messages.ToImmutableArray(); } + public LocalizedString ReplaceVariablesInMissionMessage(LocalizedString message, Submarine sub, bool replaceReward = true) + { + for (int locationIndex = 0; locationIndex < 2; locationIndex++) + { + string locationName = $"‖color:gui.orange‖{Locations[locationIndex].DisplayName}‖end‖"; + message = message.Replace("[location" + (locationIndex + 1) + "]", locationName); + } + if (replaceReward) + { + string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖"; + message = message.Replace("[reward]", rewardText); + } + return message; + } + public virtual void SetLevel(LevelData level) { } public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false) @@ -254,11 +253,30 @@ namespace Barotrauma return null; } - public virtual int GetReward(Submarine sub) + /// + /// Calculates the base reward, can be overridden for different mission types + /// + public virtual int GetBaseReward(Submarine sub) { return Prefab.Reward; } + /// + /// Calculates the available reward, taking into account universal modifiers such as campaign settings + /// + public int GetReward(Submarine sub) + { + int reward = GetBaseReward(sub); + + // Some modifiers should apply universally to all implementations of GetBaseReward + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + reward = (int)Math.Round(reward * campaign.Settings.MissionRewardMultiplier); + } + + return reward; + } + public void Start(Level level) { state = 0; @@ -353,7 +371,7 @@ namespace Barotrauma } if (GameMain.GameSession?.EventManager != null) { - var newEvent = eventPrefab.CreateInstance(); + var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); GameMain.GameSession.EventManager.ActivateEvent(newEvent); } } @@ -455,9 +473,13 @@ namespace Barotrauma foreach (var reputationReward in ReputationRewards) { + var reputationGainMultiplier = new AbilityMissionReputationGainMultiplier(this, 1f, character: null); + foreach (var c in crewCharacters) { c.CheckTalents(AbilityEffectType.OnCrewGainMissionReputation, reputationGainMultiplier); } + float amount = reputationReward.Amount * reputationGainMultiplier.Value; + if (reputationReward.FactionIdentifier == "location") { - OriginLocation.Reputation?.AddReputation(reputationReward.Amount); + OriginLocation.Reputation?.AddReputation(amount); TryGiveReputationForOpposingFaction(OriginLocation.Faction, reputationReward.AmountForOpposingFaction); } else @@ -465,7 +487,7 @@ namespace Barotrauma Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.FactionIdentifier); if (faction != null) { - faction.Reputation.AddReputation(reputationReward.Amount); + faction.Reputation.AddReputation(amount); TryGiveReputationForOpposingFaction(faction, reputationReward.AmountForOpposingFaction); } } @@ -664,5 +686,19 @@ namespace Barotrauma public Mission Mission { get; set; } public Character Character { get; set; } } + + class AbilityMissionReputationGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission, IAbilityCharacter + { + public AbilityMissionReputationGainMultiplier(Mission mission, float reputationMultiplier, Character character) + { + Value = reputationMultiplier; + 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 064527a38..96454bb6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -110,6 +110,7 @@ namespace Barotrauma public readonly int Reward; + // The titles and bodies of the popup messages during the mission, shown when the state of the mission changes. The order matters. public readonly ImmutableArray Headers; public readonly ImmutableArray Messages; @@ -187,23 +188,25 @@ namespace Barotrauma Tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet(); - string nameTag = element.GetAttributeString("name", ""); - Name = TextManager.Get($"MissionName.{TextIdentifier}"); - if (!string.IsNullOrEmpty(nameTag)) - { - Name = Name - .Fallback(TextManager.Get(nameTag)) - .Fallback(nameTag); - } + Name = GetText(element.GetAttributeString("name", ""), "MissionName"); + Description = GetText(element.GetAttributeString("description", ""), "MissionDescription"); - string descriptionTag = element.GetAttributeString("description", ""); - Description = - TextManager.Get($"MissionDescription.{TextIdentifier}"); - if (!string.IsNullOrEmpty(descriptionTag)) + LocalizedString GetText(string textTag, string textTagPrefix) { - Description = Description - .Fallback(TextManager.Get(descriptionTag)) - .Fallback(descriptionTag); + if (string.IsNullOrEmpty(textTag)) + { + return TextManager.Get($"{textTagPrefix}.{TextIdentifier}"); + } + else + { + return + //prefer finding a text based on the specific text tag defined in the mission config + TextManager.Get(textTag) + //2nd option: the "default" format (MissionName.SomeMission) + .Fallback(TextManager.Get($"{textTagPrefix}.{TextIdentifier}")) + //last option: use the text in the xml as-is with no localization + .Fallback(textTag); + } } Reward = element.GetAttributeInt("reward", 1); @@ -372,6 +375,12 @@ namespace Barotrauma DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - mission type cannot be none."); return; } +#if DEBUG + if (Type == MissionType.Monster && SonarLabel.IsNullOrEmpty()) + { + DebugConsole.AddWarning($"Potential error in mission prefab \"{Identifier}\" - sonar label not set."); + } +#endif if (CoOpMissionClasses.ContainsKey(Type)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index e87e70696..1819a821b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -68,7 +68,7 @@ namespace Barotrauma } } - public override int GetReward(Submarine sub) + public override int GetBaseReward(Submarine sub) { return alternateReward; } @@ -262,13 +262,13 @@ namespace Barotrauma enemySub.EnableMaintainPosition(); enemySub.TeamID = CharacterTeamType.None; //make the enemy sub withstand atleast the same depth as the player sub - enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth); + enemySub.SetCrushDepth(Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth)); if (Level.Loaded != null) { //...and the depth of the patrol positions + 1000 m foreach (var patrolPos in patrolPositions) { - enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Level.Loaded.GetRealWorldDepth(patrolPos.Y) + 1000); + enemySub.SetCrushDepth(Math.Max(enemySub.RealWorldCrushDepth, Level.Loaded.GetRealWorldDepth(patrolPos.Y) + 1000)); } } enemySub.ImmuneToBallastFlora = true; @@ -394,11 +394,11 @@ namespace Barotrauma DebugConsole.NewMessage("Patrol pos: " + patrolPos); } #endif + enemySub.SetPosition(spawnPos); if (!IsClient) { InitPirateShip(); } - enemySub.SetPosition(spawnPos); // flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections in the submarine // creating the pirates has to be done after the sub has been flipped, or it seems to break the AI pathing diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index c8184730c..8b3059af8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -10,10 +10,13 @@ namespace Barotrauma { partial class SalvageMission : Mission { - private class Target { public Item Item; + /// + /// The target this item spawns inside (usually a crate for example). + /// + public Target ParentTarget; /// /// Note that the integer values matter here: @@ -29,14 +32,20 @@ namespace Barotrauma } public readonly ItemPrefab ItemPrefab; + /// + /// Where the target can be spawned to. E.g. MainPath or Wreck. + /// public readonly Level.PositionType SpawnPositionType; public readonly Identifier ContainerTag; public readonly Identifier ExistingItemTag; - + public readonly bool RemoveItem; public readonly LocalizedString SonarLabel; + /// + /// Can the mission continue before this target has been retrieved? Can be used if you want the targets to be retrieved in a specific order. + /// public readonly bool AllowContinueBeforeRetrieved; /// @@ -51,6 +60,13 @@ namespace Barotrauma { get { + //if placing the item inside the parent (e.g. some item inside a crate) failed, + //consider this item retrieved (= essentially ignoring the item, it's not necessary to retrieve) + if (PlacingInsideParentTargetFailed) + { + return true; + } + return RequiredRetrievalState switch { RetrievalState.None => true, @@ -78,20 +94,29 @@ namespace Barotrauma public bool Interacted; private readonly SalvageMission mission; + public readonly bool RequireInsideOriginalContainer; + public Item OriginalContainer; + + /// + /// Means that the item could not be placed inside the container it was intended to spawn inside (probably meaning the mission has been misconfigured to e.g. spawn more items inside a crate than what the crate can hold). + /// + public bool PlacingInsideParentTargetFailed; /// /// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list. /// public readonly List> StatusEffects = new List>(); - public Target(ContentXElement element, SalvageMission mission) + public Target(ContentXElement element, SalvageMission mission, Target parentTarget) { this.mission = mission; + ParentTarget = parentTarget; ContainerTag = element.GetAttributeIdentifier("containertag", Identifier.Empty); - RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", RetrievalState.RetrievedToSub); - AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", false); - HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", false); - + RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", parentTarget?.RequiredRetrievalState ?? RetrievalState.RetrievedToSub); + AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", parentTarget != null); + HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", parentTarget?.HideLabelAfterRetrieved ?? false); + RequireInsideOriginalContainer = element.GetAttributeBool("requireinsideoriginalcontainer", false); + string sonarLabelTag = element.GetAttributeString("sonarlabel", ""); if (!string.IsNullOrEmpty(sonarLabelTag)) { @@ -126,6 +151,7 @@ namespace Barotrauma if (ItemPrefab == null) { string itemTag = element.GetAttributeString("itemtag", ""); + //NOTE: using unsynced random here is fine, the clients receive the info of what item spawned from the server ItemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; } if (ItemPrefab == null && ExistingItemTag.IsEmpty) @@ -135,7 +161,7 @@ namespace Barotrauma } } - SpawnPositionType = element.GetAttributeEnum("spawntype", Level.PositionType.Cave | Level.PositionType.Ruin); + SpawnPositionType = element.GetAttributeEnum("spawntype", parentTarget?.SpawnPositionType ?? (Level.PositionType.Cave | Level.PositionType.Ruin)); foreach (var subElement in element.Elements()) { @@ -149,12 +175,15 @@ namespace Barotrauma break; } case "chooserandom": - StatusEffects.Add(new List()); - foreach (var effectElement in subElement.Elements()) + if (subElement.Elements().Any(static e => e.NameAsIdentifier() == "statuseffect")) { - var newEffect = StatusEffect.Load(effectElement, parentDebugName: mission.Prefab.Name.Value); - if (newEffect == null) { continue; } - StatusEffects.Last().Add(newEffect); + StatusEffects.Add(new List()); + foreach (var effectElement in subElement.Elements()) + { + var newEffect = StatusEffect.Load(effectElement, parentDebugName: mission.Prefab.Name.Value); + if (newEffect == null) { continue; } + StatusEffects.Last().Add(newEffect); + } } break; } @@ -170,7 +199,24 @@ namespace Barotrauma private readonly List targets = new List(); - public bool AnyTargetNeedsToBeRetrievedToSub => targets.Any(t => t.RequiredRetrievalState == Target.RetrievalState.RetrievedToSub && !t.Retrieved); + /// + /// What percentage of targets need to be retrieved for the mission to complete (0.0 - 1.0). Defaults to 0.98. + /// + private readonly float requiredDeliveryAmount; + + /// + /// Message displayed when at least one of the targets is retrieved, but the mission is not complete yet. + /// + private LocalizedString partiallyRetrievedMessage; + + /// + /// Message displayed when all targets have been retrieved. + /// + private LocalizedString allRetrievedMessage; + + public bool AnyTargetNeedsToBeRetrievedToSub => targets.Any(static t => t.RequiredRetrievalState == Target.RetrievalState.RetrievedToSub && !t.Retrieved); + + private readonly MTRandom rng; public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { @@ -179,8 +225,23 @@ namespace Barotrauma foreach (var target in targets) { if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; } - if (target.Item != null) + if (target.Item != null && !target.Item.Removed) { + if (target.Item.ParentInventory?.Owner is Item parentItem) + { + bool insideParentItem = false; + foreach (var parentTarget in targets) + { + if (parentTarget.Item == parentItem && !parentTarget.SonarLabel.IsNullOrEmpty()) + { + insideParentItem = true; + break; + } + } + //if the item is inside another target that has it's own sonar label, no need to show one on this item + if (insideParentItem) { continue; } + } + yield return ( target.SonarLabel ?? Prefab.SonarLabel, target.Item.GetRootInventoryOwner()?.WorldPosition ?? target.Item.WorldPosition); @@ -193,17 +254,82 @@ namespace Barotrauma public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { + requiredDeliveryAmount = prefab.ConfigElement.GetAttributeFloat(nameof(requiredDeliveryAmount), 0.98f); + + //LevelData may not be instantiated at this point, in that case use the name identifier of the location + rng = new MTRandom(ToolBox.StringToInt( + locations[0].LevelData?.Seed ?? locations[0].NameIdentifier.Value + + locations[1].LevelData?.Seed ?? locations[1].NameIdentifier.Value)); + + partiallyRetrievedMessage = GetMessage(nameof(partiallyRetrievedMessage)); + allRetrievedMessage = GetMessage(nameof(allRetrievedMessage)); + foreach (ContentXElement subElement in prefab.ConfigElement.Elements()) { - if (subElement.NameAsIdentifier() == "target") + if (subElement.NameAsIdentifier() == "target" || + subElement.NameAsIdentifier() == "chooserandom") { - targets.Add(new Target(subElement, this)); + LoadTarget(subElement, parentTarget: null); } + } if (!targets.Any()) { - targets.Add(new Target(prefab.ConfigElement, this)); + targets.Add(new Target(prefab.ConfigElement, this, parentTarget: null)); } + + LocalizedString GetMessage(string attributeName) + { + if (prefab.ConfigElement.GetAttribute(attributeName) != null) + { + string msgTag = prefab.ConfigElement.GetAttributeString(attributeName, string.Empty); + return ReplaceVariablesInMissionMessage(TextManager.Get(msgTag).Fallback(msgTag), sub); + } + return string.Empty; + } + } + + private void LoadTarget(ContentXElement element, Target parentTarget) + { + ContentXElement chosenElement = element; + if (element.NameAsIdentifier() == "chooserandom") + { + /* chooserandom in this context can be used to choose either between targets or status effects to apply to the target, + ensure we don't try to load a statuseffect as a "child target" */ + if (element.Elements().Any(static e => e.NameAsIdentifier() == "statuseffect")) + { + return; + } + //this needs to be deterministic, use RNG with a specific seed + chosenElement = element.Elements().ToList().GetRandom(rng); + } + + int amount = GetAmount(chosenElement); + for (int i = 0; i < amount; i++) + { + var target = new Target(chosenElement, this, parentTarget); + targets.Add(target); + foreach (ContentXElement subElement in chosenElement.Elements()) + { + LoadTarget(subElement, parentTarget: target); + } + } + } + + private int GetAmount(ContentXElement targetElement) + { + int amount = targetElement.GetAttributeInt("amount", 1); + int minAmount = targetElement.GetAttributeInt("minamount", amount); + int maxAmount = targetElement.GetAttributeInt("maxamount", amount); + + // if the amount is a range, pick a random value between minAmount and maxAmount + if (minAmount < maxAmount) + { + //this needs to be deterministic, use RNG with a specific seed + amount = rng.Next(minAmount, maxAmount + 1); + } + + return amount; } protected override void StartMissionSpecific(Level level) @@ -294,8 +420,16 @@ namespace Barotrauma continue; } target.Item = new Item(target.ItemPrefab, position, null); - target.Item.body.SetTransformIgnoreContacts(target.Item.body.SimPosition, target.Item.body.Rotation); - target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; +#if CLIENT + target.Item.HighlightColor = GUIStyle.Orange; + target.Item.ExternalHighlight = true; +#endif + target.Item.UpdateTransform(); + if (target.Item.CurrentHull == null) + { + //prevent the body from moving if it spawned outside the hulls (we don't want it e.g. falling to the bottom of a cave or into the abyss) + target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; + } } else if (target.RequiredRetrievalState == Target.RetrievalState.Interact) { @@ -344,6 +478,7 @@ namespace Barotrauma } if (validContainers.Any()) { + //NOTE: using unsynced random here is fine, clients don't run this logic but rely on where the server places the item var selectedContainer = validContainers.GetRandomUnsynced(); if (selectedContainer.Combine(target.Item, user: null)) { @@ -362,6 +497,40 @@ namespace Barotrauma new SpawnInfo(usedExistingItem, originalInventoryID, originalItemContainerIndex, originalSlotIndex, executedEffectIndices)); #endif } + + if (!IsClient) + { + // after spawning all the items from prefabs, need to find all targets where parentTarget is defined, and set the item inside parent target container (if applicable) + foreach (var target in targets) + { + if (target.ParentTarget == null) { continue; } + + if (target.Item == null) + { + DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)", + contentPackage: Prefab.ContentPackage); + continue; + } + + if (target.ParentTarget.Item == null) + { + DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (parent item was null)", + contentPackage: Prefab.ContentPackage); + continue; + } + + if (target.ParentTarget.Item.GetComponent() is ItemContainer container) + { + if (!container.Inventory.TryPutItem(target.Item, user: null)) + { + DebugConsole.ThrowError($"Error in salvage mission {Prefab.Identifier}: failed to put the item {target.Item.Name} inside {target.ParentTarget.Item.Name}.", + contentPackage: Prefab.ContentPackage); + target.PlacingInsideParentTargetFailed = true; + } + target.OriginalContainer = target.ParentTarget.Item; + } + } + } } protected override void UpdateMissionSpecific(float deltaTime) @@ -376,6 +545,7 @@ namespace Barotrauma if (IsClient) { return; } + bool atLeastOneTargetWasRetrieved = false; for (int i = 0; i < targets.Count; i++) { var target = targets[i]; @@ -388,6 +558,10 @@ namespace Barotrauma #endif return; } + + Entity rootInventoryOwner = target.Item.GetRootInventoryOwner(); + Submarine parentSub = target.Item.CurrentHull?.Submarine ?? rootInventoryOwner?.Submarine; + bool inPlayerSub = parentSub != null && parentSub.Info.Type == SubmarineType.Player; switch (target.State) { case Target.RetrievalState.None: @@ -401,16 +575,16 @@ namespace Barotrauma { TrySetRetrievalState(Target.RetrievalState.PickedUp); } + if (inPlayerSub) + { + TrySetRetrievalState(Target.RetrievalState.RetrievedToSub); + } } - break; case Target.RetrievalState.PickedUp: case Target.RetrievalState.RetrievedToSub: { - Entity rootInventoryOwner = target.Item.GetRootInventoryOwner(); - Submarine parentSub = target.Item.CurrentHull?.Submarine ?? rootInventoryOwner?.Submarine; - bool inPlayerSub = parentSub != null && parentSub.Info.Type == SubmarineType.Player; bool inPlayerInventory = false; bool playerInFriendlySub = false; if (rootInventoryOwner is Character character && character.TeamID == CharacterTeamType.Team1) @@ -441,33 +615,70 @@ namespace Barotrauma if (retrievalState < target.State || target.State == retrievalState) { return; } bool wasRetrieved = target.Retrieved; target.State = retrievalState; - //increment the mission state if the target became retrieved - if (!wasRetrieved && target.Retrieved) { State = i + 1; } + //increment the mission state if the target became retrieved + if (!wasRetrieved && target.Retrieved) + { + State = Math.Max(i + 1, State); + atLeastOneTargetWasRetrieved = true; + } } } +#if CLIENT + if (atLeastOneTargetWasRetrieved) + { + TryShowRetrievedMessage(); + } +#endif if (targets.All(t => t.Retrieved)) { State = targets.Count + 1; - } + } } protected override bool DetermineCompleted() { - return targets.All(t => t.State >= t.RequiredRetrievalState); + if (requiredDeliveryAmount < 1.0f) + { + return targets.Count(t => IsTargetRetrieved(t)) / (float)targets.Count >= requiredDeliveryAmount; + } + else + { + return targets.All(IsTargetRetrieved); + } + + static bool IsTargetRetrieved(Target target) + { + if (target.State < target.RequiredRetrievalState) { return false; } + if (target.RequireInsideOriginalContainer) + { + if (target.Item.ParentInventory != target.OriginalContainer?.OwnInventory) { return false; } + } + return true; + } } protected override void EndMissionSpecific(bool completed) { //consider failed (can't attempt again) if we picked up any of the items but failed to bring them out of the level failed = !completed && targets.Any(t => t.State >= Target.RetrievalState.PickedUp); + List targetsToRemove = new List(); foreach (var target in targets) { - if (target.RemoveItem) + if (target.RemoveItem || + /*remove the target if it's inside another target that's set to be removed (e.g. inside the crate it spawned in)*/ + targets.Any(t => t.RemoveItem && target.Item?.ParentInventory?.Owner as Item == t.Item)) { - target.Item?.Remove(); - target.Reset(); + targetsToRemove.Add(target); } } + foreach (var target in targetsToRemove) + { + if (target.Item != null && !target.Item.Removed) + { + target.Item.Remove(); + } + target.Reset(); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 187c8f919..e2c58bb74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -99,8 +99,8 @@ namespace Barotrauma } } - public MonsterEvent(EventPrefab prefab) - : base(prefab) + public MonsterEvent(EventPrefab prefab, int seed) + : base(prefab, seed) { string speciesFile = prefab.ConfigElement.GetAttributeString("characterfile", ""); CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesFile); @@ -173,9 +173,8 @@ namespace Barotrauma } } - public override void Init(EventSet parentSet) + protected override void InitEventSpecific(EventSet parentSet) { - base.Init(parentSet); if (parentSet != null && resetTime == 0) { // Use the parent reset time only if there's no reset time defined for the event. @@ -192,7 +191,7 @@ namespace Barotrauma int amount = Rand.Range(MinAmount, MaxAmount + 1); for (int i = 0; i < amount; i++) { - string seed = Level.Loaded.Seed + i.ToString(); + string seed = i.ToString() + Level.Loaded.Seed; Character createdCharacter = Character.Create(SpeciesName, Vector2.Zero, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true, throwErrorIfNotFound: false); if (createdCharacter == null) { @@ -271,14 +270,18 @@ namespace Barotrauma spawnPos = Vector2.Zero; var availablePositions = GetAvailableSpawnPositions(); chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); - bool isRuinOrWreck = SpawnPosType.HasFlag(Level.PositionType.Ruin) || SpawnPosType.HasFlag(Level.PositionType.Wreck); - if (affectSubImmediately && !isRuinOrWreck && !SpawnPosType.HasFlag(Level.PositionType.Abyss)) + bool isRuinOrWreckOrCave = + SpawnPosType.HasFlag(Level.PositionType.Ruin) || + SpawnPosType.HasFlag(Level.PositionType.Wreck) || + SpawnPosType.HasFlag(Level.PositionType.Cave) || + SpawnPosType.HasFlag(Level.PositionType.AbyssCave); + if (affectSubImmediately && !isRuinOrWreckOrCave && !SpawnPosType.HasFlag(Level.PositionType.Abyss)) { if (availablePositions.None()) { //no suitable position found, disable the event spawnPos = null; - Finish(); + disallowed = true; return; } Submarine refSub = GetReferenceSub(); @@ -348,7 +351,7 @@ namespace Barotrauma } else { - if (!isRuinOrWreck) + if (!isRuinOrWreckOrCave) { float minDistance = 20000; for (int i = 0; i < Submarine.MainSubs.Length; i++) @@ -361,7 +364,7 @@ namespace Barotrauma { //no suitable position found, disable the event spawnPos = null; - Finish(); + disallowed = true; return; } chosenPosition = availablePositions.GetRandomUnsynced(); @@ -371,21 +374,17 @@ namespace Barotrauma spawnPos = chosenPosition.Position.ToVector2(); if (chosenPosition.Submarine != null || chosenPosition.Ruin != null) { - bool ignoreSubmarine = chosenPosition.Ruin != null; - var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag, ignoreSubmarine: ignoreSubmarine); + var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag); if (spawnPoint != null) { - if (!ignoreSubmarine) - { - System.Diagnostics.Debug.Assert(spawnPoint.Submarine == chosenPosition.Submarine); - } + System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine)); spawnPos = spawnPoint.WorldPosition; } else { //no suitable position found, disable the event spawnPos = null; - Finish(); + disallowed = true; return; } } @@ -422,7 +421,7 @@ namespace Barotrauma { //no suitable position found, disable the event spawnPos = null; - Finish(); + disallowed = true; return; } } @@ -442,11 +441,7 @@ namespace Barotrauma public override void Update(float deltaTime) { - if (disallowed) - { - Finish(); - return; - } + if (disallowed) { return; } if (resetTimer > 0) { @@ -483,8 +478,8 @@ namespace Barotrauma } FindSpawnPosition(affectSubImmediately: true); - //the event gets marked as finished if a spawn point is not found - if (isFinished) { return; } + //the event gets marked as disallowed if a spawn point is not found + if (isFinished || disallowed) { return; } spawnPending = true; } @@ -493,7 +488,7 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(spawnPos.HasValue); if (spawnPos == null) { - Finish(); + disallowed = true; return; } //wait until there are no submarines at the spawnpos @@ -567,7 +562,7 @@ namespace Barotrauma if (CheckLineOfSight(from, to, chosenPosition.Submarine)) { // Line of sight to a player character -> don't spawn. Disable the event to prevent monsters "magically" spawning here. - Finish(); + disallowed = true; return; } } @@ -636,7 +631,7 @@ namespace Barotrauma scatterAmount = scatter; } } - else if (!SpawnPosType.HasFlag(Level.PositionType.MainPath)) + else if (SpawnPosType.IsIndoorsArea()) { scatterAmount = 0; } @@ -650,22 +645,46 @@ namespace Barotrauma if (GameMain.GameSession == null || Level.Loaded == null) { return; } if (monster.Removed) { return; } - + System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events."); - Vector2 pos = spawnPos.Value + Rand.Vector(scatterAmount); + Vector2 pos = spawnPos.Value; if (scatterAmount > 0) { - if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos))) + //try finding an offset position that's not inside a wall + int tries = 10; + do { - // Can't use the offset position, let's use the exact spawn position. - pos = spawnPos.Value; - } - else if (Level.Loaded.Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).ContainsWorld(pos))) - { - // Can't use the offset position, let's use the exact spawn position. - pos = spawnPos.Value; - } + tries--; + pos = spawnPos.Value + Rand.Vector(Rand.Range(0.0f, scatterAmount)); + + bool isValidPos = true; + if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos)) || + Level.Loaded.Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).ContainsWorld(pos)) || + Level.Loaded.IsPositionInsideWall(pos)) + { + isValidPos = false; + } + else if (SpawnPosType.HasFlag(Level.PositionType.Cave) || SpawnPosType.HasFlag(Level.PositionType.AbyssCave)) + { + //trying to spawn in a cave, but the position is not inside a cave -> not valid + if (Level.Loaded.Caves.None(c => c.Area.Contains(pos))) + { + isValidPos = false; + } + } + + if (isValidPos) + { + //not inside anything, all good! + break; + } + // This was the last try and couldn't find an offset position, let's use the exact spawn position. + if (tries == 0) + { + pos = spawnPos.Value; + } + } while (tries > 0); } monster.Enabled = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 517875dd4..68218dc1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -51,7 +51,7 @@ namespace Barotrauma return $"{nameof(ScriptedEvent)} ({prefab.Identifier})"; } - public ScriptedEvent(EventPrefab prefab) : base(prefab) + public ScriptedEvent(EventPrefab prefab, int seed) : base(prefab, seed) { foreach (var element in prefab.ConfigElement.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 794567a60..29d81df7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -35,12 +35,7 @@ namespace Barotrauma //spawn items in wrecks, beacon stations and pirate subs foreach (var sub in Submarine.Loaded) { - if (sub.Info.Type == SubmarineType.Player || - sub.Info.Type == SubmarineType.Outpost || - sub.Info.Type == SubmarineType.OutpostModule) - { - continue; - } + if (sub.Info.Type is SubmarineType.Player or SubmarineType.Outpost or SubmarineType.OutpostModule) { continue; } if (sub.Info.InitialSuppliesSpawned) { continue; } CreateAndPlace(sub.ToEnumerable()); sub.Info.InitialSuppliesSpawned = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 9f6cbf6dc..139ae6c5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -661,6 +661,10 @@ namespace Barotrauma #if SERVER Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif + if (item.GetComponent() is { Attached: true }) + { + item.Drop(dropper: null); + } if (!character.Inventory.TryPutItem(item, user: null, item.AllowedSlots)) { foreach (Item containedItem in character.Inventory.AllItemsMod) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 762807d0d..2b5183312 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -35,8 +35,6 @@ namespace Barotrauma private Character welcomeMessageNPC; - public List CharacterInfos => characterInfos; - public bool HasBots { get; set; } public class ActiveOrder @@ -74,8 +72,8 @@ namespace Barotrauma } // Ignore orders work a bit differently since the "unignore" order counters the "ignore" order - var isUnignoreOrder = order.Identifier == "unignorethis"; - var orderPrefab = !isUnignoreOrder ? order.Prefab : OrderPrefab.Prefabs["ignorethis"]; + var isUnignoreOrder = order.Identifier == Tags.UnignoreThis; + var orderPrefab = !isUnignoreOrder ? order.Prefab : OrderPrefab.Prefabs[Tags.IgnoreThis]; ActiveOrder existingOrder = ActiveOrders.Find(o => o.Order.Prefab == orderPrefab && MatchesTarget(o.Order.TargetEntity, order.TargetEntity) && (o.Order.TargetType != Order.OrderTargetType.WallSection || o.Order.WallSectionIndex == order.WallSectionIndex)); @@ -95,6 +93,23 @@ namespace Barotrauma } else if (!isUnignoreOrder) { + if (order.IsDeconstructOrder) + { + if (order.TargetEntity is Item item) + { + if (order.Identifier == Tags.DeconstructThis) + { + Item.DeconstructItems.Add(item); +#if CLIENT + HintManager.OnItemMarkedForDeconstruction(order.OrderGiver); +#endif + } + else + { + Item.DeconstructItems.Remove(item); + } + } + } ActiveOrders.Add(new ActiveOrder(order, fadeOutTime)); #if CLIENT HintManager.OnActiveOrderAdded(order); @@ -198,6 +213,11 @@ namespace Barotrauma } } + public bool IsFired(Character character) + { + return !GetCharacterInfos().Contains(character.Info); + } + /// /// Remove the character from the crew (and crew menus). /// @@ -237,6 +257,11 @@ namespace Barotrauma characterInfos.Add(characterInfo); } + public void ClearCharacterInfos() + { + characterInfos.Clear(); + } + public void InitRound() { #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 374768a3b..d66c8c079 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -147,6 +147,8 @@ namespace Barotrauma public bool DivingSuitWarningShown; + public bool ItemsRelocatedToMainSub; + private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions) { if (GameMain.NetworkMember == null) { return true; } @@ -878,7 +880,7 @@ namespace Barotrauma } } - foreach (CharacterInfo ci in CrewManager.CharacterInfos.ToList()) + foreach (CharacterInfo ci in CrewManager.GetCharacterInfos().ToList()) { if (ci.CauseOfDeath != null) { @@ -979,7 +981,7 @@ namespace Barotrauma Preset?.Identifier.Value ?? "none"); string eventId = "FinishCampaign:"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); - GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0)); GameAnalyticsManager.AddDesignEvent(eventId + "Money", Bank.Balance); GameAnalyticsManager.AddDesignEvent(eventId + "Playtime", TotalPlayTime); GameAnalyticsManager.AddDesignEvent(eventId + "PassedLevels", TotalPassedLevels); @@ -1024,7 +1026,7 @@ namespace Barotrauma return ToolBox.SelectWeightedRandom(factionsList, weights, random); } - public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Character hirer, Client client = null) + public bool TryHireCharacter(Location location, CharacterInfo characterInfo, bool takeMoney = true, Client client = null) { if (characterInfo == null) { return false; } if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) @@ -1034,8 +1036,8 @@ namespace Barotrauma return false; } } + if (takeMoney && !TryPurchase(client, HireManager.GetSalaryFor(characterInfo))) { return false; } - if (!TryPurchase(client, HireManager.GetSalaryFor(characterInfo))) { return false; } characterInfo.IsNewHire = true; characterInfo.Title = null; location.RemoveHireableCharacter(characterInfo); @@ -1047,7 +1049,6 @@ namespace Barotrauma private void NPCInteract(Character npc, Character interactor) { if (!npc.AllowCustomInteract) { return; } - GameAnalyticsManager.AddDesignEvent("CampaignInteraction:" + Preset.Identifier + ":" + npc.CampaignInteractionType); NPCInteractProjSpecific(npc, interactor); string coroutineName = "DoCharacterWait." + (npc?.ID ?? Entity.NullEntityID); if (!CoroutineManager.IsCoroutineRunning(coroutineName)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs index fd5297d89..1ab312380 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs @@ -8,7 +8,7 @@ namespace Barotrauma internal static class CampaignModePresets { public static readonly ImmutableArray List; - public static readonly ImmutableDictionary Definitions; + private static readonly ImmutableDictionary definitions; private static readonly string fileListPath = Path.Combine("Data", "campaignsettings.xml"); @@ -20,73 +20,58 @@ namespace Barotrauma return; } - List list = new List(); - Dictionary definitions = new Dictionary(); + List presetList = new List(); + Dictionary tempDefinitions = new Dictionary(); foreach (XElement element in docRoot.Elements()) { Identifier name = element.NameAsIdentifier(); + // The campaign setting presets if (name == CampaignSettings.LowerCaseSaveElementName) { - list.Add(new CampaignSettings(element)); + presetList.Add(new CampaignSettings(element)); } + // All the definitions for the setting value options else if (name == nameof(CampaignSettingDefinitions)) { + // The single definitions that the settings may refer to (eg. PatdownProbabilityMin) foreach (XElement subElement in element.Elements()) { - definitions.Add(subElement.NameAsIdentifier(), new CampaignSettingDefinitions(subElement)); + tempDefinitions.Add(subElement.NameAsIdentifier(), new CampaignSettingDefinitions(subElement)); } } } - List = list.ToImmutableArray(); - Definitions = definitions.ToImmutableDictionary(); + List = presetList.ToImmutableArray(); + definitions = tempDefinitions.ToImmutableDictionary(); + } + + public static bool TryGetAttribute(Identifier propertyName, Identifier attributeName, out XAttribute attribute) + { + attribute = null; + if (definitions.TryGetValue(propertyName, out CampaignSettingDefinitions definition)) + { + if (definition.Attributes.TryGetValue(attributeName, out XAttribute att)) + { + attribute = att; + return true; + } + } + return false; } } internal readonly struct CampaignSettingDefinitions { - // Definitely not the best way to do this - private readonly ImmutableDictionary> values; + public readonly ImmutableDictionary Attributes; public CampaignSettingDefinitions(XElement element) { - var definitions = new Dictionary>(); - foreach (XAttribute attribute in element.Attributes()) - { - Identifier name = attribute.NameAsIdentifier(); - if (attribute.Value.Contains('.')) - { - definitions.Add(name, element.GetAttributeFloat(name.Value, 0)); - } - else - { - definitions.Add(name, element.GetAttributeInt(name.Value, 0)); - } - } - - values = definitions.ToImmutableDictionary(); - } - - public float GetFloat(Identifier identifier) - { - float range = 0; - if (!values.TryGetValue(identifier, out Either value) || !value.TryGet(out range)) - { - DebugConsole.ThrowError($"CampaignSettings: Can't find value for {identifier}"); - } - return range; - } - - public int GetInt(Identifier identifier) - { - int integer = 0; - if (!values.TryGetValue(identifier, out Either value) || !value.TryGet(out integer)) - { - DebugConsole.ThrowError($"CampaignSettings: Can't find value for {identifier}"); - } - return integer; + Attributes = element.Attributes().ToImmutableDictionary( + a => a.NameAsIdentifier(), + a => a + ); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index a9f757191..ffafbc542 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Microsoft.Xna.Framework; using System.Collections.Generic; @@ -27,6 +27,10 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), NetworkSerialize] public bool RadiationEnabled { get; set; } + public const int DefaultMaxMissionCount = 2; + public const int MaxMissionCountLimit = 10; + public const int MinMissionCountLimit = 1; + private int maxMissionCount; [Serialize(DefaultMaxMissionCount, IsPropertySaveable.Yes), NetworkSerialize(MinValueInt = MinMissionCountLimit, MaxValueInt = MaxMissionCountLimit)] @@ -38,60 +42,200 @@ namespace Barotrauma public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount(); - [Serialize(StartingBalanceAmount.Medium, IsPropertySaveable.Yes), NetworkSerialize] - public StartingBalanceAmount StartingBalanceAmount { get; set; } - - [Serialize(GameDifficulty.Medium, IsPropertySaveable.Yes), NetworkSerialize] - public GameDifficulty Difficulty { get; set; } + [Serialize(WorldHostilityOption.Medium, IsPropertySaveable.Yes), NetworkSerialize] + public WorldHostilityOption WorldHostility { get; set; } [Serialize("normal", IsPropertySaveable.Yes), NetworkSerialize] public Identifier StartItemSet { get; set; } + [Serialize(StartingBalanceAmountOption.Medium, IsPropertySaveable.Yes), NetworkSerialize] + public StartingBalanceAmountOption StartingBalanceAmount { get; set; } + + private int? _initialMoney; + public const int DefaultInitialMoney = 8000; + public int InitialMoney { get { - if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition)) + if (_initialMoney is int alreadyCachedValue) { - return definition.GetInt(StartingBalanceAmount.ToIdentifier()); + return alreadyCachedValue; + } + else + { + _initialMoney = DefaultInitialMoney; + Identifier settingDefinitionIdentifier = nameof(StartingBalanceAmount).ToIdentifier(); + Identifier attributeIdentifier = StartingBalanceAmount.ToIdentifier(); + if (CampaignModePresets.TryGetAttribute(settingDefinitionIdentifier, attributeIdentifier, out XAttribute? attribute)) + { + _initialMoney = attribute.GetAttributeInt(DefaultInitialMoney); + } + else + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {attributeIdentifier} in {settingDefinitionIdentifier}"); + } + return _initialMoney ?? DefaultInitialMoney; } - return 8000; } } + private float? _extraEventManagerDifficulty; + private const float defaultExtraEventManagerDifficulty = 0; + public float ExtraEventManagerDifficulty { get { - if (CampaignModePresets.Definitions.TryGetValue(nameof(ExtraEventManagerDifficulty).ToIdentifier(), out var definition)) + if (_extraEventManagerDifficulty is float alreadyCachedValue) { - return definition.GetFloat(Difficulty.ToIdentifier()); + return alreadyCachedValue; + } + else + { + _extraEventManagerDifficulty = defaultExtraEventManagerDifficulty; + Identifier settingDefinitionIdentifier = nameof(ExtraEventManagerDifficulty).ToIdentifier(); + Identifier attributeIdentifier = WorldHostility.ToIdentifier(); + if (CampaignModePresets.TryGetAttribute(settingDefinitionIdentifier, attributeIdentifier, out XAttribute? attribute)) + { + _extraEventManagerDifficulty = attribute.GetAttributeFloat(defaultExtraEventManagerDifficulty); + } + else + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {attributeIdentifier} in {settingDefinitionIdentifier}"); + } + return _extraEventManagerDifficulty ?? defaultExtraEventManagerDifficulty; } - return 0; } } + private float? _levelDifficultyMultiplier; + private const float defaultLevelDifficultyMultiplier = 1.0f; + public float LevelDifficultyMultiplier { get { - if (CampaignModePresets.Definitions.TryGetValue(nameof(LevelDifficultyMultiplier).ToIdentifier(), out var definition)) + if (_levelDifficultyMultiplier is float alreadyCachedValue) { - return definition.GetFloat(Difficulty.ToIdentifier()); + return alreadyCachedValue; + } + else + { + _levelDifficultyMultiplier = defaultLevelDifficultyMultiplier; + Identifier settingDefinitionIdentifier = nameof(LevelDifficultyMultiplier).ToIdentifier(); + Identifier attributeIdentifier = WorldHostility.ToIdentifier(); + if (CampaignModePresets.TryGetAttribute(settingDefinitionIdentifier, attributeIdentifier, out XAttribute? attribute)) + { + _levelDifficultyMultiplier = attribute.GetAttributeFloat(defaultLevelDifficultyMultiplier); + } + else + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {attributeIdentifier} in {settingDefinitionIdentifier}"); + } + return _levelDifficultyMultiplier ?? defaultLevelDifficultyMultiplier; } - return 1.0f; } } - [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; } + private static readonly Dictionary _multiplierSettings = new Dictionary + { + { "default", new MultiplierSettings { Min = 0.2f, Max = 2.0f, Step = 0.1f } }, + { nameof(CrewVitalityMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } }, + { nameof(NonCrewVitalityMultiplier),new MultiplierSettings { Min = 0.5f, Max = 3.0f, Step = 0.1f } }, + { nameof(MissionRewardMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } }, + { nameof(RepairFailMultiplier), new MultiplierSettings { Min = 0.5f, Max = 5.0f, Step = 0.5f } }, + { nameof(ShopPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } }, + { nameof(ShipyardPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } } + // Add overrides for default values here + }; - [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; } + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float CrewVitalityMultiplier { get; set; } - public const int DefaultMaxMissionCount = 2; - public const int MaxMissionCountLimit = 10; - public const int MinMissionCountLimit = 1; + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float NonCrewVitalityMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float OxygenMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float FuelMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float MissionRewardMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float ShopPriceMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float ShipyardPriceMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float RepairFailMultiplier { get; set; } + + [Serialize(true, IsPropertySaveable.Yes), NetworkSerialize] + public bool ShowHuskWarning { get; set; } + + [Serialize(PatdownProbabilityOption.Medium, IsPropertySaveable.Yes), NetworkSerialize] + public PatdownProbabilityOption PatdownProbability { get; set; } + + private float? _minPatdownProbability; + private float? _maxPatdownProbability; + public const float DefaultMinPatdownProbability = 0.2f; + public const float DefaultMaxPatdownProbability = 0.9f; + + public float PatdownProbabilityMin + { + get + { + if (_minPatdownProbability is float alreadyCachedValue) + { + return alreadyCachedValue; + } + else + { + _minPatdownProbability = DefaultMinPatdownProbability; + Identifier settingDefinitionIdentifier = nameof(PatdownProbabilityMin).ToIdentifier(); + Identifier attributeIdentifier = PatdownProbability.ToIdentifier(); + if (CampaignModePresets.TryGetAttribute(settingDefinitionIdentifier, attributeIdentifier, out XAttribute? attribute)) + { + _minPatdownProbability = attribute.GetAttributeFloat(DefaultMinPatdownProbability); + } + else + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {attributeIdentifier} in {settingDefinitionIdentifier}"); + } + return _minPatdownProbability ?? DefaultMinPatdownProbability; + } + } + } + + public float PatdownProbabilityMax + { + get + { + if (_maxPatdownProbability is float alreadyCachedValue) + { + return alreadyCachedValue; + } + else + { + _maxPatdownProbability = DefaultMaxPatdownProbability; + Identifier settingDefinitionIdentifier = nameof(PatdownProbabilityMax).ToIdentifier(); + Identifier attributeIdentifier = PatdownProbability.ToIdentifier(); + if (CampaignModePresets.TryGetAttribute(settingDefinitionIdentifier, attributeIdentifier, out XAttribute? attribute)) + { + _maxPatdownProbability = attribute.GetAttributeFloat(DefaultMaxPatdownProbability); + } + else + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {attributeIdentifier} in {settingDefinitionIdentifier}"); + } + return _maxPatdownProbability ?? DefaultMaxPatdownProbability; + } + } + } public Dictionary SerializableProperties { get; private set; } @@ -119,5 +263,22 @@ namespace Barotrauma if (!characters.Any()) { return 0; } return characters.Max(static character => (int)character.GetStatValue(StatTypes.ExtraMissionCount)); } + + public struct MultiplierSettings + { + public float Min { get; set; } + public float Max { get; set; } + public float Step { get; set; } + } + + public static MultiplierSettings GetMultiplierSettings(string multiplierName) + { + if (_multiplierSettings.TryGetValue(multiplierName, out MultiplierSettings value)) + { + return value; + } + + return _multiplierSettings["default"]; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index aa06066e2..1037d9607 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -390,11 +390,27 @@ namespace Barotrauma .Where(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)) .GetRandom(rand); dummyLocations = CreateDummyLocations(levelSeed, locationType); - if (!mission.Prefab.RequiredLocationFaction.IsEmpty && - FactionPrefab.Prefabs.TryGet(mission.Prefab.RequiredLocationFaction, out var factionPrefab)) + + if (!tryCreateFaction(mission.Prefab.RequiredLocationFaction, dummyLocations, static (loc, fac) => loc.Faction = fac)) { - dummyLocations[0].Faction = dummyLocations[1].Faction = new Faction(metadata: null, factionPrefab); + tryCreateFaction(locationType.Faction, dummyLocations, static (loc, fac) => loc.Faction = fac); + tryCreateFaction(locationType.SecondaryFaction, dummyLocations, static (loc, fac) => loc.SecondaryFaction = fac); } + static bool tryCreateFaction(Identifier factionIdentifier, Location[] locations, Action setter) + { + if (factionIdentifier.IsEmpty) { return false; } + if (!FactionPrefab.Prefabs.TryGet(factionIdentifier, out var prefab)) { return false; } + if (locations.Length == 0) { return false; } + + var newFaction = new Faction(metadata: null, prefab); + for (int i = 0; i < locations.Length; i++) + { + setter(locations[i], newFaction); + } + + return true; + } + randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true); break; } @@ -473,6 +489,11 @@ namespace Barotrauma } foreach (Item item in items) { + if (item.GetComponent() is { } cb) + { + cb.Locked = true; + } + Wire wire = item.GetComponent(); if (wire != null && !wire.NoAutoLock && wire.Connections.Any(c => c != null)) { wire.Locked = true; } } @@ -498,7 +519,7 @@ namespace Barotrauma string eventId = "StartRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier.Value ?? "none")); - GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0)); foreach (Mission mission in missions) { GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier); @@ -523,16 +544,24 @@ namespace Barotrauma GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}"); #endif var campaignMode = GameMode as CampaignMode; - if (campaignMode != null) - { - if (campaignMode.Map?.Radiation != null && campaignMode.Map.Radiation.Enabled) - { - GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Enabled"); - } - else - { - GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Disabled"); - } + if (campaignMode != null) + { + GameAnalyticsManager.AddDesignEvent("CampaignSettings:RadiationEnabled:" + campaignMode.Settings.RadiationEnabled); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:WorldHostility:" + campaignMode.Settings.WorldHostility); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShowHuskWarning:" + campaignMode.Settings.ShowHuskWarning); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:StartItemSet:" + campaignMode.Settings.StartItemSet); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:MaxMissionCount:" + campaignMode.Settings.MaxMissionCount); + //log the multipliers as integers to reduce the number of distinct values + GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:FuelMultiplier:" + (int)(campaignMode.Settings.FuelMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:MissionRewardMultiplier:" + (int)(campaignMode.Settings.MissionRewardMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:CrewVitalityMultiplier:" + (int)(campaignMode.Settings.CrewVitalityMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:NonCrewVitalityMultiplier:" + (int)(campaignMode.Settings.NonCrewVitalityMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:OxygenMultiplier:" + (int)(campaignMode.Settings.OxygenMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShipyardPriceMultiplier:" + (int)(campaignMode.Settings.ShipyardPriceMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShopPriceMultiplier:" + (int)(campaignMode.Settings.ShopPriceMultiplier * 100)); + bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData!.Biome); if (firstTimeInBiome) { @@ -593,6 +622,19 @@ namespace Barotrauma HintManager.OnRoundStarted(); EnableEventLogNotificationIcon(enabled: false); #endif + if (campaignMode is { ItemsRelocatedToMainSub: true }) + { +#if SERVER + GameMain.Server.SendChatMessage(TextManager.Get("itemrelocated").Value, ChatMessageType.ServerMessageBoxInGame); +#else + if (campaignMode.IsSinglePlayer) + { + new GUIMessageBox(string.Empty, TextManager.Get("itemrelocated")); + } +#endif + campaignMode.ItemsRelocatedToMainSub = false; + } + EventManager?.EventLog?.Clear(); if (campaignMode is { DivingSuitWarningShown: false } && Level.Loaded != null && Level.Loaded.GetRealWorldDepth(0) > 4000) @@ -1002,46 +1044,54 @@ namespace Barotrauma public void LogEndRoundStats(string eventId, TraitorManager.TraitorResults? traitorResults = null) { - GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration); + if (Submarine.MainSub?.Info?.IsVanillaSubmarine() ?? false) + { + //don't log modded subs, that's a ton of extra data to collect + GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration); + } GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count ?? 0), RoundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0), RoundDuration); foreach (Mission mission in missions) { GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), RoundDuration); } - if (Level.Loaded != null) + if (!ContentPackageManager.ModsEnabled) { - Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ? - Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : - Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier(); - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), RoundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration); - } - - if (Submarine.MainSub != null) - { - Dictionary submarineInventory = new Dictionary(); - foreach (Item item in Item.ItemList) + if (Level.Loaded != null) { - var rootContainer = item.RootContainer ?? item; - if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; } - if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; } + Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ? + Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : + Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier(); + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), RoundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration); + } - var holdable = item.GetComponent(); - if (holdable == null || holdable.Attached) { continue; } - var wire = item.GetComponent(); - if (wire != null && wire.Connections.Any(c => c != null)) { continue; } - - if (!submarineInventory.ContainsKey(item.Prefab)) + //disabled for now, we're collecting too many events and this is information we don't need atm + /*if (Submarine.MainSub != null) + { + Dictionary submarineInventory = new Dictionary(); + foreach (Item item in Item.ItemList) { - submarineInventory.Add(item.Prefab, 0); + var rootContainer = item.RootContainer ?? item; + if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; } + if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; } + + var holdable = item.GetComponent(); + if (holdable == null || holdable.Attached) { continue; } + var wire = item.GetComponent(); + if (wire != null && wire.Connections.Any(c => c != null)) { continue; } + + if (!submarineInventory.ContainsKey(item.Prefab)) + { + submarineInventory.Add(item.Prefab, 0); + } + submarineInventory[item.Prefab]++; } - submarineInventory[item.Prefab]++; - } - foreach (var subItem in submarineInventory) - { - GameAnalyticsManager.AddDesignEvent(eventId + "SubmarineInventory:" + subItem.Key.Identifier, subItem.Value); - } + foreach (var subItem in submarineInventory) + { + GameAnalyticsManager.AddDesignEvent(eventId + "SubmarineInventory:" + subItem.Key.Identifier, subItem.Value); + } + }*/ } if (traitorResults.HasValue) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index fd7a6fcc9..887028517 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -278,6 +278,20 @@ namespace Barotrauma } } + /// + /// Used for purchasing upgrades from outside the upgrade store. + /// Doesn't deduct the credit, adds the upgrade to the pending list and performs a level sanity check. + /// + public void AddUpgradeExternally(UpgradePrefab prefab, UpgradeCategory category, int level) + { + int maxLevel = prefab.GetMaxLevelForCurrentSub(); + int currentLevel = GetUpgradeLevel(prefab, category); + if (currentLevel + 1 > maxLevel) { return; } + + PendingUpgrades.Add(new PurchasedUpgrade(prefab, category, level)); + OnUpgradesChanged?.Invoke(this); + } + /// /// Purchases an item swap and handles logic for deducting the credit. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index 911fc6a4e..6de40b6ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -10,12 +10,14 @@ namespace Barotrauma Run, Crouch, InfoTab, Chat, RadioChat, CrewOrders, Ragdoll, Health, Grab, + DropItem, SelectNextCharacter, SelectPreviousCharacter, Voice, RadioVoice, LocalVoice, Deselect, Shoot, Command, + ContextualCommand, ToggleInventory, TakeOneFromInventorySlot, TakeHalfFromInventorySlot, @@ -23,6 +25,7 @@ namespace Barotrauma PreviousFireMode, ActiveChat, ToggleChatMode, - ChatBox + ChatBox, + ShowInteractionLabels } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 81ecc5a01..bfef76f61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -326,6 +326,8 @@ namespace Barotrauma.Items.Components ConnectWireBetweenPorts(); CreateJoint(true); + item.SendSignal("1", "on_dock"); + DockingTarget.Item.SendSignal("1", "on_dock"); #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) @@ -655,7 +657,8 @@ namespace Barotrauma.Items.Components hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); hulls[i] = new Hull(hullRects[i], subs[i]) { - RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch" + RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch", + AvoidStaying = true }; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); @@ -974,6 +977,7 @@ namespace Barotrauma.Items.Components item.linkedTo.Clear(); docked = false; + item.SendSignal("1", "on_undock"); Item.Submarine.RefreshOutdoorNodes(); Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 041465193..08e2ba6b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -165,6 +165,8 @@ namespace Barotrauma.Items.Components public bool IsHorizontal { get; private set; } + public bool IsConvexHullHorizontal => autoOrientGap && linkedGap != null ? !linkedGap.IsHorizontal : IsHorizontal; + [Serialize("0.0,0.0,0.0,0.0", IsPropertySaveable.No, description: "Position and size of the window on the door. The upper left corner is 0,0. Set the width and height to 0 if you don't want the door to have a window.")] public Rectangle Window { get; set; } @@ -321,7 +323,7 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { if (item.Condition < RepairThreshold && item.GetComponent().HasRequiredItems(picker, addMessage: false)) { return true; } - if (requiredItems.None()) { return false; } + if (RequiredItems.None()) { return false; } if (HasAccess(picker) && HasRequiredItems(picker, false)) { return false; } return base.Pick(picker); } @@ -559,6 +561,7 @@ namespace Barotrauma.Items.Components public void RefreshLinkedGap() { + LinkedGap.Layer = item.Layer; LinkedGap.ConnectedDoor = this; if (autoOrientGap) { @@ -572,10 +575,10 @@ namespace Barotrauma.Items.Components { RefreshLinkedGap(); #if CLIENT - convexHull = new ConvexHull(doorRect, IsHorizontal, item); + convexHull = new ConvexHull(doorRect, IsConvexHullHorizontal, item); if (Window != Rectangle.Empty) { - convexHull2 = new ConvexHull(doorRect, IsHorizontal, item); + convexHull2 = new ConvexHull(doorRect, IsConvexHullHorizontal, item); } UpdateConvexHulls(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 846e92df5..8395c7c58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -77,6 +77,9 @@ namespace Barotrauma.Items.Components { Stem = 0b0000, CrossJunction = 0b1111, + HorizontalLine = 0b1010, + VerticalLine = 0b0101, + /*backwards compatibility, the vertical and horizontal "lane" used to be backwards*/ VerticalLane = 0b1010, HorizontalLane = 0b0101, TurnTopRight = 0b1001, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index bc06c7c4b..a7671a642 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -32,11 +32,6 @@ namespace Barotrauma.Items.Components private LocalizedString prevMsg; private Dictionary> prevRequiredItems; - //the distance from the holding characters elbow to center of the physics body of the item - protected Vector2 holdPos; - - protected Vector2 aimPos; - private float swingState; private Character prevEquipper; @@ -131,6 +126,9 @@ namespace Barotrauma.Items.Components get { return ConvertUnits.ToDisplayUnits(holdPos); } set { holdPos = ConvertUnits.ToSimUnits(value); } } + //the distance from the holding characters elbow to center of the physics body of the item + protected Vector2 holdPos; + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+ " Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")] @@ -139,6 +137,7 @@ namespace Barotrauma.Items.Components get { return ConvertUnits.ToDisplayUnits(aimPos); } set { aimPos = ConvertUnits.ToSimUnits(value); } } + protected Vector2 aimPos; protected float holdAngle; #if DEBUG @@ -258,12 +257,12 @@ namespace Barotrauma.Items.Components } canBePicked = true; + prevRequiredItems = new Dictionary>(RequiredItems); if (attachable) { prevMsg = DisplayMsg; prevPickKey = PickKey; - prevRequiredItems = new Dictionary>(requiredItems); if (item.Submarine != null) { @@ -320,7 +319,7 @@ namespace Barotrauma.Items.Components if (attachable) { prevMsg = DisplayMsg; - prevRequiredItems = new Dictionary>(requiredItems); + prevRequiredItems = new Dictionary>(RequiredItems); } } @@ -649,7 +648,7 @@ namespace Barotrauma.Items.Components DisplayMsg = prevMsg; PickKey = prevPickKey; - requiredItems = new Dictionary>(prevRequiredItems); + RequiredItems = new Dictionary>(prevRequiredItems); Attached = true; #if CLIENT @@ -667,7 +666,7 @@ namespace Barotrauma.Items.Components item.DrawDepthOffset = 0.0f; #endif //make the item pickable with the default pick key and with no specific tools/items when it's deattached - requiredItems.Clear(); + RequiredItems.Clear(); DisplayMsg = ""; PickKey = InputType.Select; #if CLIENT @@ -916,15 +915,25 @@ namespace Barotrauma.Items.Components bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim && !UsageDisabledByRangedWeapon(picker); if (aim) { - picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle); + if (picker.AnimController.IsHoldingToRope && GetRope() is { Snapped: false } rope) + { + Vector2 targetPos = Submarine.GetRelativeSimPosition(picker, rope.Item); + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: aimPos, aim: true, holdAngle, aimAngle, targetPos: targetPos); + } + else + { + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: aimPos + swingPos, aim: true, holdAngle, aimAngle); + } } else { - picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle); - var rope = GetRope(); - if (rope != null && rope.SnapWhenNotAimed && rope.Item.ParentInventory == null) + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: holdPos + swingPos, aim: false, holdAngle); + if (GetRope() is { SnapWhenNotAimed: true } rope) { - rope.Snap(); + if (rope.Item.ParentInventory == null) + { + rope.Snap(); + } } } } @@ -1054,15 +1063,15 @@ namespace Barotrauma.Items.Components } var tempMsg = DisplayMsg; - var tempRequiredItems = requiredItems; + var tempRequiredItems = RequiredItems; DisplayMsg = prevMsg; - requiredItems = prevRequiredItems; + RequiredItems = prevRequiredItems; XElement saveElement = base.Save(parentElement); DisplayMsg = tempMsg; - requiredItems = tempRequiredItems; + RequiredItems = tempRequiredItems; return saveElement; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 67c3ff90d..a6495844c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -126,7 +126,7 @@ namespace Barotrauma.Items.Components return; } holdable.Reattachable = false; - if (requiredItems.Any()) + if (RequiredItems.Any()) { holdable.PickingTime = float.MaxValue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 737ae2232..12c906e15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -91,7 +91,9 @@ namespace Barotrauma.Items.Components public override void Equip(Character character) { base.Equip(character); - reloadTimer = Math.Min(reload, 1.0f); + //force a wait of at least 1 second when equipping the weapon, so you can't "rapid-fire" by swapping between weapons + const float forcedDelayOnEquip = 1.0f; + reloadTimer = Math.Max(Math.Min(reload, forcedDelayOnEquip), reloadTimer); IsActive = true; } @@ -218,13 +220,12 @@ namespace Barotrauma.Items.Components AnimController ac = picker.AnimController; if (!hitting) { - bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim && - !UsageDisabledByRangedWeapon(picker); + bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim && !UsageDisabledByRangedWeapon(picker); if (aim) { UpdateSwingPos(deltaTime, out Vector2 swingPos); hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4)); - ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true); + ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos + swingPos, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true); if (ac.InWater) { ac.LockFlipping(); @@ -233,7 +234,7 @@ namespace Barotrauma.Items.Components else { hitPos = 0; - ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle); } } else @@ -242,11 +243,11 @@ namespace Barotrauma.Items.Components hitPos -= deltaTime * 15f; if (Swing) { - ac.HoldItem(deltaTime, item, handlePos, SwingPos, Vector2.Zero, aim: false, hitPos, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: SwingPos, aim: false, hitPos, holdAngle); } else { - ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle); } if (hitPos < -MathHelper.Pi) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 4935c05f5..5db4a58ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -82,9 +82,9 @@ namespace Barotrauma.Items.Components var abilityPickingTime = new AbilityItemPickingTime(PickingTime, item.Prefab); picker.CheckTalents(AbilityEffectType.OnItemPicked, abilityPickingTime); - if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped)) + if (RequiredItems.ContainsKey(RelatedItem.RelationType.Equipped)) { - foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Equipped]) + foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Equipped]) { foreach (var heldItem in picker.HeldItems) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 315f8cc71..52a49838e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -95,6 +95,20 @@ namespace Barotrauma.Items.Components get; private set; } + + [Serialize(defaultValue: 1f, IsPropertySaveable.Yes, description: "Penalty multiplier to reload time when dual-wielding.")] + public float DualWieldReloadTimePenaltyMultiplier + { + get; + private set; + } + + [Serialize(defaultValue: 0f, IsPropertySaveable.Yes, description: "Additive penalty to accuracy (spread angle) when dual-wielding.")] + public float DualWieldAccuracyPenalty + { + get; + private set; + } private readonly IReadOnlySet suitableProjectiles; @@ -128,8 +142,11 @@ namespace Barotrauma.Items.Components : base(item, element) { item.IsShootable = true; - // TODO: should define this in xml if we have ranged weapons that don't require aim to use - item.RequireAimToUse = true; + if (element.Parent is { } parent) + { + item.RequireAimToUse = parent.GetAttributeBool(nameof(item.RequireAimToUse), true); + } + characterUsable = true; suitableProjectiles = element.GetAttributeIdentifierArray(nameof(suitableProjectiles), Array.Empty()).ToHashSet(); if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload) @@ -140,7 +157,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(ContentXElement element); + partial void InitProjSpecific(ContentXElement rangedWeaponElement); public override void Equip(Character character) { @@ -194,9 +211,28 @@ namespace Barotrauma.Items.Components float degreeOfFailure = MathHelper.Clamp(1.0f - DegreeOfSuccess(user), 0.0f, 1.0f); degreeOfFailure *= degreeOfFailure; float spread = MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure) / (1f + user.GetStatValue(StatTypes.RangedSpreadReduction)); + if (user.IsDualWieldingRangedWeapons()) + { + spread += Math.Max(0f, ApplyDualWieldPenaltyReduction(user, DualWieldAccuracyPenalty, neutralValue: 0f)); + } return MathHelper.ToRadians(spread); } + /// + /// Lerps between the original penalty and a neutral value, which should be 1 for multipliers and 0 for additive penalties. + /// + /// The character to get stat values from + /// The original penalty value + /// Neutral value to lerp towards. Should be 1 for multipliers and 0 for additives. + /// + private static float ApplyDualWieldPenaltyReduction(Character character, float originalPenalty, float neutralValue) + { + float statAdjustmentPrc = character.GetStatValue(StatTypes.DualWieldingPenaltyReduction); + statAdjustmentPrc = MathHelper.Clamp(statAdjustmentPrc, 0f, 1f); + float reducedPenaltyMultiplier = MathHelper.Lerp(originalPenalty, neutralValue, statAdjustmentPrc); + return reducedPenaltyMultiplier; + } + private readonly List ignoredBodies = new List(); public override bool Use(float deltaTime, Character character = null) { @@ -208,22 +244,27 @@ namespace Barotrauma.Items.Components IsActive = true; float baseReloadTime = reload; float weaponSkill = character.GetSkillLevel("weapons"); - if (ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement) + + bool applyReloadFailure = ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement; + if (applyReloadFailure) { //Examples, assuming 40 weapon skill required: 1 - 40/40 = 0 ... 1 - 0/40 = 1 ... 1 - 20 / 40 = 0.5 float reloadFailure = MathHelper.Clamp(1 - (weaponSkill / ReloadSkillRequirement), 0, 1); baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure); } + + if (character.IsDualWieldingRangedWeapons()) + { + baseReloadTime *= Math.Max(1f, ApplyDualWieldPenaltyReduction(character, DualWieldReloadTimePenaltyMultiplier, neutralValue: 1f)); + } + ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f); ReloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.FiringRateMultiplier); currentChargeTime = 0f; - if (character != null) - { - var abilityRangedWeapon = new AbilityRangedWeapon(item); - character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityRangedWeapon); - } + var abilityRangedWeapon = new AbilityRangedWeapon(item); + character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityRangedWeapon); if (item.AiTarget != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 4f0686cef..906682b3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -90,12 +90,11 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")] public bool RepairThroughHoles { get; set; } - [Serialize(100.0f, IsPropertySaveable.No, description: "How far two walls need to not be considered overlapping and to stop the ray.")] - public float MaxOverlappingWallDist - { - get; set; - } + public float MaxOverlappingWallDist { get; set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "How fast the tool detaches level resources (e.g. minerals). Acts as a multiplier on the speed: with a value of 2, detaching an item whose DeattachDuration is set to 30 seconds would take 15 seconds.")] + public float DeattachSpeed { get; set; } [Serialize(true, IsPropertySaveable.No, description: "Can the item hit doors.")] public bool HitItems { get; set; } @@ -171,7 +170,7 @@ namespace Barotrauma.Items.Components } } item.IsShootable = true; - item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true); + item.RequireAimToUse = element.Parent.GetAttributeBool(nameof(item.RequireAimToUse), true); InitProjSpecific(element); } @@ -321,7 +320,7 @@ namespace Barotrauma.Items.Components private readonly List fireSourcesInRange = new List(); private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List ignoredBodies) { - var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; + var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall; if (!IgnoreCharacters) { collisionCategories |= Physics.CollisionCharacter; @@ -414,7 +413,7 @@ namespace Barotrauma.Items.Components break; } pickedPosition = rayStart + (rayEnd - rayStart) * thisBodyFraction; - if (FixBody(user, deltaTime, degreeOfSuccess, body)) + if (FixBody(user, pickedPosition, deltaTime, degreeOfSuccess, body)) { lastPickedFraction = thisBodyFraction; if (bodyType != null) { lastHitType = bodyType; } @@ -452,7 +451,7 @@ namespace Barotrauma.Items.Components }, allowInsideFixture: true); pickedPosition = Submarine.LastPickedPosition; - FixBody(user, deltaTime, degreeOfSuccess, pickedBody); + FixBody(user, pickedPosition, deltaTime, degreeOfSuccess, pickedBody); lastPickedFraction = Submarine.LastPickedFraction; } @@ -543,7 +542,7 @@ namespace Barotrauma.Items.Components } } - private bool FixBody(Character user, float deltaTime, float degreeOfSuccess, Body targetBody) + private bool FixBody(Character user, Vector2 hitPosition, float deltaTime, float degreeOfSuccess, Body targetBody) { if (targetBody?.UserData == null) { return false; } @@ -600,7 +599,7 @@ namespace Barotrauma.Items.Components { if (Level.Loaded?.ExtraWalls.Find(w => w.Body == cell.Body) is DestructibleLevelWall levelWall) { - levelWall.AddDamage(-LevelWallFixAmount * deltaTime, item.WorldPosition); + levelWall.AddDamage(-LevelWallFixAmount * deltaTime, ConvertUnits.ToDisplayUnits(hitPosition)); } return true; } @@ -661,10 +660,13 @@ namespace Barotrauma.Items.Components var levelResource = targetItem.GetComponent(); if (levelResource != null && levelResource.Attached && - levelResource.requiredItems.Any() && + levelResource.RequiredItems.Any() && levelResource.HasRequiredItems(user, addMessage: false)) { - float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); + float addedDetachTime = deltaTime * + DeattachSpeed * + (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * + (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); levelResource.DeattachTimer += addedDetachTime; #if CLIENT if (targetItem.Prefab.ShowHealthBar && Character.Controlled != null && diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index e972c855e..33c3f6f37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -149,7 +149,7 @@ namespace Barotrauma.Items.Components if (aim || throwState == ThrowState.Initiated) { throwAngle = System.Math.Min(throwAngle + deltaTime * 8.0f, ThrowAngleEnd); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, throwAngle); if (throwAngle >= ThrowAngleEnd && throwState == ThrowState.Initiated) { throwState = ThrowState.Throwing; @@ -158,13 +158,13 @@ namespace Barotrauma.Items.Components else { throwAngle = ThrowAngleStart; - ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, holdAngle); } } else { throwAngle = MathUtils.WrapAnglePi(throwAngle - deltaTime * 15.0f); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, throwAngle); if (throwAngle < 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 16d6b788e..b24a94f6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -48,10 +48,10 @@ namespace Barotrauma.Items.Components public readonly Dictionary> statusEffectLists; - public Dictionary> requiredItems; + public Dictionary> RequiredItems; public readonly List DisabledRequiredItems = new List(); - public List requiredSkills; + public readonly List RequiredSkills = new List(); private ItemComponent parent; public ItemComponent Parent @@ -286,9 +286,7 @@ namespace Barotrauma.Items.Components originalElement = element; name = element.Name.ToString(); SerializableProperties = SerializableProperty.GetProperties(this); - requiredItems = new Dictionary>(); - requiredSkills = new List(); - + RequiredItems = new Dictionary>(); #if CLIENT hasSoundsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; sounds = new Dictionary>(); @@ -336,7 +334,7 @@ namespace Barotrauma.Items.Components } else { - requiredSkills = component.requiredSkills; + RequiredSkills = component.RequiredSkills; } } @@ -389,7 +387,7 @@ namespace Barotrauma.Items.Components } Identifier skillIdentifier = subElement.GetAttributeIdentifier("identifier", ""); - requiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0))); + RequiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0))); break; case "statuseffect": statusEffectLists ??= new Dictionary>(); @@ -414,7 +412,7 @@ namespace Barotrauma.Items.Components void LoadStatusEffect(ContentXElement subElement) { - var statusEffect = StatusEffect.Load(subElement, item.Name); + var statusEffect = StatusEffect.Load(subElement, item.Name + ", " + GetType().Name); if (!statusEffectLists.TryGetValue(statusEffect.type, out List effectList)) { effectList = new List(); @@ -444,11 +442,11 @@ namespace Barotrauma.Items.Components } else { - if (!requiredItems.ContainsKey(ri.Type)) + if (!RequiredItems.ContainsKey(ri.Type)) { - requiredItems.Add(ri.Type, new List()); + RequiredItems.Add(ri.Type, new List()); } - requiredItems[ri.Type].Add(ri); + RequiredItems[ri.Type].Add(ri); } } else if (!allowEmpty) @@ -675,7 +673,7 @@ namespace Barotrauma.Items.Components public bool HasRequiredSkills(Character character, out Skill insufficientSkill) { - foreach (Skill skill in requiredSkills) + foreach (Skill skill in RequiredSkills) { float characterLevel = character.GetSkillLevel(skill.Identifier); if (characterLevel < skill.Level * GetSkillMultiplier()) @@ -696,7 +694,7 @@ namespace Barotrauma.Items.Components /// 0.5f if all the skills meet the skill requirements exactly, 1.0f if they're way above and 0.0f if way less public float DegreeOfSuccess(Character character) { - return DegreeOfSuccess(character, requiredSkills); + return DegreeOfSuccess(character, RequiredSkills); } /// @@ -731,16 +729,18 @@ namespace Barotrauma.Items.Components public virtual void FlipY(bool relativeToSub) { } /// - /// Shorthand for !HasRequiredContainedItems() + /// Returns true if the item is lacking required contained items, or if there's nothing with a non-zero condition inside. /// - public bool IsEmpty(Character user) => !HasRequiredContainedItems(user, addMessage: false); + public bool IsEmpty(Character user) => + !HasRequiredContainedItems(user, addMessage: false) || + (Item.OwnInventory != null && !Item.OwnInventory.AllItems.Any(i => i.Condition > 0)); public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null) { - if (!requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; } + if (!RequiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; } if (item.OwnInventory == null) { return false; } - foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Contained]) + foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Contained]) { if (!ri.CheckRequirements(user, item)) { @@ -765,8 +765,8 @@ namespace Barotrauma.Items.Components { if (character.IsBot && item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } - if (requiredItems.Count == 0) { return true; } - if (character.Inventory != null && requiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List relatedItems)) + if (RequiredItems.Count == 0) { return true; } + if (character.Inventory != null && RequiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List relatedItems)) { foreach (RelatedItem relatedItem in relatedItems) { @@ -811,13 +811,13 @@ namespace Barotrauma.Items.Components public virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { - if (requiredItems.None()) { return true; } + if (RequiredItems.None()) { return true; } if (character.Inventory == null) { return false; } bool hasRequiredItems = false; bool canContinue = true; - if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped)) + if (RequiredItems.ContainsKey(RelatedItem.RelationType.Equipped)) { - foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Equipped]) + foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Equipped]) { canContinue = CheckItems(ri, character.HeldItems); if (!canContinue) { break; } @@ -825,9 +825,9 @@ namespace Barotrauma.Items.Components } if (canContinue) { - if (requiredItems.ContainsKey(RelatedItem.RelationType.Picked)) + if (RequiredItems.ContainsKey(RelatedItem.RelationType.Picked)) { - foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Picked]) + foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Picked]) { if (!CheckItems(ri, character.Inventory.AllItems)) { break; } } @@ -1056,7 +1056,7 @@ namespace Barotrauma.Items.Components { XElement componentElement = new XElement(name); - foreach (var kvp in requiredItems) + foreach (var kvp in RequiredItems) { foreach (RelatedItem ri in kvp.Value) { @@ -1089,8 +1089,8 @@ namespace Barotrauma.Items.Components private void OverrideRequiredItems(ContentXElement element) { - var prevRequiredItems = new Dictionary>(requiredItems); - requiredItems.Clear(); + var prevRequiredItems = new Dictionary>(RequiredItems); + RequiredItems.Clear(); bool returnEmptyRequirements = false; #if CLIENT @@ -1115,11 +1115,11 @@ namespace Barotrauma.Items.Components newRequiredItem.IgnoreInEditor = prevRequiredItem.IgnoreInEditor; } - if (!requiredItems.ContainsKey(newRequiredItem.Type)) + if (!RequiredItems.ContainsKey(newRequiredItem.Type)) { - requiredItems[newRequiredItem.Type] = new List(); + RequiredItems[newRequiredItem.Type] = new List(); } - requiredItems[newRequiredItem.Type].Add(newRequiredItem); + RequiredItems[newRequiredItem.Type].Add(newRequiredItem); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 8a164684c..66fef2fbf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -673,7 +673,7 @@ namespace Barotrauma.Items.Components return false; } } - if (AutoInteractWithContained && character.SelectedItem == null) + if (AutoInteractWithContained && character.SelectedItem == null && Screen.Selected is not { IsEditor: true }) { foreach (Item contained in Inventory.AllItems) { @@ -708,7 +708,7 @@ namespace Barotrauma.Items.Components return false; } } - if (AutoInteractWithContained) + if (AutoInteractWithContained && Screen.Selected is not { IsEditor: true }) { foreach (Item contained in Inventory.AllItems) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index e48ec1934..ee964d4d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -34,10 +34,16 @@ namespace Barotrauma.Items.Components get { return outputContainer; } } + /// + /// Should the output items left in the deconstructor be automatically moved to the main sub at the end of the round + /// if the deconstructor is not in the main sub? + /// + public bool RelocateOutputToMainSub; + [Serialize(false, IsPropertySaveable.Yes)] public bool DeconstructItemsSimultaneously { get; set; } - [Editable, Serialize(1.0f, IsPropertySaveable.Yes)] + [Editable(MinValueFloat = 0.1f, MaxValueFloat = 1000), Serialize(1.0f, IsPropertySaveable.Yes)] public float DeconstructionSpeed { get; set; } public Deconstructor(Item item, ContentXElement element) @@ -290,6 +296,10 @@ namespace Barotrauma.Items.Components spawnedItem.AllowStealing = targetItem.AllowStealing; spawnedItem.OriginalOutpost = targetItem.OriginalOutpost; spawnedItem.SpawnedInCurrentOutpost = targetItem.SpawnedInCurrentOutpost; + if (RelocateOutputToMainSub && user is { AIController: HumanAIController humanAi }) + { + humanAi.HandleRelocation(spawnedItem); + } for (int i = 0; i < outputContainer.Capacity; i++) { var containedItem = outputContainer.Inventory.GetItemAt(i); @@ -318,7 +328,12 @@ namespace Barotrauma.Items.Components } } - GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + targetItem.Prefab.Identifier); + if (targetItem.Prefab.ContentPackage == ContentPackageManager.VanillaCorePackage && + /* we don't need info of every item, we can get a good sample size just by logging 5% */ + Rand.Range(0.0f, 1.0f) < 0.05f) + { + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + targetItem.Prefab.Identifier); + } if (targetItem.AllowDeconstruct && allowRemove) { @@ -329,6 +344,10 @@ namespace Barotrauma.Items.Components foreach (Item outputItem in ic.Inventory.AllItemsMod) { tryPutInOutputSlots(outputItem); + if (RelocateOutputToMainSub && user != null && user.AIController is HumanAIController humanAi) + { + humanAi.HandleRelocation(outputItem); + } } } inputContainer.Inventory.RemoveItem(targetItem); @@ -436,11 +455,12 @@ namespace Barotrauma.Items.Components } } - private void SetActive(bool active, Character user = null) + public void SetActive(bool active, Character user = null, bool createNetworkEvent = false) { PutItemsToLinkedContainer(); this.user = user; + RelocateOutputToMainSub = false; if (inputContainer.Inventory.IsEmpty()) { active = false; } @@ -453,6 +473,10 @@ namespace Barotrauma.Items.Components { GameServer.Log(GameServer.CharacterLogName(user) + (IsActive ? " activated " : " deactivated ") + item.Name, ServerLog.MessageType.ItemInteraction); } + if (createNetworkEvent) + { + item.CreateServerEvent(this); + } #endif if (!IsActive) { @@ -462,7 +486,11 @@ namespace Barotrauma.Items.Components #if CLIENT else { - HintManager.OnStartDeconstructing(user, this); + HintManager.OnStartDeconstructing(user, this); + if (Item.Submarine is { Info.IsOutpost: true } && user is { IsBot: true }) + { + HintManager.OnItemMarkedForRelocation(); + } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 4331f4199..790838fe0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components private ItemContainer inputContainer, outputContainer; - [Serialize(1.0f, IsPropertySaveable.Yes)] + [Editable(MinValueFloat = 0.1f, MaxValueFloat = 1000), Serialize(1.0f, IsPropertySaveable.Yes)] public float FabricationSpeed { get; set; } [Serialize(1.0f, IsPropertySaveable.Yes)] @@ -469,7 +469,10 @@ namespace Barotrauma.Items.Components character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); - quality = GetFabricatedItemQuality(fabricatedItem, user).RollQuality(); + quality = + fabricatedItem.TargetItem.MaxStackSize > 1 ? + GetFabricatedItemQuality(fabricatedItem, user).Quality : + GetFabricatedItemQuality(fabricatedItem, user).RollQuality(); } int amount = (int)fabricationitemAmount.Value; @@ -490,7 +493,12 @@ namespace Barotrauma.Items.Components for (int i = 0; i < amount; i++) { float outCondition = fabricatedItem.OutCondition; - GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); + if (fabricatedItem.TargetItem.ContentPackage == ContentPackageManager.VanillaCorePackage && + /* we don't need info of every fabricated item, we can get a good sample size just by logging 5% */ + Rand.Range(0.0f, 1.0f) < 0.05f) + { + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); + } if (i < amountFittingContainer) { Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, @@ -577,11 +585,11 @@ namespace Barotrauma.Items.Components 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) + public readonly record struct QualityResult(int Quality, bool HasRandomQuality, float PlusOnePercentage, float PlusTwoPercentage) { - public static readonly QualityResult Empty = new QualityResult(0, 0, 0); + public static readonly QualityResult Empty = new QualityResult(0, true, 0, 0); - public bool HasRandomQualityRollChance => PlusOnePercentage > 0f || PlusTwoPercentage > 0f; + public bool HasRandomQualityRollChance => HasRandomQuality && (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 @@ -676,9 +684,23 @@ namespace Barotrauma.Items.Components } } + bool hasRandomQuality = !(fabricatedItem.TargetItem.MaxStackSize > 1); //don't randomise items with a stacksize > 1 + float PlusOnePercentage = plusOne.Match(some: static f => f, none: static () => 0f); + float PlusTwoPercentage = plusTwo.Match(some: static f => f, none: static () => 0f); + + if (!hasRandomQuality && PlusOnePercentage > 0) + { + quality++; + if (PlusTwoPercentage > 0) + { + 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)); + hasRandomQuality, + PlusOnePercentage, + PlusTwoPercentage); } partial void UpdateRequiredTimeProjSpecific(); @@ -690,6 +712,8 @@ namespace Barotrauma.Items.Components GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier)); } + private readonly HashSet usedIngredients = new HashSet(); + private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary> availableIngredients, Character character) { if (fabricableItem == null) { return false; } @@ -733,22 +757,26 @@ namespace Barotrauma.Items.Components return false; } + //maintain a list of used ingredients so we don't end up considering the same item a suitable for multiple required ingredients + usedIngredients.Clear(); + return fabricableItem.RequiredItems.All(requiredItem => { - int availablePrefabsAmount = 0; + int availableItemsAmount = 0; foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { - if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + if (!availableIngredients.TryGetValue(requiredPrefab.Identifier, out var availableItems)) { continue; } - var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; - foreach (Item availablePrefab in availablePrefabs) + foreach (Item availableItem in availableItems) { - if (requiredItem.IsConditionSuitable(availablePrefab.ConditionPercentage)) + if (usedIngredients.Contains(availableItem)) { continue; } + if (requiredItem.IsConditionSuitable(availableItem.ConditionPercentage)) { - availablePrefabsAmount++; + usedIngredients.Add(availableItem); + availableItemsAmount++; } - if (availablePrefabsAmount >= requiredItem.Amount) + if (availableItemsAmount >= requiredItem.Amount) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs index 47609785a..bfa77903b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs @@ -91,7 +91,7 @@ namespace Barotrauma.Items.Components ventList.Clear(); foreach (MapEntity entity in item.linkedTo) { - if (!(entity is Item linkedItem)) { continue; } + if (entity is not Item linkedItem) { continue; } Vent vent = linkedItem.GetComponent(); if (vent?.Item.CurrentHull == null) { continue; } @@ -132,5 +132,19 @@ namespace Barotrauma.Items.Components vent.IsActive = true; } } + + public float GetVentOxygenFlow(Vent targetVent) + { + if (ventList == null) + { + GetVents(); + } + foreach ((Vent vent, float hullVolume) in ventList) + { + if (vent != targetVent) { continue; } + return generatedAmount * 100.0f * (hullVolume / totalHullVolume); + } + return 0.0f; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index f76dace91..fccc5a2c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -215,10 +214,10 @@ namespace Barotrauma.Items.Components activePings[currentPingIndex].Direction = pingDirection; activePings[currentPingIndex].State = 0.0f; activePings[currentPingIndex].PrevPingRadius = 0.0f; - if (item.AiTarget != null) + foreach (AITarget aiTarget in GetAITargets()) { - item.AiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f; - item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); + aiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f; + aiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); } item.Use(deltaTime); } @@ -231,10 +230,10 @@ namespace Barotrauma.Items.Components for (var pingIndex = 0; pingIndex < activePingsCount;) { - if (item.AiTarget != null) + foreach (AITarget aiTarget in GetAITargets()) { - float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom); - item.AiTarget.SoundRange = Math.Max(item.AiTarget.SoundRange, MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range)); + float range = MathUtils.InverseLerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom); + aiTarget.SoundRange = Math.Max(aiTarget.SoundRange, MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range)); } if (activePings[pingIndex].State > 1.0f) { @@ -254,6 +253,24 @@ namespace Barotrauma.Items.Components } } + private IEnumerable GetAITargets() + { + if (!UseTransducers) + { + if (item.AiTarget != null) { yield return item.AiTarget; } + } + else + { + foreach (var transducer in connectedTransducers) + { + if (transducer.Transducer.Item.AiTarget != null) + { + yield return transducer.Transducer.Item.AiTarget; + } + } + } + } + /// /// Power consumption of the sonar. Only consume power when active and adjust the consumption based on the sonar mode. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 9784e92b7..3780f7e1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -22,6 +22,11 @@ namespace Barotrauma.Items.Components private const float AutoPilotMaxSpeed = 0.5f; private const float AIPilotMaxSpeed = 1.0f; + /// + /// How many units before crush depth the pressure warning is shown + /// + public const float PressureWarningThreshold = 500.0f; + /// /// How fast the steering vector adjusts when the nav terminal is operated by something else than a character (= signals) /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 9b3444649..dde0538e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -636,6 +636,8 @@ namespace Barotrauma.Items.Components } if (fixture.Body.UserData is VineTile) { return true; } if (fixture.CollidesWith == Category.None) { return true; } + //only collides with characters = probably an "outsideCollisionBlocker" created by a gap + if (fixture.CollidesWith == Physics.CollisionCharacter) { return true; } if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; } @@ -689,6 +691,8 @@ namespace Barotrauma.Items.Components } if (fixture.Body.UserData is VineTile) { return -1; } if (fixture.CollidesWith == Category.None) { return -1; } + //only collides with characters = probably an "outsideCollisionBlocker" created by a gap + if (fixture.CollidesWith == Physics.CollisionCharacter) { return -1; } if (fixture.Body.UserData is Item item) { if (item.Condition <= 0) { return -1; } @@ -945,9 +949,15 @@ namespace Barotrauma.Items.Components item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir, item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir, collisionCategory: Physics.CollisionWall); + + Vector2 launchPosInCurrentCoordinateSpace = launchPos; + if (item.body.Submarine == null && LaunchSub != null) + { + launchPosInCurrentCoordinateSpace += ConvertUnits.ToSimUnits(LaunchSub.Position); + } if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) && //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction - Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0) + Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPosInCurrentCoordinateSpace, dir) > 0) { target = wallBody.FixtureList.First(); if (hits.Contains(target.Body)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 48a127a8d..65bc2c00c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -144,8 +144,10 @@ namespace Barotrauma.Items.Components private bool tinkeringPowersDevices; public bool TinkeringPowersDevices => tinkeringPowersDevices; - public bool IsBelowRepairThreshold => item.ConditionPercentage <= RepairThreshold; - public bool IsBelowRepairIconThreshold => item.ConditionPercentage <= RepairThreshold / 2; + public bool IsBelowRepairThreshold => item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold; + + public bool IsBelowRepairIconThreshold => item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold / 2; + public enum FixActions : int { @@ -186,6 +188,21 @@ namespace Barotrauma.Items.Components } } + // Modify damage (not stun) caused by repair failure based on campaign settings + if (GameMain.GameSession?.Campaign is CampaignMode campaign + && statusEffectLists != null + && statusEffectLists.TryGetValue(ActionType.OnFailure, out var onFailureEffects)) + { + foreach (var effect in onFailureEffects) + { + foreach (Affliction affliction in effect.Afflictions) + { + if (affliction.Prefab.AfflictionType == Tags.Stun) { continue; } + affliction.Strength *= campaign.Settings.RepairFailMultiplier; + } + } + } + InitProjSpecific(element); } @@ -203,12 +220,12 @@ namespace Barotrauma.Items.Components { if (character == null) { return false; } - if (statusEffectLists == null || statusEffectLists.None(s => s.Key == ActionType.OnFailure)) { return true; } + if (statusEffectLists == null) { return true; } if (bestRepairItem != null && bestRepairItem.Prefab.CannotRepairFail) { return true; } // unpowered (electrical) items can be repaired without a risk of electrical shock - if (requiredSkills.Any(s => s != null && s.Identifier == "electrical")) + if (RequiredSkills.Any(s => s != null && s.Identifier == "electrical")) { if (item.GetComponent() is Reactor reactor) { @@ -216,18 +233,29 @@ namespace Barotrauma.Items.Components } else if (item.GetComponent() is Powered powered && powered.Voltage < 0.1f) { - return true; + return true; } } - if (Rand.Range(0.0f, 0.5f) < RepairDegreeOfSuccess(character, requiredSkills)) { return true; } + bool success = Rand.Range(0.0f, 0.5f) < RepairDegreeOfSuccess(character, RequiredSkills); + ActionType actionType = success ? ActionType.OnSuccess : ActionType.OnFailure; - ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); - if (bestRepairItem != null && bestRepairItem.GetComponent() is Holdable h) + ApplyStatusEffectsAndCreateEntityEvent(this, actionType, character); + ApplyStatusEffectsAndCreateEntityEvent(this, ActionType.OnUse, character); + if (bestRepairItem != null && bestRepairItem.GetComponent() is Holdable holdable) { - h.ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); + ApplyStatusEffectsAndCreateEntityEvent(holdable, actionType, character); + ApplyStatusEffectsAndCreateEntityEvent(holdable, ActionType.OnUse, character); } - return false; + static void ApplyStatusEffectsAndCreateEntityEvent(ItemComponent ic, ActionType actionType, Character character) + { + ic.ApplyStatusEffects(actionType, 1.0f, character); + if (GameMain.NetworkMember is { IsServer: true } && ic.statusEffectLists != null && ic.statusEffectLists.ContainsKey(actionType)) + { + GameMain.NetworkMember.CreateEntityEvent(ic.Item, new Item.ApplyStatusEffectEventData(actionType, ic, character)); + } + } + return success; } public override float GetSkillMultiplier() @@ -251,9 +279,9 @@ namespace Barotrauma.Items.Components if (CurrentFixer == null) { return; } if (qteSuccess) { - item.Condition += RepairDegreeOfSuccess(CurrentFixer, requiredSkills) * 3 * (currentFixerAction == FixActions.Repair ? 1.0f : -1.0f); + item.Condition += RepairDegreeOfSuccess(CurrentFixer, RequiredSkills) * 3 * (currentFixerAction == FixActions.Repair ? 1.0f : -1.0f); } - else if (Rand.Range(0.0f, 2.0f) > RepairDegreeOfSuccess(CurrentFixer, requiredSkills)) + else if (Rand.Range(0.0f, 2.0f) > RepairDegreeOfSuccess(CurrentFixer, RequiredSkills)) { ApplyStatusEffects(ActionType.OnFailure, 1.0f, CurrentFixer); #if SERVER @@ -283,12 +311,6 @@ namespace Barotrauma.Items.Components if (!CheckCharacterSuccess(character, bestRepairItem)) { GameServer.Log($"{GameServer.CharacterLogName(character)} failed to {(action == FixActions.Sabotage ? "sabotage" : "repair")} {item.Name}", ServerLog.MessageType.ItemInteraction); - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, this, character)); - if (bestRepairItem != null && bestRepairItem.GetComponent() is Holdable h) - { - GameMain.Server?.CreateEntityEvent(bestRepairItem, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, h, character)); - } - return false; } @@ -449,7 +471,7 @@ namespace Barotrauma.Items.Components return; } - float successFactor = requiredSkills.Count == 0 ? 1.0f : RepairDegreeOfSuccess(CurrentFixer, requiredSkills); + float successFactor = RequiredSkills.Count == 0 ? 1.0f : RepairDegreeOfSuccess(CurrentFixer, RequiredSkills); //item must have been below the repair threshold for the player to get an achievement or XP for repairing it if (IsBelowRepairThreshold) @@ -462,9 +484,16 @@ namespace Barotrauma.Items.Components } float talentMultiplier = CurrentFixer.GetStatValue(StatTypes.RepairSpeed); - if (requiredSkills.Any(static skill => skill.Identifier == "mechanical")) + foreach (Skill skill in RequiredSkills) { - talentMultiplier += CurrentFixer.GetStatValue(StatTypes.MechanicalRepairSpeed); + if (skill.Identifier == "mechanical") + { + talentMultiplier += CurrentFixer.GetStatValue(StatTypes.MechanicalRepairSpeed); + } + else if (skill.Identifier == "electrical") + { + talentMultiplier += CurrentFixer.GetStatValue(StatTypes.ElectricalRepairSpeed); + } } float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor); @@ -493,7 +522,7 @@ namespace Barotrauma.Items.Components { if (wasBroken) { - foreach (Skill skill in requiredSkills) + foreach (Skill skill in RequiredSkills) { CurrentFixer.Info?.ApplySkillGain(skill.Identifier, SkillSettings.Current.SkillIncreasePerRepair); } @@ -527,7 +556,7 @@ namespace Barotrauma.Items.Components { if (wasGoodCondition) { - foreach (Skill skill in requiredSkills) + foreach (Skill skill in RequiredSkills) { float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier); CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier, @@ -578,11 +607,11 @@ namespace Barotrauma.Items.Components { if (character == null) { return 1.0f; } // kind of rough to keep this in update, but seems most robust - if (requiredSkills.Any(s => s != null && s.Identifier == "mechanical")) + if (RequiredSkills.Any(s => s != null && s.Identifier == "mechanical")) { return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierMechanical); } - if (requiredSkills.Any(s => s != null && s.Identifier == "electrical")) + if (RequiredSkills.Any(s => s != null && s.Identifier == "electrical")) { return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierElectrical); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index fd7fe8aca..96b34c9f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -11,8 +11,8 @@ namespace Barotrauma.Items.Components { private ISpatialEntity source; private Item target; - private Vector2? launchDir; + private float currentRopeLength; private void SetSource(ISpatialEntity source) { @@ -73,6 +73,13 @@ namespace Barotrauma.Items.Components get; set; } + + [Serialize(200.0f, IsPropertySaveable.No, description: "At which distance the user stops pulling the target?")] + public float MinPullDistance + { + get; + set; + } [Serialize(360.0f, IsPropertySaveable.No, description: "The maximum angle from the source to the target until the rope breaks.")] public float MaxAngle @@ -108,7 +115,8 @@ namespace Barotrauma.Items.Components get; set; } - + + private bool isReelingIn; private bool snapped; public bool Snapped { @@ -134,6 +142,15 @@ namespace Barotrauma.Items.Components { snapTimer = 0; } + else if (target != null && source != null && target != source) + { +#if CLIENT + // Play a sound at both ends. Initially tested playing the sound in the middle when the rope snaps in the middle, + // but I think it's more important to ensure that the players hear the sound. + PlaySound(snapSound, source.WorldPosition); + PlaySound(snapSound, target.WorldPosition); +#endif + } } } @@ -156,14 +173,17 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition); IsActive = true; } - + public override void Update(float deltaTime, Camera cam) { - var user = item.GetComponent()?.User; + UpdateProjSpecific(); + isReelingIn = false; + Character user = item.GetComponent()?.User; if (source == null || target == null || target.Removed || - (source is Entity sourceEntity && sourceEntity.Removed) || - (source is Limb limb && limb.Removed) || - (user != null && user.Removed)) + source is Entity { Removed: true } || + source is Limb { Removed: true } || + user is null || + user is { Removed: true }) { ResetSource(); target = null; @@ -191,11 +211,8 @@ namespace Barotrauma.Items.Components if (MaxAngle < 180 && lengthSqr > 2500) { - if (launchDir == null) - { - launchDir = diff; - } - float angle = MathHelper.ToDegrees(VectorExtensions.Angle(launchDir.Value, diff)); + launchDir ??= diff; + float angle = MathHelper.ToDegrees(launchDir.Value.Angle(diff)); if (angle > MaxAngle) { Snap(); @@ -262,8 +279,8 @@ namespace Barotrauma.Items.Components } Vector2 forceDir = diff; - float distance = diff.Length(); - if (distance > 0.001f) + currentRopeLength = diff.Length(); + if (currentRopeLength > 0.001f) { forceDir = Vector2.Normalize(forceDir); } @@ -277,87 +294,162 @@ namespace Barotrauma.Items.Components { float targetMass = float.MaxValue; Character targetCharacter = null; - if (projectile.StickTarget.UserData is Limb targetLimb) + switch (projectile.StickTarget.UserData) { - targetCharacter = targetLimb.character; - targetMass = targetLimb.ragdoll.Mass; - } - else if (projectile.StickTarget.UserData is Character character) - { - targetCharacter = character; - targetMass = character.Mass; - } - else if (projectile.StickTarget.UserData is Item item) - { - targetMass = projectile.StickTarget.Mass; + case Limb targetLimb: + targetCharacter = targetLimb.character; + targetMass = targetLimb.ragdoll.Mass; + break; + case Character character: + targetCharacter = character; + targetMass = character.Mass; + break; + case Item _: + targetMass = projectile.StickTarget.Mass; + break; } if (projectile.StickTarget.BodyType != BodyType.Dynamic) { targetMass = float.MaxValue; } - if (targetMass > TargetMinMass) + if (!snapped) { - if (Math.Abs(SourcePullForce) > 0.001f) + user.AnimController.HoldToRope(); + if (targetCharacter != null) { - var sourceBody = GetBodyToPull(source); - if (sourceBody != null) + targetCharacter.AnimController.DragWithRope(); + } + if (user.InWater) + { + user.AnimController.HangWithRope(); + } + } + if (Math.Abs(SourcePullForce) > 0.001f && targetMass > TargetMinMass) + { + // This should be the main collider. + var sourceBody = GetBodyToPull(source); + if (sourceBody != null) + { + isReelingIn = user.InWater && user.IsRagdolled || !user.InWater && targetCharacter is { IsIncapacitated: false }; + if (isReelingIn) { - if (user != null && user.InWater) + float pullForce = SourcePullForce; + if (!user.InWater) + { + // Apply a tiny amount to the character holding the rope, so that the connection "feels" more real. + pullForce *= 0.1f; + } + float lengthFactor = MathUtils.InverseLerp(0, MaxLength / 2, currentRopeLength); + float force = LerpForces ? MathHelper.Lerp(0, pullForce, lengthFactor) : pullForce; + sourceBody.ApplyForce(forceDir * force); + // Take the target velocity into account. + PhysicsBody targetBody = GetBodyToPull(target); + if (targetBody != null) { - if (user.IsRagdolled) - { - // Reel in towards the target. - user.AnimController.Hang(); - float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) : SourcePullForce; - sourceBody.ApplyForce(forceDir * force); - } - // Take the target velocity into account. if (targetCharacter != null) { - var myCollider = user.AnimController.Collider; - var targetCollider = targetCharacter.AnimController.Collider; - if (myCollider.LinearVelocity != Vector2.Zero && targetCollider.LinearVelocity != Vector2.Zero) + if (targetBody.LinearVelocity != Vector2.Zero && sourceBody.LinearVelocity != Vector2.Zero) { - if (Vector2.Dot(Vector2.Normalize(myCollider.LinearVelocity), Vector2.Normalize(targetCollider.LinearVelocity)) < 0) + Vector2 targetDir = Vector2.Normalize(targetBody.LinearVelocity); + float movementDot = Vector2.Dot(Vector2.Normalize(sourceBody.LinearVelocity), targetDir); + if (movementDot < 0) { - myCollider.ApplyForce(targetCollider.LinearVelocity * targetCollider.Mass); + // Pushing to a different dir -> add some counter force + const float multiplier = 5; + float inverseLengthFactor = MathHelper.Lerp(1, 0, lengthFactor); + sourceBody.ApplyForce(targetBody.LinearVelocity * Math.Min(targetBody.Mass * multiplier, 250) * sourceBody.Mass * -movementDot * inverseLengthFactor); + } + float forceDot = Vector2.Dot(forceDir, targetDir); + if (forceDot > 0) + { + // Pulling to the same dir -> add extra force + float targetSpeed = targetBody.LinearVelocity.Length(); + const float multiplier = 25; + sourceBody.ApplyForce(forceDir * targetSpeed * sourceBody.Mass * multiplier * forceDot * lengthFactor); + } + float colliderMainLimbDistance = Vector2.Distance(sourceBody.SimPosition, user.AnimController.MainLimb.SimPosition); + const float minDist = 1; + const float maxDist = 10; + if (colliderMainLimbDistance > minDist) + { + // Move the ragdoll closer to the collider, if it's too far (the correction force in HumanAnimController is not enough -> the ragdoll would lag behind and get teleported). + float correctionForce = MathHelper.Lerp(10.0f, NetConfig.MaxPhysicsBodyVelocity, MathUtils.InverseLerp(minDist, maxDist, colliderMainLimbDistance)); + Vector2 targetPos = sourceBody.SimPosition + new Vector2((float)Math.Sin(-sourceBody.Rotation), (float)Math.Cos(-sourceBody.Rotation)) * 0.4f; + user.AnimController.MainLimb.MoveToPos(targetPos, correctionForce); } } } else { - var targetBody = GetBodyToPull(target); - if (targetBody != null) - { - sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass); - } + sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass); } } } } } - if (Math.Abs(TargetPullForce) > 0.001f) + if (Math.Abs(TargetPullForce) > 0.001f && !user.IsRagdolled) { - var targetBody = GetBodyToPull(target); + PhysicsBody targetBody = GetBodyToPull(target); + if (targetBody == null) { return; } bool lerpForces = LerpForces; - if (!lerpForces && user != null && targetCharacter != null && !user.AnimController.InWater) + float maxVelocity = NetConfig.MaxPhysicsBodyVelocity * 0.25f; + // The distance where we start pulling with max force. + float maxPullDistance = MaxLength / 3; + float minPullDistance = MinPullDistance; + const float absoluteMinPullDistance = 50; + if (targetCharacter != null) { - if ((forceDir.X < 0) != (user.AnimController.Dir < 0)) + if (targetCharacter.IsRagdolled || targetCharacter.IsUnconscious) { - // Prevents rubberbanding horizontally when dragging a corpse. - lerpForces = true; + if (!targetCharacter.InWater) + { + // Limits the velocity of ragdolled characters on ground/air, because otherwise they tend to move with too high forces. + maxVelocity = NetConfig.MaxPhysicsBodyVelocity * 0.075f; + } + } + else + { + // Target alive and kicking -> Use the absolute min pull distance and full forces to pull. + // Keep some lerping, because it results into smoothing when the target is close by. + minPullDistance = absoluteMinPullDistance; + maxPullDistance = 200; } } - float force = lerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce; - targetBody?.ApplyForce(-forceDir * force); - var targetRagdoll = targetCharacter?.AnimController; - if (targetRagdoll?.Collider != null && (targetRagdoll.InWater || targetRagdoll.OnGround)) + minPullDistance = MathHelper.Max(minPullDistance, absoluteMinPullDistance); + if (currentRopeLength < minPullDistance) { return; } + maxPullDistance = MathHelper.Max(minPullDistance * 2, maxPullDistance); + float force = lerpForces + ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(minPullDistance, maxPullDistance, currentRopeLength)) + : TargetPullForce; + targetBody.ApplyForce(-forceDir * force, maxVelocity); + AnimController targetRagdoll = targetCharacter?.AnimController; + if (targetRagdoll?.Collider != null) { - targetRagdoll.Collider.ApplyForce(-forceDir * force * 3); + isReelingIn = true; + if (targetRagdoll.InWater || targetRagdoll.OnGround) + { + float forceMultiplier = 1; + if (!targetCharacter.IsRagdolled && !targetCharacter.IsIncapacitated) + { + // Pulling the main collider requires higher forces when the target is trying to move away. + Vector2 targetMovement = targetCharacter.AnimController.TargetMovement; + float dot = Vector2.Dot(Vector2.Normalize(targetMovement), forceDir); + if (dot > 0) + { + const float constMultiplier = 2.5f; + float targetVelocity = targetMovement.Length(); + float massFactor = Math.Max((float)Math.Log(targetCharacter.Mass / 10), 1); + forceMultiplier = Math.Max(targetVelocity * massFactor * constMultiplier * dot, 1); + } + } + targetRagdoll.Collider.ApplyForce(-forceDir * force * forceMultiplier, maxVelocity); + } } } } } + + partial void UpdateProjSpecific(); public override void UpdateBroken(float deltaTime, Camera cam) { @@ -409,32 +501,22 @@ namespace Barotrauma.Items.Components { if (target is Item targetItem) { - if (targetItem.ParentInventory is CharacterInventory characterInventory && - characterInventory.Owner is Character ownerCharacter) + if (targetItem.ParentInventory is CharacterInventory { Owner: Character ownerCharacter }) { if (ownerCharacter.Removed) { return null; } return ownerCharacter.AnimController.Collider; } var projectile = targetItem.GetComponent(); - if (projectile != null && projectile.StickTarget != null) + if (projectile is { StickTarget: not null }) { - if (projectile.StickTarget.UserData is Structure structure) + return projectile.StickTarget.UserData switch { - return structure.Submarine?.PhysicsBody; - } - else if (projectile.StickTarget.UserData is Submarine sub) - { - return sub.PhysicsBody; - } - else if (projectile.StickTarget.UserData is Item item) - { - return item.body; - } - else if (projectile.StickTarget.UserData is Limb limb) - { - return limb.body; - } - return null; + Structure structure => structure.Submarine?.PhysicsBody, + Submarine sub => sub.PhysicsBody, + Item item => item.body, + Limb limb => limb.body, + _ => null + }; } if (targetItem.body != null) { return targetItem.body; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs index 69c395e3c..64fa0d3bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components public readonly List InputOutputNodes = new(); + public readonly List Labels = new(); + public readonly List Wires = new List(); public override bool IsActive => true; @@ -80,6 +82,9 @@ namespace Barotrauma.Items.Components public bool IsFull => ComponentContainer?.Inventory is { } inventory && inventory.IsFull(true); + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Locked circuit boxes can only be viewed and not interacted with.")] + public bool Locked { get; set; } + public CircuitBox(Item item, ContentXElement element) : base(item, element) { containers = item.GetComponents().ToArray(); @@ -176,6 +181,9 @@ namespace Barotrauma.Items.Components case "outputnode": LoadFor(CircuitBoxInputOutputNode.Type.Output, subElement); break; + case "label": + Labels.Add(CircuitBoxLabelNode.LoadFromXML(subElement, this)); + break; } } @@ -204,11 +212,14 @@ namespace Barotrauma.Items.Components { Components.Clear(); Wires.Clear(); + Labels.Clear(); - foreach (var origComp in original.Components) + foreach (var label in original.Labels) { - var newComponent = new CircuitBoxComponent(origComp.ID, clonedContainedItems[origComp.Item.ID], origComp.Position, this, origComp.UsedResource); - Components.Add(newComponent); + var newLabel = new CircuitBoxLabelNode(label.ID, label.Color, label.Position, this); + newLabel.EditText(label.HeaderText, label.BodyText); + newLabel.ApplyResize(label.Size, label.Position); + Labels.Add(newLabel); } for (int ioIndex = 0; ioIndex < original.InputOutputNodes.Count; ioIndex++) @@ -219,10 +230,19 @@ namespace Barotrauma.Items.Components cloneNode.Position = origNode.Position; } + if (!clonedContainedItems.Any()) { return; } + + foreach (var origComp in original.Components) + { + if (!clonedContainedItems.TryGetValue(origComp.Item.ID, out var clonedItem)) { continue; } + var newComponent = new CircuitBoxComponent(origComp.ID, clonedItem, origComp.Position, this, origComp.UsedResource); + Components.Add(newComponent); + } + foreach (var origWire in original.Wires) { Option to = CircuitBoxConnectorIdentifier.FromConnection(origWire.To).FindConnection(this), - from = CircuitBoxConnectorIdentifier.FromConnection(origWire.From).FindConnection(this); + from = CircuitBoxConnectorIdentifier.FromConnection(origWire.From).FindConnection(this); if (!to.TryUnwrap(out var toConn) || !from.TryUnwrap(out var fromConn)) { @@ -230,7 +250,8 @@ namespace Barotrauma.Items.Components continue; } - var newWire = new CircuitBoxWire(this, origWire.ID, origWire.BackingWire.Select(w => clonedContainedItems[w.ID]), fromConn, toConn, origWire.UsedItemPrefab); + var wireItem = origWire.BackingWire.Select(w => clonedContainedItems[w.ID]); + var newWire = new CircuitBoxWire(this, origWire.ID, wireItem, fromConn, toConn, origWire.UsedItemPrefab); Wires.Add(newWire); } } @@ -254,6 +275,11 @@ namespace Barotrauma.Items.Components componentElement.Add(wire.Save()); } + foreach (var label in Labels) + { + componentElement.Add(label.Save()); + } + return componentElement; } @@ -324,6 +350,36 @@ namespace Barotrauma.Items.Components return true; } + private void AddLabelInternal(ushort id, Color color, Vector2 pos, NetLimitedString header, NetLimitedString body) + { + var newLabel = new CircuitBoxLabelNode(id, color, pos, this); + newLabel.EditText(header, body); + Labels.Add(newLabel); + OnViewUpdateProjSpecific(); + } + + private void RemoveLabelInternal(IReadOnlyCollection ids) + { + foreach (CircuitBoxLabelNode node in Labels.ToImmutableArray()) + { + if (!ids.Contains(node.ID)) { continue; } + Labels.Remove(node); + } + OnViewUpdateProjSpecific(); + } + + private void ResizeLabelInternal(ushort id, Vector2 pos, Vector2 size) + { + size = Vector2.Max(size, CircuitBoxLabelNode.MinSize); + foreach (CircuitBoxLabelNode node in Labels) + { + if (node.ID != id) { continue; } + node.ApplyResize(size, pos); + break; + } + OnViewUpdateProjSpecific(); + } + private static bool IsExternalConnection(CircuitBoxConnection conn) => conn is (CircuitBoxInputConnection or CircuitBoxOutputConnection); private void CreateWireWithoutItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort id, ItemPrefab prefab) @@ -380,6 +436,18 @@ namespace Barotrauma.Items.Components private void AddWireDirect(ushort id, ItemPrefab prefab, Option backingItem, CircuitBoxConnection one, CircuitBoxConnection two) => Wires.Add(new CircuitBoxWire(this, id, backingItem, one, two, prefab)); + private void RenameLabelInternal(ushort id, Color color, NetLimitedString header, NetLimitedString body) + { + foreach (CircuitBoxLabelNode node in Labels) + { + if (node.ID != id) { continue; } + + node.EditText(header, body); + node.Color = color; + break; + } + } + private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Character? user, Action? onItemSpawned) { if (id is ICircuitBoxIdentifiable.NullComponentID) @@ -426,6 +494,21 @@ namespace Barotrauma.Items.Components ClearSelectionFor(characterId, Components); ClearSelectionFor(characterId, InputOutputNodes); ClearSelectionFor(characterId, Wires); + ClearSelectionFor(characterId, Labels); + } + + private void SelectLabelsInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) + { + if (overwrite) { ClearSelectionFor(characterId, Labels); } + + if (!ids.Any()) { return; } + + foreach (CircuitBoxLabelNode node in Labels) + { + if (!ids.Contains(node.ID)) { continue; } + + node.SetSelected(Option.Some(characterId)); + } } private void SelectComponentsInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) @@ -444,7 +527,8 @@ namespace Barotrauma.Items.Components private void UpdateSelections(ImmutableDictionary> nodeIds, ImmutableDictionary> wireIds, - ImmutableDictionary> inputOutputs) + ImmutableDictionary> inputOutputs, + ImmutableDictionary> labels) { foreach (var wire in Wires) { @@ -474,6 +558,13 @@ namespace Barotrauma.Items.Components node.SetSelected(selectedBy); } + + foreach (var node in Labels) + { + if (!labels.TryGetValue(node.ID, out var selectedBy)) { continue; } + + node.SetSelected(selectedBy); + } } private void SelectWiresInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) @@ -555,6 +646,7 @@ namespace Barotrauma.Items.Components private void MoveNodesInternal(IReadOnlyCollection ids, IReadOnlyCollection ios, + IReadOnlyCollection labels, Vector2 moveAmount) { IEnumerable nodes = Components.Where(node => ids.Contains(node.ID)); @@ -563,6 +655,11 @@ namespace Barotrauma.Items.Components node.Position += moveAmount; } + foreach (var label in Labels.Where(n => labels.Contains(n.ID))) + { + label.Position += moveAmount; + } + foreach (var io in InputOutputNodes) { @@ -635,7 +732,7 @@ namespace Barotrauma.Items.Components } } - public static ImmutableArray GetSortedCircuitBoxSortedItemsFromPlayer(Character? character) + public static ImmutableArray GetSortedCircuitBoxItemsFromPlayer(Character? character) => character?.Inventory?.FindAllItems(predicate: CanItemBeAccessed, recursive: true) .OrderBy(static i => i.Prefab.Identifier == Tags.FPGACircuit) .ToImmutableArray() ?? ImmutableArray.Empty; @@ -651,7 +748,7 @@ namespace Barotrauma.Items.Components { if (character is null) { return Option.None; } - return GetApplicableResourcePlayerHas(prefab, GetSortedCircuitBoxSortedItemsFromPlayer(character)); + return GetApplicableResourcePlayerHas(prefab, GetSortedCircuitBoxItemsFromPlayer(character)); } public static Option GetApplicableResourcePlayerHas(ItemPrefab prefab, ImmutableArray playerItems) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index f96dc53e4..3923350ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -292,9 +292,8 @@ namespace Barotrauma.Items.Components refSub = attachTarget?.Submarine; } - Vector2 nodePos = refSub == null ? - newConnection.Item.Position : - newConnection.Item.Position - refSub.HiddenSubPosition; + Vector2 nodePos = RoundNode(newConnection.Item.Position); + if (refSub != null) { nodePos -= refSub.HiddenSubPosition; } if (nodes.Count > 0 && nodes[0] == nodePos) { return; } if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { return; } @@ -469,9 +468,7 @@ namespace Barotrauma.Items.Components Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition; mouseDiff = mouseDiff.ClampLength(MaxAttachDistance); - return new Vector2( - MathUtils.RoundTowardsClosest(user.Position.X + mouseDiff.X, Submarine.GridSize.X), - MathUtils.RoundTowardsClosest(user.Position.Y + mouseDiff.Y, Submarine.GridSize.Y)); + return RoundNode(user.Position + mouseDiff); } public override bool Use(float deltaTime, Character character = null) @@ -662,11 +659,14 @@ namespace Barotrauma.Items.Components Drawable = sections.Count > 0; } - private Vector2 RoundNode(Vector2 position) + private static Vector2 RoundNode(Vector2 position) { - position.X = MathUtils.Round(position.X, Submarine.GridSize.X / 2.0f); - position.Y = MathUtils.Round(position.Y, Submarine.GridSize.Y / 2.0f); - return position; + Vector2 halfGrid = Submarine.GridSize / 2; + + position += halfGrid; + position.X = MathUtils.RoundTowardsClosest(position.X, Submarine.GridSize.X / 2.0f); + position.Y = MathUtils.RoundTowardsClosest(position.Y, Submarine.GridSize.Y / 2.0f); + return position - halfGrid; } public void SetConnectedDirty() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 8f1228208..e6a875f6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -143,7 +143,7 @@ namespace Barotrauma.Items.Components private bool OnCollision(Fixture sender, Fixture other, Contact contact) { - if (!(LevelTrigger.GetEntity(other) is Entity entity)) { return false; } + if (LevelTrigger.GetEntity(other) is not Entity entity) { return false; } if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; } triggerers.Add(entity); return true; @@ -151,7 +151,7 @@ namespace Barotrauma.Items.Components private void OnSeparation(Fixture sender, Fixture other, Contact contact) { - if (!(LevelTrigger.GetEntity(other) is Entity entity)) + if (LevelTrigger.GetEntity(other) is not Entity entity) { return; } @@ -233,7 +233,17 @@ namespace Barotrauma.Items.Components } else if (triggerer is Character c) { - ApplyForce(c.AnimController.Collider); + if (c.AnimController.Collider.BodyType == BodyType.Dynamic) + { + if (c.AnimController.Collider.Enabled) + { + ApplyForce(c.AnimController.Collider); + } + foreach (var limb in c.AnimController.Limbs) + { + ApplyForce(limb.body, multiplier: limb.Mass * c.AnimController.Collider.Mass / c.AnimController.Mass); + } + } } else if (triggerer is Submarine s) { @@ -248,13 +258,13 @@ namespace Barotrauma.Items.Components item.SendSignal(IsActive ? "1" : "0", "state_out"); } - private void ApplyForce(PhysicsBody body) + private void ApplyForce(PhysicsBody body, float multiplier = 1.0f) { Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition); if (diff.LengthSquared() < 0.0001f) { return; } float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f; if (distanceFactor <= 0.0f) { return; } - Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff); + Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff) * multiplier; if (force.LengthSquared() < 0.01f) { return; } body.ApplyForce(force); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 180487e83..001fd3eef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -338,6 +338,11 @@ namespace Barotrauma.Items.Components } } + /// + /// How deep down does the item protect from pressure? Determined by status effects. + /// + public readonly float PressureProtection; + public Wearable(Item item, ContentXElement element) : base(item, element) { this.item = item; @@ -415,6 +420,12 @@ namespace Barotrauma.Items.Components WearableStatValues.TryAdd(statType, statValue); } break; + case "statuseffect": + if (subElement.GetAttributeString("Target", string.Empty).ToLowerInvariant().Contains("character")) + { + PressureProtection = Math.Max(subElement.GetAttributeFloat(nameof(PressureProtection), 0), PressureProtection); + } + break; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ContainerTagPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ContainerTagPrefab.cs new file mode 100644 index 000000000..ad61ca8e2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ContainerTagPrefab.cs @@ -0,0 +1,149 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + internal class ContainerTagPrefab : Prefab + { + public static readonly PrefabCollection Prefabs = new(); + + public readonly LocalizedString Name; + public readonly LocalizedString Description; + public readonly Identifier Category; + public readonly int RecommendedAmount; + public readonly bool WarnIfLess; + + private static readonly Dictionary categoryToSubmarineType = new() + { + { new Identifier("Submarine"), SubmarineType.Player.ToIdentifier() }, + { new Identifier("AbandonedOutpost"), SubmarineType.OutpostModule.ToIdentifier() }, + { new Identifier("Ruin"), SubmarineType.OutpostModule.ToIdentifier() }, + { new Identifier("Enemy"), SubmarineType.EnemySubmarine.ToIdentifier() } + }; + + public bool IsRecommendedForSub(Submarine sub) + { + var type = sub.Info?.Type ?? SubmarineType.Player; + Identifier category = categoryToSubmarineType.GetValueOrDefault(Category, Category); + return type.ToIdentifier() == category; + } + + public ContainerTagPrefab(ContentXElement element, ContainerTagFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) + { + Category = element.GetAttributeIdentifier("category", ""); + + var nameOverride = element.GetAttributeString("nameidentifier", string.Empty); + + Name = string.IsNullOrEmpty(nameOverride) + ? TextManager.Get($"tagname.{Identifier}").Fallback(Identifier.Value) + : TextManager.Get($"tagname.{nameOverride}").Fallback(Identifier.Value); + + Description = string.IsNullOrEmpty(nameOverride) + ? TextManager.Get($"tagdescription.{Identifier}") + : TextManager.Get($"tagdescription.{nameOverride}"); + + var suffix = element.GetAttributeString("suffix", string.Empty); + if (!string.IsNullOrEmpty(suffix)) + { + Name = TextManager.GetWithVariable($"{suffix}.tagnamesuffix", "[tagname]", Name); + } + + RecommendedAmount = element.GetAttributeInt("recommendedamount", 0); + WarnIfLess = element.GetAttributeBool("warnifless", true); + } + + public readonly record struct ItemAndProbability(ItemPrefab Prefab, float Probability, float CampaignProbability); + + public ImmutableArray GetItemsAndSpawnProbabilities() + { + var items = ImmutableArray.CreateBuilder(); + foreach (ItemPrefab ip in ItemPrefab.Prefabs) + { + bool found = false; + float spawnProbability = 0f; + float campaignSpawnProbability = 0f; + + foreach (PreferredContainer pc in ip.PreferredContainers) + { + if (!pc.Primary.Contains(Identifier) && !pc.Secondary.Contains(Identifier)) { continue; } + + found = true; + spawnProbability = Math.Max(pc.SpawnProbability, spawnProbability); + if (!pc.NotCampaign) + { + campaignSpawnProbability = Math.Max(spawnProbability, campaignSpawnProbability); + } + + if (!pc.NotCampaign || pc.CampaignOnly) + { + campaignSpawnProbability = Math.Max(pc.SpawnProbability, campaignSpawnProbability); + } + } + + if (found) + { + items.Add(new ItemAndProbability(ip, spawnProbability, campaignSpawnProbability)); + } + } + return items.ToImmutable(); + } + + public static void CheckForContainerTagErrors() + { + var allContainerTagsInTheGame = new HashSet(); + var vanillaContainerTags = new HashSet(); + + foreach (var prefab in ItemPrefab.Prefabs) + { + foreach (Identifier tag in prefab.PreferredContainers.SelectMany(pc => Enumerable.Union(pc.Primary, pc.Secondary))) + { + allContainerTagsInTheGame.Add(tag); + if (prefab.ContentPackage == GameMain.VanillaContent && !TagExistsInItemOrCharacterPrefab(tag)) + { + vanillaContainerTags.Add(tag); + } + } + } + + static bool TagExistsInItemOrCharacterPrefab(Identifier tag) + { + if (CharacterPrefab.Prefabs.TryGet(tag, out _)) + { + return true; + } + + foreach (var prefab in ItemPrefab.Prefabs) + { + if (prefab.Tags.Contains(tag) || prefab.Identifier == tag) { return true; } + } + + return false; + } + + // Find container tags that are defined in a ContainerTagPrefab but not used in any item prefabs. + foreach (var prefab in Prefabs) + { + if (!allContainerTagsInTheGame.Contains(prefab.Identifier)) + { + DebugConsole.ThrowError($"Container tag \"{prefab.Identifier}\" defined in ContainerTagPrefab is not used in any item prefabs, did you misspell it?", contentPackage: prefab.ContentPackage); + } + } + + // Find container tags that are used in vanilla item prefabs but not defined in a ContainerTagPrefab. + // We only check vanilla item prefabs because we don't want to force modders to define all vanilla container tags. + foreach (var vanillaTag in vanillaContainerTags) + { + if (Prefabs.All(p => p.Identifier != vanillaTag)) + { + DebugConsole.ThrowError($"Container tag \"{vanillaTag}\" is used in vanilla item prefabs but not defined in a ContainerTagPrefab."); + } + } + } + + public override void Dispose() { } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 5d732d53c..b28202141 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -43,6 +43,13 @@ namespace Barotrauma /// public static IReadOnlyCollection CleanableItems => cleanableItems; + private static readonly HashSet deconstructItems = new HashSet(); + + /// + /// Items that have been marked for deconstruction + /// + public static HashSet DeconstructItems => deconstructItems; + private static readonly List sonarVisibleItems = new List(); /// @@ -183,6 +190,11 @@ namespace Barotrauma private readonly bool[] hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; private readonly Dictionary> statusEffectLists; + /// + /// Helper variable for handling max condition multipliers from campaign settings + /// + private readonly float conditionMultiplierCampaign = 1.0f; + public Action OnInteract; public Dictionary SerializableProperties { get; protected set; } @@ -274,6 +286,11 @@ namespace Barotrauma } } + /// + /// Note that this is not a instance, just the current name of the item as a string. + /// If you e.g. set this as the text in a textbox, it will not update automatically when the language is changed. + /// If you want that to happen, use instead. + /// public override string Name { get { return base.Prefab.Name.Value; } @@ -557,11 +574,10 @@ namespace Barotrauma public Color? HighlightColor; - [Serialize("", IsPropertySaveable.Yes)] - /// /// Can be used to modify the AITarget's label using status effects /// + [Serialize("", IsPropertySaveable.Yes)] public string SonarLabel { get { return AiTarget?.SonarLabel?.Value ?? ""; } @@ -586,20 +602,20 @@ namespace Barotrauma } } - [Serialize(0.0f, IsPropertySaveable.No)] /// /// Can be used by status effects or conditionals to modify the sound range /// + [Serialize(0.0f, IsPropertySaveable.No)] public new float SoundRange { get { return aiTarget == null ? 0.0f : aiTarget.SoundRange; } set { if (aiTarget != null) { aiTarget.SoundRange = Math.Max(0.0f, value); } } } - [Serialize(0.0f, IsPropertySaveable.No)] /// /// Can be used by status effects or conditionals to modify the sight range /// + [Serialize(0.0f, IsPropertySaveable.No)] public new float SightRange { get { return aiTarget == null ? 0.0f : aiTarget.SightRange; } @@ -630,6 +646,14 @@ namespace Barotrauma get; set; } + /// + /// Can be set by status effects to prevent bots from cleaning up the item + /// + public bool DontCleanUp + { + get; set; + } + public Color Color { get { return spriteColor; } @@ -639,6 +663,18 @@ namespace Barotrauma public float MaxCondition { get; private set; } public float ConditionPercentage { get; private set; } + /// + /// Condition percentage disregarding MaxRepairConditionMultiplier (i.e. this can go above 100% if the item is repaired beyond the normal maximum) + /// + public float ConditionPercentageRelativeToDefaultMaxCondition + { + get + { + float defaultMaxCondition = MaxCondition / MaxRepairConditionMultiplier; + return MathUtils.Percentage(Condition, defaultMaxCondition); + } + } + private float offsetOnSelectedMultiplier = 1.0f; [Serialize(1.0f, IsPropertySaveable.No)] @@ -676,6 +712,9 @@ namespace Barotrauma RecalculateConditionValues(); } } + + [Serialize(false, IsPropertySaveable.Yes)] + private bool HasBeenInstantiatedOnce { get; set; } //the default value should be Prefab.Health, but because we can't use it in the attribute, //we'll just use NaN (which does nothing) and set the default value in the constructor/load @@ -1077,7 +1116,7 @@ namespace Barotrauma string collisionCategoryStr = subElement.GetAttributeString("collisioncategory", null); Category collisionCategory = Physics.CollisionItem; - Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; + Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionRepairableWall; if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) && Condition > 0) { //force collision category to Character to allow projectiles and weapons to hit @@ -1264,6 +1303,27 @@ namespace Barotrauma if (HasTag(Barotrauma.Tags.LogicItem)) { isLogic = true; } ApplyStatusEffects(ActionType.OnSpawn, 1.0f); + + // Set max condition multipliers from campaign settings for RecalculateConditionValues() + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + if (HasTag(Barotrauma.Tags.OxygenSource)) + { + conditionMultiplierCampaign *= campaign.Settings.OxygenMultiplier; + } + if (HasTag(Barotrauma.Tags.Fuel)) + { + conditionMultiplierCampaign *= campaign.Settings.FuelMultiplier; + } + } + if (!HasBeenInstantiatedOnce) + { + // This only needs to be done on the very first instantiation. + // MaxCondition will be multiplied in RecalculateConditionValues(), ensuring + // that Condition will stay in line with the multiplier from then on. + condition *= conditionMultiplierCampaign; + } + RecalculateConditionValues(); if (callOnItemLoaded) @@ -1273,6 +1333,7 @@ namespace Barotrauma #if CLIENT Submarine.ForceVisibilityRecheck(); #endif + HasBeenInstantiatedOnce = true; // Enable executing certain things only once } partial void InitProjSpecific(); @@ -1311,17 +1372,17 @@ namespace Barotrauma } //clone requireditem identifiers - foreach (var kvp in components[i].requiredItems) + foreach (var kvp in components[i].RequiredItems) { for (int j = 0; j < kvp.Value.Count; j++) { - if (!clone.components[i].requiredItems.ContainsKey(kvp.Key) || - clone.components[i].requiredItems[kvp.Key].Count <= j) + if (!clone.components[i].RequiredItems.ContainsKey(kvp.Key) || + clone.components[i].RequiredItems[kvp.Key].Count <= j) { continue; } - clone.components[i].requiredItems[kvp.Key][j].JoinedIdentifiers = + clone.components[i].RequiredItems[kvp.Key][j].JoinedIdentifiers = kvp.Value[j].JoinedIdentifiers; } } @@ -1360,20 +1421,17 @@ namespace Barotrauma } } - if (clonedContainedItems.Any()) + for (int i = 0; i < components.Count && i < clone.components.Count; i++) { - for (int i = 0; i < components.Count && i < clone.components.Count; i++) + ItemComponent component = components[i], + cloneComp = clone.components[i]; + + if (component is not CircuitBox origBox || cloneComp is not CircuitBox cloneBox) { - ItemComponent component = components[i], - cloneComp = clone.components[i]; - - if (component is not CircuitBox origBox || cloneComp is not CircuitBox cloneBox) - { - continue; - } - - cloneBox.CloneFrom(origBox, clonedContainedItems); + continue; } + + cloneBox.CloneFrom(origBox, clonedContainedItems); } clone.FullyInitialized = true; @@ -1792,6 +1850,12 @@ namespace Barotrauma if (tags.Contains(tag)) { return; } tags.Add(tag); } + + public void RemoveTag(Identifier tag) + { + if (!tags.Contains(tag)) { return; } + tags.Remove(tag); + } public bool HasTag(Identifier tag) { @@ -1839,6 +1903,22 @@ namespace Barotrauma public bool ConditionalMatches(PropertyConditional conditional) { + return ConditionalMatches(conditional, checkContainer: true); + } + + public bool ConditionalMatches(PropertyConditional conditional, bool checkContainer) + { + if (checkContainer) + { + if (conditional.TargetContainer) + { + if (conditional.TargetGrandParent) + { + return container?.container != null && container.container.ConditionalMatches(conditional, checkContainer: false); + } + return container != null && container.ConditionalMatches(conditional, checkContainer: false); + } + } if (string.IsNullOrEmpty(conditional.TargetItemComponent)) { if (!conditional.Matches(this)) { return false; } @@ -1854,13 +1934,16 @@ namespace Barotrauma return true; } + /// + /// Executes all StatusEffects of the specified type. Note that condition checks are ignored here: that should be handled by the code calling the method. + /// public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, Vector2? worldPosition = null) { if (!hasStatusEffectsOfType[(int)type]) { return; } foreach (StatusEffect effect in statusEffectLists[type]) { - ApplyStatusEffect(effect, type, deltaTime, character, limb, useTarget, isNetworkEvent, false, worldPosition); + ApplyStatusEffect(effect, type, deltaTime, character, limb, useTarget, isNetworkEvent, checkCondition: false, worldPosition); } } @@ -2099,7 +2182,7 @@ namespace Barotrauma /// public void RecalculateConditionValues() { - MaxCondition = Prefab.Health * healthMultiplier * maxRepairConditionMultiplier * (1.0f + GetQualityModifier(Items.Components.Quality.StatType.Condition)); + MaxCondition = Prefab.Health * healthMultiplier * conditionMultiplierCampaign * maxRepairConditionMultiplier * (1.0f + GetQualityModifier(Items.Components.Quality.StatType.Condition)); IsFullCondition = MathUtils.NearlyEqual(Condition, MaxCondition); ConditionPercentage = MathUtils.Percentage(Condition, MaxCondition); } @@ -2193,7 +2276,9 @@ namespace Barotrauma { ItemComponent ic = updateableComponents[i]; - if (ic.IsActiveConditionals != null) + bool isParentInActive = ic.InheritParentIsActive && ic.Parent is { IsActive: false }; + + if (ic.IsActiveConditionals != null && !isParentInActive) { if (ic.IsActiveConditionalComparison == PropertyConditional.LogicalOperatorType.And) { @@ -2288,7 +2373,7 @@ namespace Barotrauma if (needsWaterCheck) { bool wasInWater = inWater; - inWater = !inWaterProofContainer && IsInWater() && !WaterProof; + inWater = !inWaterProofContainer && IsInWater(); if (inWater) { //the item has gone through the surface of the water @@ -2493,9 +2578,12 @@ namespace Barotrauma OnCollisionProjSpecific(impact); if (GameMain.NetworkMember is { IsClient: true }) { return; } - if (ImpactTolerance > 0.0f && condition > 0.0f && Math.Abs(impact) > ImpactTolerance) + if (ImpactTolerance > 0.0f && Math.Abs(impact) > ImpactTolerance && hasStatusEffectsOfType[(int)ActionType.OnImpact]) { - ApplyStatusEffects(ActionType.OnImpact, 1.0f); + foreach (StatusEffect effect in statusEffectLists[ActionType.OnImpact]) + { + ApplyStatusEffect(effect, ActionType.OnImpact, deltaTime: 1.0f); + } #if SERVER GameMain.Server?.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnImpact)); #endif @@ -3129,7 +3217,12 @@ namespace Barotrauma if (character.IsDead) { return; } if (!UseInHealthInterface) { return; } - GameAnalyticsManager.AddDesignEvent("ApplyTreatment:" + Prefab.Identifier); + if (Prefab.ContentPackage == ContentPackageManager.VanillaCorePackage && + /* we don't need info of every item, we can get a good sample size just by logging 5% */ + Rand.Range(0.0f, 1.0f) < 0.05f) + { + GameAnalyticsManager.AddDesignEvent("ApplyTreatment:" + Prefab.Identifier); + } #if CLIENT if (user == Character.Controlled) { @@ -3547,12 +3640,12 @@ namespace Barotrauma } bool canAccess = false; - if (Container?.GetComponent() != null && + if (Container?.GetComponent() is { } cb && 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; + canAccess = !cb.Locked; } else { @@ -3865,6 +3958,8 @@ namespace Barotrauma } } + if (element.GetAttributeBool("markedfordeconstruction", false)) { deconstructItems.Add(item); } + float prevRotation = item.Rotation; if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); } if (element.GetAttributeBool("flippedy", false)) { item.FlipY(false); } @@ -3978,7 +4073,8 @@ namespace Barotrauma element.Add( new XAttribute("name", Prefab.OriginalName), new XAttribute("identifier", Prefab.Identifier), - new XAttribute("ID", ID)); + new XAttribute("ID", ID), + new XAttribute("markedfordeconstruction", deconstructItems.Contains(this))); if (PendingItemSwap != null) { @@ -4182,6 +4278,7 @@ namespace Barotrauma repairableItems.Remove(this); sonarVisibleItems.Remove(this); cleanableItems.Remove(this); + deconstructItems.Remove(this); RemoveFromDroppedStack(allowClientExecute: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 0770ba69f..7d5068b22 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -1009,8 +1009,8 @@ namespace Barotrauma.MapCreatures.Behavior Body branchBody = GameMain.World.CreateRectangle(ConvertUnits.ToSimUnits(rect.Width * scale), ConvertUnits.ToSimUnits(rect.Height * scale), 1.5f); branchBody.BodyType = BodyType.Static; branchBody.UserData = branch; - branchBody.SetCollidesWith(Physics.CollisionRepair); - branchBody.SetCollisionCategories(Physics.CollisionRepair); + branchBody.SetCollidesWith(Physics.CollisionRepairableWall); + branchBody.SetCollisionCategories(Physics.CollisionRepairableWall); branchBody.Position = ConvertUnits.ToSimUnits(pos); branchBody.Enabled = HasBrokenThrough; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index b1f0392c8..4bdd1678f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -54,6 +54,9 @@ namespace Barotrauma public AITarget AiTarget => aiTarget; + /// + /// Indetectable characters can't be spotted by AIs and aren't visible on the sonar or health scanner. + /// public bool InDetectable { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 3fab10249..241300f15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -35,7 +35,7 @@ namespace Barotrauma /// 10% of the range if showEffects is true, 0 otherwise. /// /// - private readonly float cameraShake; + public float CameraShake { get; set; } /// /// How far away does the camera shake effect reach. @@ -45,7 +45,7 @@ namespace Barotrauma /// Same as attack range if showEffects is true, 0 otherwise. /// /// - private readonly float cameraShakeRange; + public float CameraShakeRange { get; set; } /// /// Color tint to apply to the player's screen when in range of the explosion. @@ -173,6 +173,11 @@ namespace Barotrauma /// public bool OnlyOutside; + /// + /// Should the normal damage sounds be played when the explosion damages something. Usually disabled. + /// + public bool PlayDamageSounds; + /// /// How much the explosion repairs items. /// @@ -239,6 +244,8 @@ namespace Barotrauma if (element.GetAttribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); } flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow); + PlayDamageSounds = element.GetAttributeBool(nameof(PlayDamageSounds), false); + EmpStrength = element.GetAttributeFloat("empstrength", 0.0f); BallastFloraDamage = element.GetAttributeFloat("ballastfloradamage", 0.0f); @@ -247,8 +254,8 @@ namespace Barotrauma decal = element.GetAttributeString("decal", ""); decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize"); - cameraShake = element.GetAttributeFloat("camerashake", showEffects ? Attack.Range * 0.1f : 0f); - cameraShakeRange = element.GetAttributeFloat("camerashakerange", showEffects ? Attack.Range : 0f); + CameraShake = element.GetAttributeFloat("camerashake", showEffects ? Attack.Range * 0.1f : 0f); + CameraShakeRange = element.GetAttributeFloat("camerashakerange", showEffects ? Attack.Range : 0f); screenColorRange = element.GetAttributeFloat("screencolorrange", showEffects ? Attack.Range * 0.1f : 0f); screenColor = element.GetAttributeColor("screencolor", Color.Transparent); @@ -301,7 +308,7 @@ namespace Barotrauma Vector2 cameraPos = GameMain.GameScreen.Cam.Position; float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f; - GameMain.GameScreen.Cam.Shake = cameraShake * Math.Max((cameraShakeRange - cameraDist) / cameraShakeRange, 0.0f); + GameMain.GameScreen.Cam.Shake = CameraShake * Math.Max((CameraShakeRange - cameraDist) / CameraShakeRange, 0.0f); #if CLIENT if (screenColor != Color.Transparent) { @@ -318,9 +325,12 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(Attack.GetStructureDamage(1.0f), 0.0f) || !MathUtils.NearlyEqual(Attack.GetLevelWallDamage(1.0f), 0.0f)) { - RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, - IgnoredSubmarines, + RangedStructureDamage(worldPosition, displayRange, + Attack.GetStructureDamage(1.0f), + Attack.GetLevelWallDamage(1.0f), + attacker, IgnoredSubmarines, Attack.EmitStructureDamageParticles, + Attack.CreateWallDamageProjectiles, DistanceFalloff); } @@ -455,6 +465,7 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { + if (attack.OnlyHumans && !c.IsHuman) { continue; } if (IgnoredCharacters.Contains(c)) { continue; } if (!c.Enabled || @@ -485,6 +496,8 @@ namespace Barotrauma Dictionary damages = new Dictionary(); List modifiedAfflictions = new List(); + Limb closestLimb = null; + float closestDistFactor = 0; foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered || limb.IgnoreCollisions || !limb.body.Enabled) { continue; } @@ -511,6 +524,11 @@ namespace Barotrauma if (distFactor > 0) { distFactors.Add(limb, distFactor); + if (distFactor > closestDistFactor) + { + closestLimb = limb; + closestDistFactor = distFactor; + } } } @@ -558,7 +576,11 @@ namespace Barotrauma //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods Vector2 dir = worldPosition - limb.WorldPosition; Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; - AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); + + //only play the damage sound on the closest limb (playing it on all just sounds like a mess) + bool playSound = PlayDamageSounds && limb == closestLimb; + + AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, playSound: playSound, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); damages.Add(limb, attackResult.Damage); } } @@ -622,7 +644,9 @@ namespace Barotrauma /// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken /// public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null, - bool emitWallDamageParticles = true, bool distanceFalloff = true) + bool emitWallDamageParticles = true, + bool createWallDamageProjectiles = false, + bool distanceFalloff = true) { float dist = 600.0f; damagedStructures.Clear(); @@ -642,7 +666,7 @@ namespace Barotrauma 1.0f; if (distFactor <= 0.0f) { continue; } - structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles); + structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles, createWallDamageProjectiles); if (damagedStructures.ContainsKey(structure)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 40e897dea..4d3e9b8d9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -850,9 +850,9 @@ namespace Barotrauma Gap g = new Gap(rect, isHorizontal, submarine, id: idRemap.GetOffsetId(element)) { linkedToID = new List(), + Layer = element.GetAttributeString(nameof(Layer), null) }; - - g.HiddenInGame = element.GetAttributeBool(nameof(HiddenInGame).ToLower(), g.HiddenInGame); + g.HiddenInGame = element.GetAttributeBool(nameof(HiddenInGame), g.HiddenInGame); return g; } @@ -863,7 +863,8 @@ namespace Barotrauma element.Add( new XAttribute("ID", ID), new XAttribute("horizontal", IsHorizontal ? "true" : "false"), - new XAttribute(nameof(HiddenInGame).ToLower(), HiddenInGame)); + new XAttribute(nameof(HiddenInGame), HiddenInGame), + new XAttribute(nameof(Layer), Layer ?? string.Empty)); element.Add(new XAttribute("rect", (int)(rect.X - Submarine.HiddenSubPosition.X) + "," + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 049d7f621..2899a06b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1141,8 +1141,10 @@ namespace Barotrauma float distanceMultiplier = 1; if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) { - //gap blocked if the door is not open or the predicted state is not open - if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) + //gap blocked if the door is closed, and we haven't made any predictions of it opening client-side + if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.PredictedState.HasValue) || + //OR we've predicted that the door is closed client-side + (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) { if (g.ConnectedDoor.OpenState < 0.1f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs index bf28c09aa..b25213a23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Barotrauma.Extensions; -using Barotrauma.MapCreatures.Behavior; +using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; +using System; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 9746cb9ae..3d85f1b53 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -17,7 +17,12 @@ namespace Barotrauma private readonly XElement configElement; - public readonly ImmutableArray<(Identifier Identifier, Rectangle Rect)> DisplayEntities; + public readonly record struct DisplayEntity( + Identifier Identifier, + Rectangle Rect, + float RotationRad); + + public readonly ImmutableArray DisplayEntities; public readonly Rectangle Bounds; @@ -81,23 +86,25 @@ namespace Barotrauma int minX = int.MaxValue, minY = int.MaxValue; int maxX = int.MinValue, maxY = int.MinValue; - var displayEntities = new List<(Identifier, Rectangle)>(); + var displayEntities = new List(); foreach (XElement entityElement in element.Elements()) { ushort id = (ushort)entityElement.GetAttributeInt("ID", 0); if (id > 0 && containedItemIDs.Contains(id)) { continue; } + if (entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase))) { continue; } + Identifier identifier = entityElement.GetAttributeIdentifier("identifier", entityElement.Name.ToString().ToLowerInvariant()); - Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty); - if (!entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase))) - { - if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) { displayEntities.Add((identifier, rect)); } - minX = Math.Min(minX, rect.X); - minY = Math.Min(minY, rect.Y - rect.Height); - maxX = Math.Max(maxX, rect.Right); - maxY = Math.Max(maxY, rect.Y); + float rotation = MathHelper.ToRadians(entityElement.GetAttributeFloat("rotation", 0.0f)); + if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) + { + displayEntities.Add(new DisplayEntity(identifier, rect, rotation)); } + minX = Math.Min(minX, rect.X); + minY = Math.Min(minY, rect.Y - rect.Height); + maxX = Math.Max(maxX, rect.Right); + maxY = Math.Max(maxY, rect.Y); } DisplayEntities = displayEntities.ToImmutableArray(); @@ -121,7 +128,11 @@ namespace Barotrauma public List CreateInstance(Vector2 position, Submarine sub, bool selectInstance = false) { - return PasteEntities(position, sub, configElement, ContentFile.Path.Value, selectInstance); + var retVal = PasteEntities(position, sub, configElement, ContentFile.Path.Value, selectInstance); +#if CLIENT + GameMain.SubEditorScreen?.ReconstructLayers(); +#endif + return retVal; } public static List PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs index 4731ca9a0..cd6bcf0ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs @@ -2,6 +2,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Linq; using Voronoi2; namespace Barotrauma @@ -75,6 +76,7 @@ namespace Barotrauma public void AddDamage(float damage, Vector2 worldPosition) { AddDamageProjSpecific(damage, worldPosition); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (Destroyed) { return; } if (!MathUtils.NearlyEqual(damage, 0.0f)) { NetworkUpdatePending = true; } @@ -100,6 +102,7 @@ namespace Barotrauma #if CLIENT SoundPlayer.PlaySound("icebreak", WorldPosition); #endif + Vector2 center = Vector2.Zero; //generate initial triangles (one triangle from each edge to the center of the cell) List> triangles = new List>(); foreach (var cell in Cells) @@ -114,6 +117,11 @@ namespace Barotrauma }; triangles.Add(triangleVerts); } + center += cell.Center; + } + if (Cells.Any()) + { + center /= Cells.Count; } //split triangles that have edges more than 1000 units long @@ -174,23 +182,24 @@ namespace Barotrauma Vector2 bodyDiff = simTriangleCenter - Body.Position; fragment.Body.LinearVelocity = (bodyDiff + Rand.Vector(0.5f)).ClampLength(15.0f); - fragment.Body.AngularVelocity = Rand.Range(-0.5f, 0.5f);// MathHelper.Clamp(-bodyDiff.X * 0.1f, -0.5f, 0.5f); + fragment.Body.AngularVelocity = Rand.Range(-0.5f, 0.5f); Level.Loaded.UnsyncedExtraWalls.Add(fragment); #if CLIENT - for (int i = 0; i < 20; i++) + for (int i = 0; i < 5; i++) { int startEdgeIndex = Rand.Int(3); Vector2 pos1 = triangle[startEdgeIndex]; Vector2 pos2 = triangle[(startEdgeIndex + 1) % 3]; - var particle = GameMain.ParticleManager.CreateParticle("iceshards", + var particle = GameMain.ParticleManager.CreateParticle("iceexplosion", triangleCenter + Vector2.Lerp(pos1, pos2, Rand.Range(0.0f, 1.0f)), - Rand.Vector(Rand.Range(50.0f, 1000.0f)) + fragment.Body.LinearVelocity * 100.0f); + velocity: (Rand.Vector(Rand.Range(50.0f, 1000.0f)) + fragment.Body.LinearVelocity * 100.0f)); if (particle != null) { particle.Size *= Rand.Range(1.0f, 5.0f); + particle.ColorMultiplier *= Rand.Range(0.7f, 1.0f); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 592e70844..cea585ed9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -119,7 +119,6 @@ namespace Barotrauma /// /// Caves, ruins, outposts and similar enclosed areas /// - /// public bool IsEnclosedArea() { return @@ -127,6 +126,7 @@ namespace Barotrauma PositionType == PositionType.Ruin || PositionType == PositionType.Outpost || PositionType == PositionType.BeaconStation || + PositionType == PositionType.Wreck || PositionType == PositionType.AbyssCave; } } @@ -698,106 +698,8 @@ namespace Barotrauma //---------------------------------------------------------------------------------- //generate voronoi sites //---------------------------------------------------------------------------------- - - Point siteInterval = GenerationParams.VoronoiSiteInterval; - int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y); - Point siteVariance = GenerationParams.VoronoiSiteVariance; - siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); - siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); - const int caveSiteInterval = 500; - for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X) - { - for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y) - { - int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient); - int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient); - bool closeToTunnel = false; - bool closeToCave = false; - foreach (Tunnel tunnel in Tunnels) - { - float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y)); - for (int i = 1; i < tunnel.Nodes.Count; i++) - { - if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; } - if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; } - if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; } - if (siteY > Math.Max(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) + minDist) { continue; } - - double tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); - if (Math.Sqrt(tunnelDistSqr) < minDist) - { - closeToTunnel = true; - //tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); - if (tunnel.Type == TunnelType.Cave) - { - closeToCave = true; - } - break; - } - } - } - - if (!closeToTunnel) - { - //make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry - if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; } - } - - if (!TooCloseToOtherSites(siteX, siteY)) - { - siteCoordsX.Add(siteX); - siteCoordsY.Add(siteY); - } - - if (closeToCave) - { - for (int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval) - { - for (int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval) - { - int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); - int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); - - if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval)) - { - siteCoordsX.Add(caveSiteX); - siteCoordsY.Add(caveSiteY); - } - } - } - } - } - } - - bool TooCloseToOtherSites(double siteX, double siteY, float minDistance = 10.0f) - { - float minDistanceSqr = minDistance * minDistance; - for (int i = 0; i < siteCoordsX.Count; i++) - { - if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr) - { - return true; - } - } - return false; - } - - for (int i = 0; i < siteCoordsX.Count; i++) - { - Debug.Assert( - siteCoordsX[i] > 0 || siteCoordsY[i] > 0, - $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); - Debug.Assert( - siteCoordsX[i] < borders.Width || siteCoordsY[i] < borders.Height, - $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); - for (int j = i + 1; j < siteCoordsX.Count; j++) - { - Debug.Assert( - MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteCoordsX[j], siteCoordsY[j]) > 1.0f, - "Potential error in level generation: two voronoi sites are extremely close to each other."); - } - } + GenerateVoronoiSites(); GenerateEqualityCheckValue(LevelGenStage.VoronoiGen); @@ -809,13 +711,52 @@ namespace Barotrauma sw2.Start(); Debug.Assert(siteCoordsX.Count == siteCoordsY.Count); - List graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), borders.Width, borders.Height); - Debug.WriteLine("MakeVoronoiGraph: " + sw2.ElapsedMilliseconds + " ms"); - sw2.Restart(); - - //construct voronoi cells based on the graph edges - cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders, GridCellSize, out cellGrid); + int remainingRetries = 5; + bool voronoiGraphInvalid = false; + do + { + remainingRetries--; + voronoiGraphInvalid = false; + //construct voronoi cells based on the graph edges + List graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), borders.Width, borders.Height); + cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders, GridCellSize, out cellGrid); + for (int i = 0; i < cells.Count; i++) + { + for (int j = i + 1; j < cells.Count; j++) + { + //sites can never be inside multiple cells in a voronoi graph by definition + //if they are, that'll cause severe issues with the rest of the level generation. + + //There seems to be a very rare issue that sometimes causes the graph to generate incorrectly (see #10944 and #12980), + //leading to a crash due. I haven't been able to figure out what's causing that - there don't seem to be any issues in the sites, + //so I'm getting the feeling it could be a bug with the voronoi graph generation. + + //If that happens, let's just retry a couple of times (re-randomizing the sites and regenerating + //the map seems to fix the issue in all cases I've seen) + if (cells[j].IsPointInside(cells[i].Center)) + { + voronoiGraphInvalid = true; + break; + } + if (voronoiGraphInvalid) { break; } + } + } + if (voronoiGraphInvalid) + { + string errorMsg = "Unknown error during level generation. Invalid voronoi graph: the same voronoi site was inside multiple cells."; + if (remainingRetries > 0) + { + DebugConsole.AddWarning(errorMsg + " Retrying..."); + GenerateVoronoiSites(); + } + else + { + //throw a console error and let the generation finish, hoping for the best + DebugConsole.ThrowError(errorMsg); + } + } + } while (remainingRetries > 0 && voronoiGraphInvalid); GenerateAbyssGeometry(); GenerateAbyssPositions(); @@ -1449,6 +1390,116 @@ namespace Barotrauma Generating = false; } + + private void GenerateVoronoiSites() + { + Point siteInterval = GenerationParams.VoronoiSiteInterval; + int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y); + Point siteVariance = GenerationParams.VoronoiSiteVariance; + siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); + siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); + const int caveSiteInterval = 500; + for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X) + { + for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y) + { + int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient); + int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient); + + bool closeToTunnel = false; + bool closeToCave = false; + foreach (Tunnel tunnel in Tunnels) + { + float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y)); + for (int i = 1; i < tunnel.Nodes.Count; i++) + { + if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; } + if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; } + if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; } + if (siteY > Math.Max(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) + minDist) { continue; } + + double tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); + if (Math.Sqrt(tunnelDistSqr) < minDist) + { + closeToTunnel = true; + //tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); + if (tunnel.Type == TunnelType.Cave) + { + closeToCave = true; + } + break; + } + } + } + + if (!closeToTunnel) + { + //make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry + if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; } + } + + if (!TooCloseToOtherSites(siteX, siteY)) + { + siteCoordsX.Add(siteX); + siteCoordsY.Add(siteY); + } + + if (closeToCave) + { + for (int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval) + { + for (int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval) + { + int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); + int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); + + if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval)) + { + siteCoordsX.Add(caveSiteX); + siteCoordsY.Add(caveSiteY); + } + } + } + } + } + } + + bool TooCloseToOtherSites(double siteX, double siteY, float minDistance = 10.0f) + { + float minDistanceSqr = minDistance * minDistance; + for (int i = 0; i < siteCoordsX.Count; i++) + { + if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr) + { + return true; + } + } + return false; + } + + for (int i = 0; i < siteCoordsX.Count; i++) + { + Debug.Assert( + !double.IsNaN(siteCoordsX[i]) && !double.IsInfinity(siteCoordsX[i]), + $"Potential error in level generation: invalid voronoi site ({siteCoordsX[i]})"); + Debug.Assert( + !double.IsNaN(siteCoordsY[i]) && !double.IsInfinity(siteCoordsY[i]), + $"Potential error in level generation: invalid voronoi site ({siteCoordsY[i]})"); + Debug.Assert( + siteCoordsX[i] > 0 || siteCoordsY[i] > 0, + $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); + Debug.Assert( + siteCoordsX[i] < borders.Width || siteCoordsY[i] < borders.Height, + $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); + for (int j = i + 1; j < siteCoordsX.Count; j++) + { + Debug.Assert( + MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteCoordsX[j], siteCoordsY[j]) > 10.0f, + "Potential error in level generation: two voronoi sites are extremely close to each other."); + } + } + } + private List GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel, float variance) { List pathNodes = new List { startPosition }; @@ -3165,7 +3216,7 @@ namespace Barotrauma private List GetAllValidClusterLocations() { var subBorders = new List(); - Wrecks.ForEach(w => AddBordersToList(w)); + Wrecks.ForEach(AddBordersToList); AddBordersToList(BeaconStation); var locations = new List(); @@ -3186,6 +3237,8 @@ namespace Barotrauma { if (s == null) { return; } var rect = Submarine.AbsRect(s.WorldPosition, s.Borders.Size.ToVector2()); + // range of piezo crystal discharge is 3500, pad the rect to ensure no such kind of hazards spawn near + rect.Inflate(4000, 4000); subBorders.Add(rect); } @@ -3202,9 +3255,14 @@ namespace Barotrauma { foreach (var r in subBorders) { - if (Submarine.RectContains(r, e.Point1)) { return true; } - if (Submarine.RectContains(r, e.Point2)) { return true; } - if (Submarine.RectContains(r, eCenter)) { return true; } + if (r.Contains(e.Point1)) { return true; } + if (r.Contains(e.Point2)) { return true; } + if (r.Contains(eCenter)) { return true; } + + if (MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, r, out _)) + { + return true; + } } return false; } @@ -3291,7 +3349,7 @@ namespace Barotrauma Vector2 endPos = startPos - Vector2.UnitY * Size.Y; //try to find a level wall below the position unless the position is indoors - if (!potentialPos.IsEnclosedArea()) + if (!potentialPos.PositionType.IsEnclosedArea()) { if (Submarine.PickBody( ConvertUnits.ToSimUnits(startPos), @@ -3684,7 +3742,12 @@ namespace Barotrauma var placement = info.BeaconStationInfo?.Placement ?? PlacementType.Bottom; // Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by. - Point paddedDimensions = new Point(subBorders.Width + 3000, subBorders.Height + 3000); + int padding = 1500; + Rectangle paddedBorders = new Rectangle( + subBorders.X - padding, + subBorders.Y + padding, + subBorders.Width + padding * 2, + subBorders.Height + padding * 2); var positions = new List(); var rects = new List(); @@ -3733,7 +3796,8 @@ namespace Barotrauma { if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.WreckHullFloodingChance) { - hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient); + hull.WaterVolume = + Math.Max(hull.WaterVolume, hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient)); } } // Only spawn thalamus when the wreck has some thalamus items defined. @@ -3806,10 +3870,18 @@ namespace Barotrauma } } positions.Add(spawnPoint); - bool isBlocked = IsBlocked(spawnPoint, subBorders.Size - new Point(step + 50)); + //shrink the bounds a bit to allow the sub to go slightly inside the wall + //(just enough that it doesn't look like it's floating) + int shrinkAmount = step + 50; + Rectangle shrunkenBorders = new Rectangle( + subBorders.X + shrinkAmount, + subBorders.Y - shrinkAmount, + subBorders.Width - shrinkAmount * 2, + subBorders.Height - shrinkAmount * 2); + bool isBlocked = IsBlocked(spawnPoint, shrunkenBorders); if (isBlocked) { - rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint(), subBorders.Size)); + rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint() + subBorders.Location, subBorders.Size)); Debug.WriteLine($"Invalid position {spawnPoint}. Blocked by level walls."); } else if (!bottomFound) @@ -3832,7 +3904,8 @@ namespace Barotrauma float maxMovement = 5000; float totalAmount = 0; bool foundBottom = TryRaycast(subBorders, placement, ref spawnPoint); - while (!IsSideBlocked(subBorders, amount > 0)) + //move until the side is no longer blocked + while (IsSideBlocked(subBorders, front: amount < 0)) { foundBottom = TryRaycast(subBorders, placement, ref spawnPoint); totalAmount += amount; @@ -3854,7 +3927,7 @@ namespace Barotrauma { var wp = waypoints.GetRandom(Rand.RandSync.ServerAndClient); waypoints.Remove(wp); - if (!IsBlocked(wp.WorldPosition, paddedDimensions)) + if (!IsBlocked(wp.WorldPosition, paddedBorders)) { spawnPoint = wp.WorldPosition; return true; @@ -3867,76 +3940,73 @@ namespace Barotrauma { // Shoot five rays and pick the highest hit point. int rayCount = 5; - var positions = new Vector2[rayCount]; + var hitPositions = new Vector2[rayCount]; bool hit = false; for (int i = 0; i < rayCount; i++) { - float quarterWidth = subBorders.Width * 0.25f; - Vector2 rayStart = spawnPoint; - switch (i) - { - case 1: - rayStart = new Vector2(spawnPoint.X - quarterWidth, spawnPoint.Y); - break; - case 2: - rayStart = new Vector2(spawnPoint.X + quarterWidth, spawnPoint.Y); - break; - case 3: - rayStart = new Vector2(spawnPoint.X - quarterWidth / 2, spawnPoint.Y); - break; - case 4: - rayStart = new Vector2(spawnPoint.X + quarterWidth / 2, spawnPoint.Y); - break; - } + //cast rays starting from the left side of the sub, offset by 20% to 80% of the sub's width + //(ignoring the very back and front of the sub, it's fine if they overlap a bit) + float xOffset = + subBorders.Width * MathHelper.Lerp(0.2f, 0.8f, i / (float)(rayCount - 1)); + Vector2 rayStart = new Vector2( + spawnPoint.X + subBorders.Location.X + xOffset, + spawnPoint.Y); var simPos = ConvertUnits.ToSimUnits(rayStart); var body = Submarine.PickBody(simPos, new Vector2(simPos.X, placement == PlacementType.Bottom ? -1 : Size.Y + 1), customPredicate: f => f.Body == TopBarrier || f.Body == BottomBarrier || (f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body)), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); if (body != null) { - positions[i] = - ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + - new Vector2(0, subBorders.Height / 2 * (placement == PlacementType.Bottom ? 1 : -1)); + hitPositions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition); hit = true; } } - float highestPoint = placement == PlacementType.Bottom ? positions.Max(p => p.Y) : positions.Min(p => p.Y); - spawnPoint = new Vector2(spawnPoint.X, highestPoint); + int dir = placement == PlacementType.Bottom ? -1 : 1; + float highestPoint = placement == PlacementType.Bottom ? hitPositions.Max(p => p.Y) : hitPositions.Min(p => p.Y); + float halfHeight = subBorders.Height / 2; + float centerOffset = subBorders.Y - halfHeight; + spawnPoint = new Vector2(spawnPoint.X, highestPoint + halfHeight * -dir - centerOffset); return hit; } bool IsSideBlocked(Rectangle subBorders, bool front) { + Point centerOffset = new Point(subBorders.Center.X, subBorders.Y - subBorders.Height / 2); + // Shoot three rays and check whether any of them hits. int rayCount = 3; + int dir = front ? 1 : -1; Vector2 halfSize = subBorders.Size.ToVector2() / 2; Vector2 quarterSize = halfSize / 2; - var positions = new Vector2[rayCount]; for (int i = 0; i < rayCount; i++) { - float dir = front ? 1 : -1; - Vector2 rayStart; - Vector2 to; + Vector2 rayStart, to; switch (i) { case 1: - rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y + quarterSize.Y); - to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); - break; case 2: - rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y - quarterSize.Y); - to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); + float yOffset = quarterSize.Y * (i == 1 ? 1 : -1); + //from a position half-way towards the edge, to the edge. + //we start half-way towards the edge instead of the center, because we want to allow things to poke partially inside the sub + rayStart = new Vector2(spawnPoint.X + quarterSize.X * dir, spawnPoint.Y + yOffset); + to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y); break; case 0: default: + //center to center-right rayStart = spawnPoint; to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y); break; } + + rayStart += centerOffset.ToVector2(); + to += centerOffset.ToVector2(); + Vector2 simPos = ConvertUnits.ToSimUnits(rayStart); if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to), customPredicate: f => f.Body?.UserData is VoronoiCell cell, - collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall, + allowInsideFixture: true) != null) { return true; } @@ -3944,10 +4014,11 @@ namespace Barotrauma return false; } - bool IsBlocked(Vector2 pos, Point size, float maxDistanceMultiplier = 1) + bool IsBlocked(Vector2 pos, Rectangle submarineBounds) { - float maxDistance = size.Multiply(maxDistanceMultiplier).ToVector2().LengthSquared(); - Rectangle bounds = ToolBox.GetWorldBounds(pos.ToPoint(), size); + Rectangle bounds = new Rectangle( + (int)pos.X + submarineBounds.X, (int)pos.Y + submarineBounds.Y, + submarineBounds.Width, submarineBounds.Height); if (Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).IntersectsWorld(bounds))) { return true; @@ -3958,13 +4029,16 @@ namespace Barotrauma { return true; } - return cells.Any(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance && c.BodyVertices.Any(v => bounds.ContainsWorld(v))); + return cells.Any(c => + c.Body != null && + c.BodyVertices.Any(v => bounds.ContainsWorld(v))); } } // For debugging private readonly Dictionary> wreckPositions = new Dictionary>(); private readonly Dictionary> blockedRects = new Dictionary>(); + private void CreateWrecks() { var totalSW = new Stopwatch(); @@ -4005,6 +4079,18 @@ namespace Barotrauma wreckCount = Math.Max(wreckCount, 1); } + if (LevelData.ForceWreck != null) + { + //force the desired wreck to be chosen first + var matchingFile = wreckFiles.FirstOrDefault(w => w.Path == LevelData.ForceWreck.FilePath); + if (matchingFile != null) + { + wreckFiles.Remove(matchingFile); + wreckFiles.Insert(0, matchingFile); + } + wreckCount = Math.Max(wreckCount, 1); + } + Wrecks = new List(wreckCount); for (int i = 0; i < wreckCount; i++) { @@ -4286,7 +4372,7 @@ namespace Barotrauma private void CreateBeaconStation() { - if (!LevelData.HasBeaconStation && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; } + if (!LevelData.HasBeaconStation && LevelData.ForceBeaconStation == null && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; } var beaconStationFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) .OrderBy(f => f.UintIdentifier).ToList(); @@ -4307,6 +4393,10 @@ namespace Barotrauma DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead..."); } } + else if (LevelData.ForceBeaconStation != null) + { + contentFile = beaconStationFiles.FirstOrDefault(b => b.Path == LevelData.ForceBeaconStation.FilePath); + } if (contentFile == null) { @@ -4384,10 +4474,17 @@ namespace Barotrauma fuelPrefab, reactorContainer.Inventory, onSpawned: (it) => reactorComponent.PowerUpImmediately()); } - beaconSonar.CurrentMode = Sonar.Mode.Active; + if (beaconSonar == null) + { + DebugConsole.AddWarning($"Beacon station \"{BeaconStation.Info.Name}\" has no sonar. Beacon missions might not work correctly with this beacon station."); + } + else + { + beaconSonar.CurrentMode = Sonar.Mode.Active; #if SERVER - beaconSonar.Item.CreateServerEvent(beaconSonar); + beaconSonar.Item.CreateServerEvent(beaconSonar); #endif + } } else if (GameMain.NetworkMember is not { IsClient: true }) { @@ -4470,7 +4567,7 @@ namespace Barotrauma { foreach (var connectedSub in parentSub.GetConnectedSubs()) { - connectedSub.RealWorldCrushDepth = Math.Max(connectedSub.RealWorldCrushDepth, GetRealWorldDepth(0) + 1000); + connectedSub.SetCrushDepth(Math.Max(connectedSub.RealWorldCrushDepth, GetRealWorldDepth(0) + 1000)); } } @@ -4631,7 +4728,7 @@ namespace Barotrauma } /// - /// Calculate the "real" depth in meters from the surface of Europa + /// Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav terminal) /// public float GetRealWorldDepth(float worldPositionY) { @@ -4733,4 +4830,30 @@ namespace Barotrauma Loaded = null; } } + + static class PositionTypeExtensions + { + /// + /// Caves, ruins, outposts and similar enclosed areas + /// + public static bool IsEnclosedArea(this Level.PositionType positionType) + { + return + positionType == Level.PositionType.Cave || + positionType == Level.PositionType.AbyssCave || + positionType.IsIndoorsArea(); + } + + /// + /// Area inside a submarine (outpost, wreck, beacon station) + /// + public static bool IsIndoorsArea(this Level.PositionType positionType) + { + return + positionType == Level.PositionType.Outpost || + positionType == Level.PositionType.BeaconStation || + positionType == Level.PositionType.Ruin || + positionType == Level.PositionType.Wreck; + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 25e106aba..fb6e9d1bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -43,6 +43,10 @@ namespace Barotrauma public OutpostGenerationParams ForceOutpostGenerationParams; + public SubmarineInfo ForceBeaconStation; + + public SubmarineInfo ForceWreck; + public bool AllowInvalidOutpost; public readonly Point Size; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 78deb6fe1..d7c70391f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -405,25 +405,35 @@ namespace Barotrauma } } - public static bool CheckContactsForOtherFixtures(PhysicsBody triggerBody, Fixture otherFixture, Entity separatingEntity) + /// + /// Checks whether any fixture of the trigger body is in contact with any fixture belonging to the physics bodies of separatingEntity + /// + /// Physics body of the trigger + /// Fixture that got separated from the trigger + /// Entity that got separated from the trigger + /// + public static bool CheckContactsForOtherFixtures(PhysicsBody triggerBody, Fixture separatingFixture, Entity separatingEntity) { //check if there are contacts with any other fixture of the trigger //(the OnSeparation callback happens when two fixtures separate, //e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body) - foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList) + foreach (Fixture triggerFixture in triggerBody.FarseerBody.FixtureList) { - ContactEdge contactEdge = fixture.Body.ContactList; + ContactEdge contactEdge = triggerFixture.Body.ContactList; while (contactEdge != null) { if (contactEdge.Contact != null && contactEdge.Contact.Enabled && contactEdge.Contact.IsTouching) { - if (contactEdge.Contact.FixtureA != fixture && contactEdge.Contact.FixtureB != fixture) - { - var otherEntity = GetEntity(contactEdge.Contact.FixtureB == otherFixture ? + //which fixture of this contact belongs to the "other" body (not the trigger itself) + Fixture otherFixture = + contactEdge.Contact.FixtureA == triggerFixture ? contactEdge.Contact.FixtureB : - contactEdge.Contact.FixtureA); + contactEdge.Contact.FixtureA; + if (otherFixture != separatingFixture) + { + var otherEntity = GetEntity(otherFixture); if (otherEntity == separatingEntity) { return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 7a0ef7607..dd3df3d5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -307,6 +307,12 @@ namespace Barotrauma // Adjust by current reputation price *= GetReputationModifier(true); + // Adjust by campaign difficulty settings + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + price *= campaign.Settings.ShopPriceMultiplier; + } + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) { @@ -768,27 +774,22 @@ namespace Barotrauma else { DebugConsole.Log($"Location {DisplayName.Value} changed it's type from {Type} to {newType}"); - DisplayName = - Type.NameFormats == null || !Type.NameFormats.Any() ? - TextManager.Get(nameIdentifier) : + DisplayName = + Type.NameFormats == null || !Type.NameFormats.Any() ? + TextManager.Get(nameIdentifier) : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", TextManager.Get(nameIdentifier).Value); } + TryAssignFactionBasedOnLocationType(campaign); if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { - if (Faction == null) - { - Faction = campaign.GetRandomFaction(Rand.RandSync.Unsynced); - } - if (SecondaryFaction == null) - { - SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced); - } + if (Type.Faction == Identifier.Empty) { Faction ??= campaign.GetRandomFaction(Rand.RandSync.Unsynced); } + if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced); } } else { - Faction = null; - SecondaryFaction = null; + if (Type.Faction == Identifier.Empty) { Faction = null; } + if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction = null; } } UnlockInitialMissions(Rand.RandSync.Unsynced); @@ -799,6 +800,30 @@ namespace Barotrauma } } + public void TryAssignFactionBasedOnLocationType(CampaignMode campaign) + { + if (campaign == null) { return; } + if (Type.Faction != Identifier.Empty) + { + Faction = Type.Faction == "None" ? null : TryFindFaction(Type.Faction); + } + if (Type.SecondaryFaction != Identifier.Empty) + { + SecondaryFaction = Type.SecondaryFaction == "None" ? null : TryFindFaction(Type.SecondaryFaction); + } + + Faction TryFindFaction(Identifier identifier) + { + var faction = campaign.GetFaction(identifier); + if (faction == null) + { + DebugConsole.ThrowError($"Error in location type \"{Type.Identifier}\": failed to find a faction with the identifier \"{identifier}\".", + contentPackage: Type.ContentPackage); + } + return faction; + } + } + public void UnlockInitialMissions(Rand.RandSync randSync = Rand.RandSync.ServerAndClient) { if (Type.MissionIdentifiers.Any()) @@ -1127,6 +1152,7 @@ namespace Barotrauma nameIdentifier = type.GetRandomNameId(rand, existingLocations); if (nameIdentifier.IsEmpty) { + //backwards compatibility rawName = type.GetRandomRawName(rand, existingLocations); if (rawName.IsNullOrEmpty()) { @@ -1134,7 +1160,15 @@ namespace Barotrauma rawName = "none"; } nameIdentifier = rawName.ToIdentifier(); - DisplayName = rawName; + if (type.NameFormats == null || !type.NameFormats.Any()) + { + DisplayName = rawName; + } + else + { + nameFormatIndex = rand.Next() % type.NameFormats.Count; + DisplayName = type.NameFormats[nameFormatIndex].Replace("[name]", rawName); + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index b300c4d02..d67c0c29d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -86,6 +86,16 @@ namespace Barotrauma public Identifier ReplaceInRadiation { get; } + /// + /// If set, forces the location to be assigned to this faction. Set to "None" if you don't want the location to be assigned to any faction. + /// + public Identifier Faction { get; } + + /// + /// If set, forces the location to be assigned to this secondary faction. Set to "None" if you don't want the location to be assigned to any secondary faction. + /// + public Identifier SecondaryFaction { get; } + public Sprite Sprite { get; private set; } public Sprite RadiationSprite { get; } @@ -134,6 +144,9 @@ namespace Barotrauma AllowAsBiomeGate = element.GetAttributeBool(nameof(AllowAsBiomeGate), true); AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true); + Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); + SecondaryFaction = element.GetAttributeIdentifier(nameof(SecondaryFaction), Identifier.Empty); + ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true); MissionIdentifiers = element.GetAttributeIdentifierArray("missionidentifiers", Array.Empty()).ToImmutableArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 1934e9c69..5e2b376b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -298,7 +298,7 @@ namespace Barotrauma //if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost if (CurrentLocation == null) { - FindStartLocation(l => l.Type.HasOutpost); + FindStartLocation(l => l.Type.HasOutpost && l.Type.OutpostTeam == CharacterTeamType.FriendlyNPC); } void FindStartLocation(Func predicate) @@ -313,25 +313,38 @@ namespace Barotrauma } } - StartLocation.SecondaryFaction = null; + StartLocation.SecondaryFaction = null; var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost); if (startOutpostFaction != null) { StartLocation.Faction = startOutpostFaction; - foreach (var connection in StartLocation.Connections) + } + foreach (var connection in StartLocation.Connections) + { + //force locations adjacent to the start location to have an outpost + //non-inhabited locations seem to be confusing to new players, particularly + //on the first round/mission when they still don't know how transitions between levels work + var otherLocation = connection.OtherLocation(StartLocation); + if (!otherLocation.HasOutpost()) { - var otherLocation = connection.OtherLocation(StartLocation); - if (otherLocation.HasOutpost() && otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) + if (LocationType.Prefabs.TryGet("outpost".ToIdentifier(), out LocationType outpostLocationType)) { - otherLocation.Faction = startOutpostFaction; + otherLocation.ChangeType(campaign, outpostLocationType); } } + + if (otherLocation.HasOutpost() && + otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC && + otherLocation.Type.Faction.IsEmpty) + { + otherLocation.Faction = startOutpostFaction; + } } System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); int loops = campaign.CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0); - if (loops == 0 && (campaign.Settings.Difficulty == GameDifficulty.Easy || campaign.Settings.Difficulty == GameDifficulty.Medium)) + if (loops == 0 && (campaign.Settings.WorldHostility == WorldHostilityOption.Low || campaign.Settings.WorldHostility == WorldHostilityOption.Medium)) { if (StartLocation != null) { @@ -705,10 +718,19 @@ namespace Barotrauma foreach (Location location in Locations) { location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome)); + location.TryAssignFactionBasedOnLocationType(campaign); if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { - location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); - location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient); + if (location.Type.Faction.IsEmpty) + { + //no faction defined in the location type, assign a random one + location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); + } + if (location.Type.SecondaryFaction.IsEmpty) + { + //no secondary faction defined in the location type, assign a random one + location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient); + } } location.CreateStores(force: true); } @@ -1502,6 +1524,10 @@ namespace Barotrauma LocationType prevLocationType = location.Type; LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First(); location.ChangeType(campaign, newLocationType); + + var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty); + location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier); + if (showNotifications && prevLocationType != location.Type) { var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier); @@ -1512,9 +1538,6 @@ namespace Barotrauma } } - var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty); - location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier); - var secondaryFactionIdentifier = subElement.GetAttributeIdentifier("secondaryfaction", Identifier.Empty); location.SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 657c3c83f..47f5bd9ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -634,9 +634,22 @@ namespace Barotrauma #endif Powered.UpdatePower(deltaTime); Item.UpdatePendingConditionUpdates(deltaTime); - foreach (Item item in Item.ItemList) + Item lastUpdatedItem = null; + try { - item.Update(deltaTime, cam); + foreach (Item item in Item.ItemList) + { + lastUpdatedItem = item; + item.Update(deltaTime, cam); + } + } + catch (InvalidOperationException e) + { + GameAnalyticsManager.AddErrorEventOnce( + "MapEntity.UpdateAll:ItemUpdateInvalidOperation", + GameAnalyticsManager.ErrorSeverity.Critical, + $"Error while updating item {lastUpdatedItem?.Name ?? "null"}: {e.Message}"); + throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e); } UpdateAllProjSpecific(deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 7bb55ccd6..18a0f177b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -131,6 +131,13 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the outpost generation parameters that should be used if this outpost has become critically irradiated."), Editable] public string ReplaceInRadiation { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "By default, sonar only shows the outline of the sub/outpost from the outside. Enable this if you want to see each structure individually."), Editable] + public bool AlwaysShowStructuresOnSonar + { + get; + set; + } + public ContentPath OutpostFilePath { get; set; } public class ModuleCount diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index ba0a62262..a7f726142 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -238,8 +238,7 @@ namespace Barotrauma foreach (Hull hull in Hull.HullList) { if (hull.Submarine != sub) { continue; } - if (string.IsNullOrEmpty(hull.RoomName) || - hull.RoomName.Contains("RoomName.", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(hull.RoomName)) { hull.RoomName = hull.CreateRoomName(); } @@ -877,16 +876,16 @@ namespace Barotrauma } } - if (availableModules.Count() == 0) { return null; } + if (!availableModules.Any()) { return null; } //try to search for modules made specifically for this location type first var modulesSuitableForLocationType = - availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); + availableModules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType)); //if not found, search for modules suitable for any location type if (!modulesSuitableForLocationType.Any()) { - modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); + modulesSuitableForLocationType = availableModules.Where(m => m.OutpostModuleInfo.IsAllowedInAnyLocationType()); } if (!modulesSuitableForLocationType.Any()) @@ -956,11 +955,12 @@ namespace Barotrauma { if (disallowNonLocationTypeSpecific) { + //don't use OutpostModuleInfo.IsLocationTypeAllowed here - we're trying to choose a module specifically for this location type, not modules suitable for any location type suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); } else { - suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier) || !m.OutpostModuleInfo.AllowedLocationTypes.Any()); + suitable = modules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType)); } } if (requireAllowAttachToPrevious && prevModule != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index 9a9405020..46330d892 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System; using System.Collections.Generic; using System.Linq; @@ -132,6 +133,16 @@ namespace Barotrauma this.allowedLocationTypes.Add(locationType); } } + public bool IsAllowedInAnyLocationType() + { + return allowedLocationTypes.None() || allowedLocationTypes.Contains("Any".ToIdentifier()); + } + + public bool IsAllowedInLocationType(LocationType locationType) + { + if (locationType == null || IsAllowedInAnyLocationType()) { return true; } + return allowedLocationTypes.Contains(locationType.Identifier); + } public void DetermineGapPositions(Submarine sub) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index cce8509ab..37e00e1b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -24,6 +24,8 @@ namespace Barotrauma public float damage; public Gap gap; + public bool NoPhysicsBody; + public Structure Wall { get; } public Vector2 Position => Wall.SectionPosition(Wall.Sections.IndexOf(this)); public Vector2 WorldPosition => Wall.SectionPosition(Wall.Sections.IndexOf(this), world: true); @@ -49,21 +51,18 @@ namespace Barotrauma public static List WallList = new List(); const float LeakThreshold = 0.1f; + const float BigGapThreshold = 0.7f; #if CLIENT public SpriteEffects SpriteEffects = SpriteEffects.None; #endif //dimensions of the wall sections' physics bodies (only used for debug rendering) - private readonly List bodyDebugDimensions = new List(); + private readonly Dictionary bodyDimensions = new Dictionary(); private static Explosion explosionOnBroken; -#if DEBUG [Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody)] -#else - [Serialize(false, IsPropertySaveable.Yes)] -#endif public bool Indestructible { get; @@ -596,7 +595,7 @@ namespace Barotrauma private void CreateStairBodies() { Bodies = new List(); - bodyDebugDimensions.Clear(); + bodyDimensions.Clear(); float stairAngle = MathHelper.ToRadians(Math.Min(Prefab.StairAngle, 75.0f)); @@ -621,7 +620,7 @@ namespace Barotrauma newBody.Position = ConvertUnits.ToSimUnits(stairPos) + BodyOffset * Scale; - bodyDebugDimensions.Add(new Vector2(bodyWidth, bodyHeight)); + bodyDimensions.Add(newBody, new Vector2(bodyWidth, bodyHeight)); Bodies.Add(newBody); } @@ -962,18 +961,22 @@ namespace Barotrauma return true; } - public void AddDamage(int sectionIndex, float damage, Character attacker = null, bool emitParticles = true) + public void AddDamage(int sectionIndex, float damage, Character attacker = null, bool emitParticles = true, bool createWallDamageProjectiles = false) { if (!Prefab.Body || Prefab.Platform || Indestructible) { return; } if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) { return; } var section = Sections[sectionIndex]; - + float prevDamage = section.damage; + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + { + SetDamage(sectionIndex, section.damage + damage, attacker); + } #if CLIENT if (damage > 0 && emitParticles) { - float dmg = Math.Min(MaxHealth - section.damage, damage); + float dmg = Math.Min(section.damage - prevDamage, damage); float particleAmount = MathHelper.Lerp(0, 25, MathUtils.InverseLerp(0, 100, dmg * Rand.Range(0.75f, 1.25f))); // Special case for very low but frequent dmg like plasma cutter: 10% chance for emitting a particle if (particleAmount < 1 && Rand.Value() < 0.10f) @@ -996,13 +999,13 @@ namespace Barotrauma var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle, position: particlePosFinal, velocity: Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); - if (particle == null) break; + if (particle == null) { break; } } } #endif if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - SetDamage(sectionIndex, section.damage + damage, attacker); + SetDamage(sectionIndex, section.damage + damage, attacker, createWallDamageProjectiles: createWallDamageProjectiles); } } @@ -1132,7 +1135,7 @@ namespace Barotrauma if (MathUtils.CircleIntersectsRectangle(transformedPos, attack.DamageRange, sectionRect)) { damageAmount = attack.GetStructureDamage(deltaTime); - AddDamage(i, damageAmount, attacker); + AddDamage(i, damageAmount, attacker, createWallDamageProjectiles: attack.CreateWallDamageProjectiles); #if CLIENT if (attack.EmitStructureDamageParticles) { @@ -1165,7 +1168,11 @@ namespace Barotrauma return new AttackResult(damageAmount, null); } - public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool isNetworkEvent = true, bool createExplosionEffect = true) + public void SetDamage(int sectionIndex, float damage, Character attacker = null, + bool createNetworkEvent = true, + bool isNetworkEvent = true, + bool createExplosionEffect = true, + bool createWallDamageProjectiles = false) { if (Submarine != null && Submarine.GodMode || (Indestructible && !isNetworkEvent)) { return; } if (!Prefab.Body) { return; } @@ -1173,6 +1180,8 @@ namespace Barotrauma damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth); + if (Sections[sectionIndex].NoPhysicsBody) { return; } + #if SERVER if (GameMain.Server != null && createNetworkEvent && damage != Sections[sectionIndex].damage) { @@ -1301,13 +1310,22 @@ namespace Barotrauma } var gap = Sections[sectionIndex].gap; - float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold)); + float damageRatio = MaxHealth <= 0.0f ? 0 : damage / MaxHealth; + float gapOpen = 0; + if (damageRatio > BigGapThreshold) + { + gapOpen = MathHelper.Lerp(0.35f, 0.75f, MathUtils.InverseLerp(BigGapThreshold, 1.0f, damageRatio)); + } + else if (damageRatio > LeakThreshold) + { + gapOpen = MathHelper.Lerp(0f, 0.35f, MathUtils.InverseLerp(LeakThreshold, BigGapThreshold, damageRatio)); + } gap.Open = gapOpen; //gap appeared or became much larger -> explosion effect if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom) { - CreateWallDamageExplosion(gap, attacker); + CreateWallDamageExplosion(gap, attacker, createWallDamageProjectiles); } } @@ -1337,7 +1355,7 @@ namespace Barotrauma UpdateSections(); } - private static void CreateWallDamageExplosion(Gap gap, Character attacker) + private static void CreateWallDamageExplosion(Gap gap, Character attacker, bool createProjectiles) { const float explosionRange = 500.0f; float explosionStrength = gap.Open; @@ -1367,31 +1385,52 @@ namespace Barotrauma { explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null); } + explosionOnBroken.CameraShake = 5.0f; explosionOnBroken.IgnoreCover = false; explosionOnBroken.OnlyInside = true; explosionOnBroken.DistanceFalloff = false; + explosionOnBroken.PlayDamageSounds = true; explosionOnBroken.DisableParticles(); } - + explosionOnBroken.CameraShake = 25.0f; explosionOnBroken.IgnoredCover = gap.ConnectedWall?.ToEnumerable(); - explosionOnBroken.Attack.Range = explosionRange * gap.Open; + explosionOnBroken.Attack.Range = explosionOnBroken.CameraShakeRange = explosionRange * gap.Open; explosionOnBroken.Attack.DamageMultiplier = explosionStrength; explosionOnBroken.Attack.Stun = MathHelper.Clamp(explosionStrength, 0.5f, 1.0f); explosionOnBroken.IgnoredCharacters.Clear(); if (attacker?.AIController is EnemyAIController) { explosionOnBroken.IgnoredCharacters.Add(attacker); } explosionOnBroken?.Explode(gap.WorldPosition, damageSource: null, attacker: attacker); + + if (createProjectiles) + { + if (ItemPrefab.Prefabs.TryGet("walldamageprojectile", out var projectilePrefab) && linkedHull != null) + { + float angle = gap.IsHorizontal ? + (linkedHull.WorldPosition.X < gap.WorldPosition.X ? MathHelper.Pi : 0) : + (linkedHull.WorldPosition.Y < gap.WorldPosition.Y ? -MathHelper.PiOver2 : MathHelper.PiOver2); + Spawner.AddItemToSpawnQueue(projectilePrefab, gap.WorldPosition, onSpawned: (item) => + { + item.body.SetTransformIgnoreContacts(item.body.SimPosition, angle); + var projectile = item.GetComponent(); + projectile?.Use(); + }); + } + } + #if CLIENT + SoundPlayer.PlaySound("Ricochet", gap.WorldPosition); if (linkedHull != null) { for (int i = 0; i <= 50; i++) { - Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y)); - var velocity = gap.IsHorizontal ? + var emitDirection = gap.IsHorizontal ? gap.linkedTo[0].WorldPosition.X < gap.WorldPosition.X ? -Vector2.UnitX : Vector2.UnitX : gap.linkedTo[0].WorldPosition.Y < gap.WorldPosition.Y ? -Vector2.UnitY : Vector2.UnitY; - velocity = new Vector2(velocity.X + Rand.Range(-0.2f, 0.2f), velocity.Y + Rand.Range(-0.2f, 0.2f)); - var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, velocity * Rand.Range(100.0f, 3000.0f), collisionIgnoreTimer: 0.1f); - if (particle == null) { break; } + Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y)); + emitDirection = new Vector2(emitDirection.X + Rand.Range(-0.2f, 0.2f), emitDirection.Y + Rand.Range(-0.2f, 0.2f)); + var shrapnelParticle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, emitDirection * Rand.Range(100.0f, 3000.0f), hullGuess: linkedHull, collisionIgnoreTimer: 0.1f); + var sparkParticle = GameMain.ParticleManager.CreateParticle("whitespark", particlePos, emitDirection * Rand.Range(1000.0f, 3000.0f), hullGuess: linkedHull, collisionIgnoreTimer: 0.05f); + if (shrapnelParticle == null || sparkParticle == null) { break; } } } #endif @@ -1416,7 +1455,7 @@ namespace Barotrauma GameMain.World.Remove(b); } Bodies.Clear(); - bodyDebugDimensions.Clear(); + bodyDimensions.Clear(); #if CLIENT convexHulls?.ForEach(ch => ch.Remove()); convexHulls?.Clear(); @@ -1454,8 +1493,27 @@ namespace Barotrauma if (hasHoles || !Bodies.Any()) { Body sensorBody = CreateRectBody(rect, createConvexHull: false); - sensorBody.CollisionCategories = Physics.CollisionRepair; - sensorBody.SetIsSensor(true); + sensorBody.CollisionCategories = Physics.CollisionRepairableWall; + } + + foreach (var section in Sections) + { + bool intersectsWithBody = false; + foreach (var body in Bodies) + { + var bodyRect = new Rectangle( + ConvertUnits.ToDisplayUnits(body.Position - bodyDimensions[body] / 2).ToPoint(), + ConvertUnits.ToDisplayUnits(bodyDimensions[body]).ToPoint()); + + Rectangle sectionRect = section.rect; + sectionRect.Y -= section.rect.Height; + if (bodyRect.Intersects(sectionRect)) + { + intersectsWithBody = true; + break; + } + } + section.NoPhysicsBody = !intersectsWithBody; } } @@ -1511,7 +1569,7 @@ namespace Barotrauma } Bodies.Add(newBody); - bodyDebugDimensions.Add(new Vector2(ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height))); + bodyDimensions.Add(newBody, new Vector2(ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height))); return newBody; } @@ -1534,7 +1592,7 @@ namespace Barotrauma StairDirection = StairDirection == Direction.Left ? Direction.Right : Direction.Left; Bodies.ForEach(b => GameMain.World.Remove(b)); Bodies.Clear(); - bodyDebugDimensions.Clear(); + bodyDimensions.Clear(); CreateStairBodies(); } @@ -1562,7 +1620,7 @@ namespace Barotrauma StairDirection = StairDirection == Direction.Left ? Direction.Right : Direction.Left; Bodies.ForEach(b => GameMain.World.Remove(b)); Bodies.Clear(); - bodyDebugDimensions.Clear(); + bodyDimensions.Clear(); CreateStairBodies(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index edce76390..be4caa5b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -188,7 +188,6 @@ namespace Barotrauma } return realWorldCrushDepth.Value; } - set { realWorldCrushDepth = value; } } /// @@ -976,7 +975,7 @@ namespace Barotrauma if (ignoreLevel && fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; } if (!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel) && !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) - && !fixture.CollisionCategories.HasFlag(Physics.CollisionRepair)) { return -1; } + && !fixture.CollisionCategories.HasFlag(Physics.CollisionRepairableWall)) { return -1; } if (ignoreSubs && fixture.Body.UserData is Submarine) { return -1; } if (ignoreBranches && fixture.Body.UserData is VineTile) { return -1; } if (fixture.Body.UserData as string == "ruinroom") { return -1; } @@ -1113,27 +1112,45 @@ namespace Barotrauma } public void EnableFactionSpecificEntities(Identifier factionIdentifier) + { + foreach (var faction in FactionPrefab.Prefabs) + { + SetLayerEnabled(faction.Identifier, faction.Identifier == factionIdentifier); + } + } + + public bool LayerExists(Identifier layer) { foreach (MapEntity me in MapEntity.MapEntityList) { - if (string.IsNullOrEmpty(me.Layer) || me.Submarine != this) { continue; } - - var layerAsIdentifier = me.Layer.ToIdentifier(); - if (FactionPrefab.Prefabs.ContainsKey(layerAsIdentifier)) - { - me.HiddenInGame = factionIdentifier != layerAsIdentifier; -#if CLIENT - //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that - if (me.HiddenInGame && me is Item item) - { - foreach (var lightComponent in item.GetComponents()) - { - lightComponent.Light.Enabled = false; - } - } -#endif - } + if (me.Submarine == this || me.Layer == layer) { return true; } } + return false; + } + + public void SetLayerEnabled(Identifier layer, bool enabled, bool sendNetworkEvent = false) + { + foreach (MapEntity me in MapEntity.MapEntityList) + { + if (string.IsNullOrEmpty(me.Layer) || me.Submarine != this || me.Layer != layer) { continue; } + me.HiddenInGame = !enabled; +#if CLIENT + //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that + if (me.HiddenInGame && me is Item item) + { + foreach (var lightComponent in item.GetComponents()) + { + lightComponent.Light.Enabled = false; + } + } +#endif + } +#if SERVER + if (sendNetworkEvent) + { + GameMain.Server.CreateEntityEvent(this, new SetLayerEnabledEventData(layer, enabled)); + } +#endif } public void Update(float deltaTime) @@ -1230,6 +1247,8 @@ namespace Barotrauma public void NeutralizeBallast() { + if (PhysicsBody.BodyType != BodyType.Dynamic) { return; } + float neutralBallastLevel = 0.5f; int selectedSteeringValue = 0; foreach (Item item in Item.ItemList) @@ -1240,8 +1259,8 @@ namespace Barotrauma //find how many pumps/engines in this sub the steering item is connected to int steeringValue = 1; - Connection connectionX = item.GetComponent()?.Connections.Find(c => c.Name == "velocity_x_out"); - Connection connectionY = item.GetComponent()?.Connections.Find(c => c.Name == "velocity_y_out"); + Connection connectionX = item.GetComponent()?.Connections.Find(static c => c.Name == "velocity_x_out"); + Connection connectionY = item.GetComponent()?.Connections.Find(static c => c.Name == "velocity_y_out"); if (connectionX != null) { foreach (Engine engine in steering.Item.GetConnectedComponentsRecursive(connectionX)) @@ -1371,7 +1390,7 @@ namespace Barotrauma } /// - /// Returns true if the sub is same as the other. + /// Returns true if the sub is same as the other, or connected to it via docking ports. /// public bool IsConnectedTo(Submarine otherSub) => this == otherSub || GetConnectedSubs().Contains(otherSub); @@ -1459,22 +1478,39 @@ namespace Barotrauma public static Rectangle GetBorders(XElement submarineElement) { - Vector4 bounds = Vector4.Zero; + Vector4 bounds = new Vector4(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue); foreach (XElement element in submarineElement.Elements()) { - if (element.Name != "Structure") { continue; } + if (element.Name == "Structure") + { + string name = element.GetAttributeString("name", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); + StructurePrefab prefab = Structure.FindPrefab(name, identifier); + if (prefab == null || !prefab.Body) { continue; } - string name = element.GetAttributeString("name", ""); - Identifier identifier = element.GetAttributeIdentifier("identifier", ""); - StructurePrefab prefab = Structure.FindPrefab(name, identifier); - if (prefab == null || !prefab.Body) { continue; } + var rect = element.GetAttributeRect("rect", Rectangle.Empty); + bounds = new Vector4( + Math.Min(rect.X, bounds.X), + Math.Max(rect.Y, bounds.Y), + Math.Max(rect.Right, bounds.Z), + Math.Min(rect.Y - rect.Height, bounds.W)); + } + else if (element.Name == "LinkedSubmarine") + { + Point dimensions = element.GetAttributePoint("dimensions", Point.Zero); + Point pos = element.GetAttributeVector2("pos", Vector2.Zero).ToPoint(); + bounds = new Vector4( + Math.Min(pos.X - dimensions.X / 2, bounds.X), + Math.Max(pos.Y + dimensions.Y / 2, bounds.Y), + Math.Max(pos.X + dimensions.X / 2, bounds.Z), + Math.Min(pos.Y - dimensions.Y / 2, bounds.W)); + } + } - var rect = element.GetAttributeRect("rect", Rectangle.Empty); - bounds = new Vector4( - Math.Min(rect.X, bounds.X), - Math.Max(rect.Y, bounds.Y), - Math.Max(rect.Right, bounds.Z), - Math.Min(rect.Y - rect.Height, bounds.W)); + if (bounds.X == float.MaxValue || bounds.Y == float.MinValue || bounds.Z == float.MinValue || bounds.W == float.MaxValue) + { + //no bounds found + return Rectangle.Empty; } return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W)); @@ -1667,6 +1703,11 @@ namespace Barotrauma } } + foreach (Identifier layer in Info.LayersHiddenByDefault) + { + SetLayerEnabled(layer, enabled: false); + } + GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged.Register(upgradeEventIdentifier, _ => ResetCrushDepth()); #if CLIENT @@ -1721,6 +1762,22 @@ namespace Barotrauma realWorldCrushDepth = null; } + /// + /// Normally crush depth is determined by the crush depths of the walls and upgrades applied on them. + /// This method forces the crush depths of all the walls to the specified value. + /// + /// + /// Depth in "real world" units (meters from the surface of Europa, the value you see on the nav terminal). + public void SetCrushDepth(float realWorldCrushDepth) + { + foreach (Structure structure in Structure.WallList) + { + if (structure.Submarine != this || !structure.HasBody || structure.Indestructible) { continue; } + structure.CrushDepth = realWorldCrushDepth; + } + this.realWorldCrushDepth = realWorldCrushDepth; + } + public static void RepositionEntities(Vector2 moveAmount, IEnumerable entities) { if (moveAmount.LengthSquared() < 0.00001f) { return; } @@ -1777,6 +1834,10 @@ namespace Barotrauma element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax)); element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience.ToString())); element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages))); + if (Info.LayersHiddenByDefault.Any()) + { + element.Add(new XAttribute("layerhiddenbydefault", string.Join(", ", Info.LayersHiddenByDefault))); + } if (Info.Type == SubmarineType.OutpostModule) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index c4948fc15..d15858f41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -504,7 +504,10 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { - if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine) { continue; } + if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine != CanEnterSubmarine.True) + { + continue; + } foreach (Limb limb in c.AnimController.Limbs) { @@ -567,7 +570,7 @@ namespace Barotrauma { buoyancy = MathHelper.Lerp(buoyancy, 0.1f, forceUpwardsTimer / ForceUpwardsDelay); } - return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f) * massRatio; + return new Vector2(0.0f, buoyancy * totalMass * 10.0f) * massRatio; } public void ApplyForce(Vector2 force) @@ -684,9 +687,26 @@ namespace Barotrauma private bool CheckCharacterCollision(Contact contact, Character character) { - //characters that can't enter the sub always collide regardless of gaps - if (!character.AnimController.CanEnterSubmarine) { return true; } if (character.Submarine != null) { return false; } + switch (character.AnimController.CanEnterSubmarine) + { + case CanEnterSubmarine.False: + //characters that can't enter the sub always collide regardless of gaps + return true; + case CanEnterSubmarine.Partial: + //characters that can partially enter the sub can poke their limbs inside, but not the collider + if (contact.FixtureB.Body == + character.AnimController.Collider.FarseerBody) + { + return true; + } + if (contact.FixtureB.Body.UserData is Limb limb && + !limb.Params.CanEnterSubmarine) + { + return true; + } + break; + } contact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2 points); @@ -717,17 +737,23 @@ namespace Barotrauma if (adjacentGap == null) { return true; } } - if (newHull != null) - { - CoroutineManager.Invoke(() => - { - if (character != null && !character.Removed) - { - character.AnimController.FindHull(newHull.WorldPosition, setSubmarine: true); - } - }); + if (character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial) + { + return contact.FixtureB.Body == character.AnimController.Collider.FarseerBody; + } + else + { + if (newHull != null) + { + CoroutineManager.Invoke(() => + { + if (character != null && !character.Removed) + { + character.AnimController.FindHull(newHull.WorldPosition, setSubmarine: true); + } + }); + } } - return false; } @@ -880,17 +906,12 @@ namespace Barotrauma } } -#if CLIENT - int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50); - for (int i = 0; i < particleAmount; i++) - { - GameMain.ParticleManager.CreateParticle("iceshards", - ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)), - Rand.Vector(Rand.Range(50.0f, 500.0f)) + impact.Velocity); - } -#endif + HandleLevelCollisionProjSpecific(impact); } + + partial void HandleLevelCollisionProjSpecific(Impact impact); + private void HandleSubCollision(Impact impact, Submarine otherSub) { Debug.Assert(otherSub != submarine); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index a71c4f3fb..8d0102c32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -234,6 +234,11 @@ namespace Barotrauma public readonly Dictionary> OutpostNPCs = new Dictionary>(); + /// + /// Names of layers that get automatically hidden when loading the sub + /// + public HashSet LayersHiddenByDefault { get; private set; } = new HashSet(); + //constructors & generation ---------------------------------------------------- public SubmarineInfo() { @@ -319,6 +324,7 @@ namespace Barotrauma IsManuallyOutfitted = original.IsManuallyOutfitted; Tags = original.Tags; OutpostGenerationParams = original.OutpostGenerationParams; + LayersHiddenByDefault = original.LayersHiddenByDefault; if (original.OutpostModuleInfo != null) { OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo); @@ -385,6 +391,12 @@ namespace Barotrauma RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0); RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0); var recommendedCrewExperience = SubmarineElement.GetAttributeIdentifier("recommendedcrewexperience", CrewExperienceLevel.Unknown.ToIdentifier()); + + foreach (Identifier hiddenLayer in SubmarineElement.GetAttributeIdentifierArray("layerhiddenbydefault", Array.Empty())) + { + LayersHiddenByDefault.Add(hiddenLayer); + } + // Backwards compatibility if (recommendedCrewExperience == "Beginner") { @@ -793,6 +805,13 @@ namespace Barotrauma characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); float price = Price; + + // Adjust by campaign difficulty settings + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + price *= campaign.Settings.ShipyardPriceMultiplier; + } + if (characterList.Any()) { if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index a8a0551db..90ad144f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -1060,7 +1060,8 @@ namespace Barotrauma Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType); WayPoint w = new WayPoint(spawnType == SpawnType.Path ? Type.WayPoint : Type.SpawnPoint, rect, submarine, idRemap.GetOffsetId(element)) { - spawnType = spawnType + spawnType = spawnType, + Layer = element.GetAttributeString(nameof(Layer), null) }; string idCardDescString = element.GetAttributeString("idcarddesc", ""); @@ -1115,7 +1116,8 @@ namespace Barotrauma element.Add(new XAttribute("ID", ID), new XAttribute("x", (int)(rect.X - Submarine.HiddenSubPosition.X)), new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)), - new XAttribute("spawn", spawnType)); + new XAttribute("spawn", spawnType), + new XAttribute(nameof(Layer), Layer ?? string.Empty)); if (SpawnType == SpawnType.ExitPoint) { element.Add(new XAttribute("exitpointsize", XMLExtensions.PointToString(ExitPointSize))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WreckConverter.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WreckConverter.cs new file mode 100644 index 000000000..8d1a6e711 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WreckConverter.cs @@ -0,0 +1,193 @@ +#nullable enable + +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + static class WreckConverter + { + private static readonly string[] itemsToRemove = + { + "circuitboxcomponent", + "wire", + }; + + public static XElement ConvertToWreck(XElement submarineElement) + { + ImmutableHashSet availableWreckContainerTags = ItemPrefab.Prefabs + .SelectMany(ip => ip.PreferredContainers.SelectMany(pc => pc.Primary.Union(pc.Secondary))) + .Where(t => !ItemPrefab.Prefabs.ContainsKey(t) && t.StartsWith("wreck")) + .ToImmutableHashSet(); + + bool monsterSpawnPointCreated = false; + + List warnings = new List(); + + var wreckElement = new XElement(submarineElement); + foreach (var element in wreckElement.Elements().ToList()) + { + var identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) + { + if (element.NameAsIdentifier() == "waypoint") + { + if (element.GetAttributeEnum("spawn", SpawnType.Path) != SpawnType.Human) { continue; } + if (element.GetAttributeIdentifier("job", Identifier.Empty) == Identifier.Empty) + { + element.SetAttributeValue("spawn", SpawnType.Enemy); + DebugConsole.NewMessage("Converted a non-job-specific spawnpoint to an enemy spawnpoint."); + monsterSpawnPointCreated = true; + } + else + { + element.SetAttributeValue("spawn", SpawnType.Corpse); + } + } + continue; + } + + //remove if set to be removed + var tags = element.GetAttributeIdentifierArray("tags", Array.Empty()); + if (itemsToRemove.Any(it => tags.Contains(it.ToIdentifier()))) + { + element.Remove(); + continue; + } + + bool tagsModified = false; + for (int i = 0; i < tags.Length; i++) + { + Identifier wreckTag = ("wreck" + tags[i]).ToIdentifier(); + if (availableWreckContainerTags.Contains(wreckTag)) + { + DebugConsole.NewMessage($"Replaced tag {tags[i]} with {wreckTag} in item \"{identifier}\"."); + tags[i] = wreckTag; + tagsModified = true; + } + } + if (tagsModified) + { + element.SetAttributeValue("tags", string.Join(",", tags.Select(t => t.ToString()))); + } + + Identifier[] wreckedIdentifiers = + { + (identifier + "wrecked").ToIdentifier(), + (identifier + "_wrecked").ToIdentifier(), + }; + + //turn to wrecked version if one is available + foreach (var wreckedIdentifier in wreckedIdentifiers) + { + var wreckedPrefab = MapEntityPrefab.FindByIdentifier(wreckedIdentifier); + if (wreckedPrefab == null) { continue; } + + var oldPrefab = MapEntityPrefab.FindByIdentifier(identifier); + element.SetAttributeValue("identifier", wreckedIdentifier); + float currentScale = element.GetAttributeFloat("scale", oldPrefab.Scale); + element.SetAttributeValue("scale", currentScale * (wreckedPrefab.Scale / oldPrefab.Scale)); + + if (wreckedPrefab is ItemPrefab wreckedItemPrefab) + { + //remove connections that don't exist in the wreck version + var originalConnectionPanelElement = element.GetChildElement(nameof(ConnectionPanel)); + var wreckedConnectionPanelElement = wreckedItemPrefab.ConfigElement.GetChildElement(nameof(ConnectionPanel)); + if (originalConnectionPanelElement != null && wreckedConnectionPanelElement != null) + { + foreach (var connectionElement in originalConnectionPanelElement.Elements().ToList()) + { + var elementName = connectionElement.NameAsIdentifier(); + if (elementName != "input" && elementName != "output") { continue; } + string connectionName = connectionElement.GetAttributeString("name", string.Empty); + if (wreckedConnectionPanelElement + .GetChildElements(connectionElement.Name.LocalName) + .None(c => c.GetAttributeString("name", string.Empty) == connectionName)) + { + connectionElement.Remove(); + } + } + } + } + else if (wreckedPrefab is StructurePrefab wreckedStructurePrefab) + { + //if the dimensions of the structures are different, rescale + //ignore small differences, they tend to be just irrelevant differences in how the sourcerect is scaled + const int MaximumSizeDifference = 5; + Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); + if (!wreckedStructurePrefab.ResizeHorizontal) + { + if (Math.Abs(wreckedStructurePrefab.ScaledSize.X - rect.Width) > MaximumSizeDifference) + { + DebugConsole.NewMessage($"The prefab {wreckedStructurePrefab.Name} has different dimensions than the original one. Changing the width from {rect.Width} to {(int)wreckedStructurePrefab.ScaledSize.X}.", Color.Yellow); + } + rect.Width = (int)wreckedStructurePrefab.ScaledSize.X; + } + if (!wreckedStructurePrefab.ResizeVertical) + { + if (Math.Abs(wreckedStructurePrefab.ScaledSize.Y - rect.Height) > MaximumSizeDifference) + { + DebugConsole.NewMessage($"The prefab {wreckedStructurePrefab.Name} has different dimensions than the original one. Changing the height from {rect.Height} to {(int)wreckedStructurePrefab.ScaledSize.Y}.", Color.Yellow); + } + rect.Height = (int)wreckedStructurePrefab.ScaledSize.Y; + } + element.SetAttributeValue("rect", XMLExtensions.RectToString(rect)); + } + break; + } + + var itemContainerElement = element.GetChildElement(nameof(ItemContainer)); + if (itemContainerElement != null) + { + string containedString = itemContainerElement.GetAttributeString("contained", ""); + string[] itemIdStrings = containedString.Split(','); + var itemIds = new HashSet(); + foreach (string idListStr in itemIdStrings) + { + foreach (string idStr in idListStr.Split(';')) + { + if (int.TryParse(idStr, out int id)) { itemIds.Add((UInt16)id); } + } + } + if (itemIds.Any()) + { + List containedItemNames = new List(); + foreach (var itemElement in wreckElement.Elements()) + { + var id = itemElement.GetAttributeUInt16("id", Entity.NullEntityID); + if (itemIds.Contains(id)) + { + containedItemNames.Add(itemElement.GetAttributeString("identifier", string.Empty)); + } + } + warnings.Add($"Potential issue in container \"{identifier}\". The following items are pre-placed, and may interfere with the loot generated in the wreck: " + string.Join(", ", containedItemNames)); + } + } + + //set to 0 condition if repairable, exclude doors and hatches + if (element.GetChildElement(nameof(Repairable)) != null && element.GetChildElement(nameof(Door)) == null) + { + element.SetAttributeValue("conditionpercentage", 0.0f); + } + } + + foreach (var warning in warnings) + { + DebugConsole.AddWarning(warning); + } + + if (!monsterSpawnPointCreated) + { + DebugConsole.ThrowError("There are no monster spawnpoints in the wreck. Remember to add some for monsters to spawn properly!"); + } + + return wreckElement; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index e232ae070..15968ee86 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -32,6 +32,12 @@ namespace Barotrauma.Networking public const float SpeakRange = 2000.0f; + /// + /// This is shorter than the text chat speak range, because the voice chat is still intelligible (just quiet) close to the maximum range, + /// while the text chat (which drops letters from the message) becomes unintelligible sooner + /// + public const float SpeakRangeVOIP = 1000.0f; + private static readonly string dateTimeFormatLongTimePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern; public static Color[] MessageColor = diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index e2fab1f74..9d2fbc195 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -162,7 +162,8 @@ namespace Barotrauma { typeof(AccountId), new ReadWriteBehavior(ReadAccountId, WriteAccountId) }, { typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) }, { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) }, - { typeof(SerializableDateTime), new ReadWriteBehavior(ReadSerializableDateTime, WriteSerializableDateTime) } + { typeof(SerializableDateTime), new ReadWriteBehavior(ReadSerializableDateTime, WriteSerializableDateTime) }, + { typeof(NetLimitedString), new ReadWriteBehavior(ReadNetLString, WriteNetLString) } }; private static readonly ImmutableDictionary, Func> BehaviorFactories = new Dictionary, Func> @@ -458,6 +459,12 @@ namespace Barotrauma private static double ReadDouble(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadDouble(); private static void WriteDouble(double b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteDouble(b); } + // We do not validate that the string read is within the max length, but do we need to? + // Modified client could send a network message with a really long string when we use NetLimitedString + // but they could also just do that for any other network message. + private static NetLimitedString ReadNetLString(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => new NetLimitedString(inc.ReadString()); + private static void WriteNetLString(NetLimitedString b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteString(b.Value); } + private static string ReadString(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadString(); private static void WriteString(string b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteString(b); } @@ -685,6 +692,7 @@ namespace Barotrauma /// float
/// double
/// string
+ ///
///
///
///
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index ef48e3162..ba54f79c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -210,15 +210,7 @@ namespace Barotrauma.Networking AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, senderClient, changeType: changeType, textColor: textColor)); } - public virtual void AddChatMessage(ChatMessage message) - { - if (string.IsNullOrEmpty(message.Text)) { return; } - - if (message.Sender != null && !message.Sender.IsDead) - { - message.Sender.ShowSpeechBubble(2.0f, message.Color); - } - } + public abstract void AddChatMessage(ChatMessage message); public static string ClientLogName(Client client, string name = null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index fd647fcab..f7c5235c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -31,7 +31,9 @@ namespace Barotrauma.Networking ///
public OrderChatMessage(Order order, Character targetCharacter, Character sender, bool isNewOrder = true) : this(order, - order?.GetChatMessage(targetCharacter?.Name, (order.TargetEntity as Hull ?? sender?.CurrentHull)?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), + order?.GetChatMessage(targetCharacter?.Name, + (order.TargetEntity as Hull ?? sender?.CurrentHull)?.DisplayName?.Value, + givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), targetCharacter, sender, isNewOrder) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs index 2ba983261..99cbc056e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -68,6 +68,7 @@ namespace Barotrauma.Networking { public string ServerName; public ImmutableArray ContentPackages; + public bool AllowModDownloads; } [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index b8e8965dc..cd9f1691f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -14,7 +14,14 @@ namespace Barotrauma.Networking /// /// How much skills drop towards the job's default skill levels when dying /// - public static float SkillLossPercentageOnDeath => GameMain.NetworkMember?.ServerSettings?.SkillLossPercentageOnDeath ?? 50.0f; + public static float SkillLossPercentageOnDeath => GameMain.NetworkMember?.ServerSettings?.SkillLossPercentageOnDeath ?? 20.0f; + + /// + /// How much more the skills drop towards the job's default skill levels + /// when dying, in addition to SkillLossPercentageOnDeath, if the player + /// chooses to respawn in the middle of the round + /// + public static float SkillLossPercentageOnImmediateRespawn => GameMain.NetworkMember?.ServerSettings?.SkillLossPercentageOnImmediateRespawn ?? 10.0f; public enum State { @@ -76,6 +83,10 @@ namespace Barotrauma.Networking private float updateReturnTimer; + public bool CanRespawnAgain => + /*can never respawn again if we're currently transporting and transport time is set to be infinite*/ + !(CurrentState == State.Transporting && maxTransportTime <= 0.0f); + public Submarine RespawnShuttle { get; private set; } public RespawnManager(NetworkMember networkMember, SubmarineInfo shuttleInfo) @@ -88,7 +99,7 @@ namespace Barotrauma.Networking RespawnShuttle = new Submarine(shuttleInfo, true); RespawnShuttle.PhysicsBody.FarseerBody.OnCollision += OnShuttleCollision; //set crush depth slightly deeper than the main sub's - RespawnShuttle.RealWorldCrushDepth = Math.Max(RespawnShuttle.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth * 1.2f); + RespawnShuttle.SetCrushDepth(Math.Max(RespawnShuttle.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth * 1.2f)); //prevent wifi components from communicating between the respawn shuttle and other subs List wifiComponents = new List(); @@ -340,25 +351,6 @@ namespace Barotrauma.Networking RespawnCharactersProjSpecific(shuttlePos); } - public static AfflictionPrefab GetRespawnPenaltyAfflictionPrefab() - { - return AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty"); - } - - public static Affliction GetRespawnPenaltyAffliction() - { - return GetRespawnPenaltyAfflictionPrefab()?.Instantiate(10.0f); - } - - public static void GiveRespawnPenaltyAffliction(Character character) - { - var respawnPenaltyAffliction = GetRespawnPenaltyAffliction(); - if (respawnPenaltyAffliction != null) - { - character.CharacterHealth.ApplyAffliction(targetLimb: null, respawnPenaltyAffliction); - } - } - public Vector2 FindSpawnPos() { if (Level.Loaded == null || Submarine.MainSub == null) { return Vector2.Zero; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index d890f70ca..ac443beb1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -49,8 +49,6 @@ namespace Barotrauma.Networking public enum NetFlags : byte { None = 0x0, - Name = 0x1, - Message = 0x2, Properties = 0x4, Misc = 0x8, LevelSeed = 0x10, @@ -320,24 +318,26 @@ namespace Barotrauma.Networking } } - private string serverName; + private string serverName = string.Empty; + + [Serialize("", IsPropertySaveable.Yes)] public string ServerName { get { return serverName; } set { - string val = value; - if (val.Length > NetConfig.ServerNameMaxLength) { val = val.Substring(0, NetConfig.ServerNameMaxLength); } - if (serverName == val) { return; } - serverName = val; + string newName = value; + if (newName.Length > NetConfig.ServerNameMaxLength) { newName = newName.Substring(0, NetConfig.ServerNameMaxLength); } + if (serverName == newName) { return; } + if (newName.IsNullOrWhiteSpace()) { return; } + serverName = newName; ServerDetailsChanged = true; -#if SERVER - UpdateFlag(NetFlags.Name); -#endif } } private string serverMessageText; + + [Serialize("", IsPropertySaveable.Yes)] public string ServerMessageText { get { return serverMessageText; } @@ -346,11 +346,11 @@ namespace Barotrauma.Networking string val = value; if (val.Length > NetConfig.ServerMessageMaxLength) { val = val.Substring(0, NetConfig.ServerMessageMaxLength); } if (serverMessageText == val) { return; } +#if SERVER + GameMain.Server?.SendChatMessage(TextManager.AddPunctuation(':', TextManager.Get("servermotd"), val).Value, ChatMessageType.Server); +#endif serverMessageText = val; ServerDetailsChanged = true; -#if SERVER - UpdateFlag(NetFlags.Message); -#endif } } @@ -433,7 +433,7 @@ namespace Barotrauma.Networking private set; } - [Serialize(50f, IsPropertySaveable.Yes)] + [Serialize(20f, IsPropertySaveable.Yes)] /// /// How much skills drop towards the job's default skill levels when dying /// @@ -443,6 +443,18 @@ namespace Barotrauma.Networking private set; } + [Serialize(10f, IsPropertySaveable.Yes)] + /// + /// How much more the skills drop towards the job's default skill levels + /// when dying, in addition to SkillLossPercentageOnDeath, if the player + /// chooses to respawn in the middle of the round + /// + public float SkillLossPercentageOnImmediateRespawn + { + get; + private set; + } + [Serialize(60.0f, IsPropertySaveable.Yes)] public float AutoRestartInterval { @@ -550,6 +562,7 @@ namespace Barotrauma.Networking } } + [Serialize(false, IsPropertySaveable.Yes)] public bool AutoRestart { get { return autoRestart; } @@ -625,6 +638,7 @@ namespace Barotrauma.Networking set; } + [Serialize(0.0f, IsPropertySaveable.Yes)] public float SelectedLevelDifficulty { get { return selectedLevelDifficulty; } @@ -753,13 +767,18 @@ namespace Barotrauma.Networking if (traitorDangerLevel == clampedValue) { return; } traitorDangerLevel = clampedValue; ServerDetailsChanged = true; +#if CLIENT + GameMain.NetLobbyScreen?.SetTraitorDangerLevel(traitorDangerLevel); +#endif } } + + private int traitorsMinPlayerCount; [Serialize(defaultValue: 1, isSaveable: IsPropertySaveable.Yes)] public int TraitorsMinPlayerCount { - get; - set; + get { return traitorsMinPlayerCount; } + set { traitorsMinPlayerCount = MathHelper.Clamp(value, 1, NetConfig.MaxPlayers); } } [Serialize(defaultValue: 50.0f, isSaveable: IsPropertySaveable.Yes)] @@ -865,7 +884,11 @@ namespace Barotrauma.Networking { karmaEnabled = value; #if CLIENT - if (karmaSettingsBlocker != null) { karmaSettingsBlocker.Visible = !karmaEnabled || karmaPresetDD.SelectedData as string != "custom"; } + if (karmaSettingsList != null) + { + SetElementInteractability(karmaSettingsList.Content, !karmaEnabled || KarmaPreset != "custom"); + } + karmaElements.ForEach(e => e.Visible = karmaEnabled); #endif } } @@ -1035,8 +1058,9 @@ namespace Barotrauma.Networking .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) .ToImmutableArray(); - public void ReadMonsterEnabled(IReadMessage inc) + public bool ReadMonsterEnabled(IReadMessage inc) { + bool changed = false; InitMonstersEnabled(); var monsterNames = ExtractAndSortKeys(MonsterEnabled); uint receivedMonsterCount = inc.ReadVariableUInt32(); @@ -1049,10 +1073,13 @@ namespace Barotrauma.Networking { foreach (Identifier s in monsterNames) { + MonsterEnabled.TryGetValue(s, out bool prevEnabled); MonsterEnabled[s] = inc.ReadBoolean(); + changed |= prevEnabled != MonsterEnabled[s]; } } inc.ReadPadBits(); + return changed; } public void WriteMonsterEnabled(IWriteMessage msg, Dictionary monsterEnabled = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs index 69232df45..d04088c0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs @@ -1,5 +1,4 @@ using FarseerPhysics.Dynamics; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -15,7 +14,7 @@ namespace Barotrauma public const Category CollisionItemBlocking = Category.Cat6; public const Category CollisionProjectile = Category.Cat7; public const Category CollisionLevel = Category.Cat8; - public const Category CollisionRepair = Category.Cat9; + public const Category CollisionRepairableWall = Category.Cat9; public static float DisplayToRealWorldRatio = 1.0f / 100.0f; @@ -61,7 +60,7 @@ namespace Barotrauma category = CollisionLevel; return true; case "repair": - category = CollisionRepair; + category = CollisionRepairableWall; return true; default: return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 7f9b856b1..8dc5e25ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -248,6 +248,11 @@ namespace Barotrauma get { return ConvertUnits.ToDisplayUnits(FarseerBody.Position); } } + /// + /// Offset of the DrawPosition from the Position (i.e. how much the interpolated draw position is offset from the "actual position"). In display units. + /// + public Vector2 DrawPositionOffset => DrawPosition - Position; + public Vector2 PrevPosition { get { return prevPosition; } @@ -936,6 +941,24 @@ namespace Barotrauma ApplyTorque(FarseerBody.Mass * torque); } } + + /// + /// Wraps the angle so it has "has the same number of revolutions" as this body, i.e. that the angles are at most 180 degrees apart. + /// For example, if the angle of this body was 720, an angle of 5 would get wrapped to 725. + /// + public float WrapAngleToSameNumberOfRevolutions(float angle) + { + if (float.IsInfinity(angle)) { return angle; } + while (Rotation - angle > MathHelper.TwoPi) + { + angle += MathHelper.TwoPi; + } + while (Rotation - angle < -MathHelper.TwoPi) + { + angle -= MathHelper.TwoPi; + } + return angle; + } public void Remove() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index 7a6e24432..c4e8b814e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -267,7 +267,7 @@ namespace Voronoi2 { Vector2 dir = Vector2.Normalize(Point1 - Point2); Vector2 normal = new Vector2(dir.Y, -dir.X); - if (cell != null && Vector2.Dot(normal, Vector2.Normalize(Center - cell.Center)) < 0) + if (cell != null && Vector2.Dot(normal, Vector2.Normalize(Center - (cell.Center - cell.Translation))) < 0) { normal = -normal; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs index 780e23939..106f3e879 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs @@ -31,8 +31,8 @@ namespace Barotrauma lastUpdateID++; } #elif CLIENT - levelDifficultyScrollBar.BarScroll = difficulty / 100.0f; - levelDifficultyScrollBar.OnMoved(levelDifficultyScrollBar, levelDifficultyScrollBar.BarScroll); + levelDifficultySlider.BarScroll = difficulty / 100.0f; + levelDifficultySlider.OnMoved(levelDifficultySlider, levelDifficultySlider.BarScroll); #endif } @@ -47,9 +47,6 @@ namespace Barotrauma GameMain.Server.ServerSettings.BotCount = botCount; lastUpdateID++; } -#endif -#if CLIENT - botCountText.Text = botCount.ToString(); #endif } @@ -61,15 +58,6 @@ namespace Barotrauma GameMain.Server.ServerSettings.BotSpawnMode = botSpawnMode; lastUpdateID++; } -#endif -#if CLIENT - - botSpawnModeText.Text = TextManager.Get(botSpawnMode.ToString()); - botSpawnModeText.ToolTip = TextManager.Get($"botspawnmode.{botSpawnMode}.tooltip") + "\n\n" + TextManager.Get("botspawn.campaignnote"); - foreach (var btn in botSpawnModeButtons) - { - btn.ToolTip = botSpawnModeText.ToolTip; - } #endif } @@ -79,10 +67,6 @@ namespace Barotrauma { GameMain.NetworkMember.ServerSettings.TraitorProbability = probability; } -#if CLIENT - traitorProbabilitySlider.BarScroll = probability; - traitorProbabilitySlider.OnMoved(traitorProbabilitySlider, traitorProbabilitySlider.BarScroll); -#endif } public void SetTraitorDangerLevel(int dangerLevel) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs index 1c26b59df..d32bd3fa8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs @@ -209,7 +209,7 @@ namespace Barotrauma PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value)); break; case "identifierarray": - PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value).ToIdentifiers().ToArray()); + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseIdentifierArray(value)); break; } } @@ -218,8 +218,6 @@ namespace Barotrauma DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}", e); return false; } - - return true; } @@ -295,7 +293,7 @@ namespace Barotrauma PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value)); return true; case "identifierarray": - PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value).ToIdentifiers().ToArray()); + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseIdentifierArray((string)value)); return true; default: DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}"); @@ -1089,7 +1087,7 @@ namespace Barotrauma { case "requireditem": case "requireditems": - itemComponent.requiredItems.Clear(); + itemComponent.RequiredItems.Clear(); itemComponent.DisabledRequiredItems.Clear(); itemComponent.SetRequiredItems(element, allowEmpty: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index b21c9327e..3df639213 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -1041,6 +1041,12 @@ namespace Barotrauma public static bool IsOverride(this XElement element) => element.NameAsIdentifier() == "override"; + /// + /// Get the root element of the document, or the first child element of the root if it's an override element. + /// Or in other words, the "rootmost element that actually contains some content". + /// + public static XElement GetRootExcludingOverride(this XDocument doc) => doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + public static XElement FirstElement(this XElement element) => element.Elements().FirstOrDefault(); public static XAttribute GetAttribute(this XElement element, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => element.GetAttribute(a => a.Name.ToString().Equals(name, comparisonMethod)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 12cad8921..68e231cb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -41,6 +41,13 @@ namespace Barotrauma BossHealthBarsOnly, HideAll } + + public enum InteractionLabelDisplayMode + { + Everything, + InteractionAvailable, + LooseItems + } public static class GameSettings { @@ -67,6 +74,8 @@ namespace Barotrauma RemoteMainMenuContentUrl = "https://www.barotraumagame.com/gamedata/", AimAssistAmount = DefaultAimAssist, ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll, + ChatSpeechBubbles = true, + InteractionLabelDisplayMode = InteractionLabelDisplayMode.Everything, EnableMouseLook = true, ChatOpen = true, CrewMenuOpen = true, @@ -143,6 +152,8 @@ namespace Barotrauma public float AimAssistAmount; public bool EnableMouseLook; public EnemyHealthBarMode ShowEnemyHealthBars; + public bool ChatSpeechBubbles; + public InteractionLabelDisplayMode InteractionLabelDisplayMode; public bool ChatOpen; public bool CrewMenuOpen; public bool ShowOffensiveServerPrompt; @@ -301,6 +312,7 @@ namespace Barotrauma { InputType.Health, Keys.H }, { InputType.Ragdoll, Keys.Space }, { InputType.Aim, MouseButton.SecondaryMouse }, + { InputType.DropItem, Keys.None }, { InputType.InfoTab, Keys.Tab }, { InputType.Chat, Keys.None }, @@ -314,6 +326,7 @@ namespace Barotrauma { InputType.LocalVoice, Keys.None }, { InputType.ToggleChatMode, Keys.R }, { InputType.Command, MouseButton.MiddleMouse }, + { InputType.ContextualCommand, Keys.LeftShift }, { InputType.PreviousFireMode, MouseButton.MouseWheelDown }, { InputType.NextFireMode, MouseButton.MouseWheelUp }, @@ -332,7 +345,8 @@ namespace Barotrauma { InputType.Use, Keys.E }, { InputType.Select, MouseButton.PrimaryMouse }, { InputType.Deselect, MouseButton.SecondaryMouse }, - { InputType.Shoot, MouseButton.PrimaryMouse } + { InputType.Shoot, MouseButton.PrimaryMouse }, + { InputType.ShowInteractionLabels, Keys.LeftAlt } }.ToImmutableDictionary(); public static KeyMapping GetDefault() => new KeyMapping @@ -382,6 +396,13 @@ namespace Barotrauma { foreach (var savedBinding in savedBindings) { + if (savedBinding.Key is InputType.Run or InputType.TakeHalfFromInventorySlot && + defaultBinding.Key == InputType.ContextualCommand) + { + //run and contextual commands have always defaulted to Shift, but the latter used to be hard-coded. + //don't show a warning about those being bound to the same key + continue; + } if (savedBinding.Value == defaultBinding.Value) { OnGameMainHasLoaded += () => diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 3c34fd89a..6968ad80f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -99,7 +99,12 @@ namespace Barotrauma /// /// Check against the target's limb type. See . /// - LimbType + LimbType, + + /// + /// Check against the current World Hostility setting (previously known as "Difficulty"). + /// + WorldHostility } public enum LogicalOperatorType @@ -168,6 +173,8 @@ namespace Barotrauma public readonly ImmutableArray AttributeValueAsTags; public readonly float? FloatValue; + private readonly WorldHostilityOption cachedHostilityValue; + /// /// If set to the name of one of the target's ItemComponents, the conditionals defined by this element check against the properties of that component. /// Only works on items. @@ -176,7 +183,7 @@ namespace Barotrauma /// /// If set to true, the conditionals defined by this element check against the attacking character instead of the attacked character. - /// Only applies to a character's attacks. + /// Only applies to a character's attacks and targeting parameters. /// public readonly bool TargetSelf; @@ -287,6 +294,11 @@ namespace Barotrauma { FloatValue = value; } + + if (Type == ConditionType.WorldHostility && Enum.TryParse(AttributeValue, ignoreCase: true, out WorldHostilityOption hostilityValue)) + { + cachedHostilityValue = hostilityValue; + } } public static (ComparisonOperatorType ComparisonOperator, string ConditionStr) ExtractComparisonOperatorFromConditionString(string str) @@ -394,6 +406,11 @@ namespace Barotrauma { return PropertyMatchesRequirement(target, property); } + else if (targetChar?.SerializableProperties != null + && targetChar.SerializableProperties.TryGetValue(AttributeName, out var characterProperty)) + { + return PropertyMatchesRequirement(targetChar, characterProperty); + } return ComparisonOperatorIsNotEquals; case ConditionType.SkillRequirement: if (targetChar != null) @@ -408,30 +425,47 @@ namespace Barotrauma case ConditionType.HasStatusTag: if (target == null) { return ComparisonOperatorIsNotEquals; } - // NOTE: This can be optimized further by returning - // when a match passes with the Equals operator and - // when a match fails with the NotEquals operator. - // The current form has better readability. - int numMatchingEffects = 0; - int numEffectsAffectingTarget = 0; - - foreach (var durationEffect in StatusEffect.DurationList) + int numTagsFound = 0; + foreach (var tag in AttributeValueAsTags) { - if (!durationEffect.Targets.Contains(target)) { continue; } - numEffectsAffectingTarget++; - if (StatusEffectMatchesTagCondition(durationEffect.Parent)) { numMatchingEffects++; } + bool tagFound = false; + foreach (var durationEffect in StatusEffect.DurationList) + { + if (!durationEffect.Targets.Contains(target)) { continue; } + if (durationEffect.Parent.HasTag(tag)) + { + tagFound = true; + break; + } + } + if (!tagFound) + { + foreach (var delayedEffect in DelayedEffect.DelayList) + { + if (!delayedEffect.Targets.Contains(target)) { continue; } + if (delayedEffect.Parent.HasTag(tag)) + { + tagFound = true; + break; + } + } + } + if (tagFound) + { + numTagsFound++; + } } - - foreach (var delayedEffect in DelayedEffect.DelayList) - { - if (!delayedEffect.Targets.Contains(target)) { continue; } - numEffectsAffectingTarget++; - if (StatusEffectMatchesTagCondition(delayedEffect.Parent)) { numMatchingEffects++; } - } - return ComparisonOperatorIsNotEquals - ? numMatchingEffects >= numEffectsAffectingTarget // true when none of the effects have any of the tags - : numMatchingEffects > 0; // true when any effects have all tags + ? numTagsFound < AttributeValueAsTags.Length // true when some tag wasn't found + : numTagsFound >= AttributeValueAsTags.Length; // true when all the tags are found + case ConditionType.WorldHostility: + { + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + return Compare(campaign.Settings.WorldHostility, cachedHostilityValue, ComparisonOperator); + } + return false; + } default: bool equals = CheckOnlyEquality(target); return ComparisonOperatorIsNotEquals @@ -538,16 +572,6 @@ namespace Barotrauma return SufficientTagMatches(matches); } - private bool StatusEffectMatchesTagCondition(StatusEffect statusEffect) - { - int matches = 0; - foreach (var tag in AttributeValueAsTags) - { - if (statusEffect.HasTag(tag)) { matches++; } - } - return SufficientTagMatches(matches); - } - private bool NumberMatchesRequirement(float testedValue) { if (!FloatValue.HasValue) { return ComparisonOperatorIsNotEquals; } @@ -624,5 +648,17 @@ namespace Barotrauma } } + public static bool Compare(T leftValue, T rightValue, ComparisonOperatorType comparisonOperator) where T : IComparable + { + return comparisonOperator switch + { + ComparisonOperatorType.NotEquals => leftValue.CompareTo(rightValue) != 0, + ComparisonOperatorType.GreaterThan => leftValue.CompareTo(rightValue) > 0, + ComparisonOperatorType.LessThan => leftValue.CompareTo(rightValue) < 0, + ComparisonOperatorType.GreaterThanEquals => leftValue.CompareTo(rightValue) >= 0, + ComparisonOperatorType.LessThanEquals => leftValue.CompareTo(rightValue) <= 0, + _ => leftValue.CompareTo(rightValue) == 0, + }; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 67226d137..24575372e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -132,11 +132,11 @@ namespace Barotrauma public enum SpawnPositionType { /// - /// The position of the StatusEffect's target. + /// The position of the entity (item, character, limb) the StatusEffect is defined in. /// This, /// - /// The inventory of the StatusEffect's target. + /// The inventory of the entity (item, character, limb) the StatusEffect is defined in. /// ThisInventory, /// @@ -146,18 +146,26 @@ namespace Barotrauma /// /// The inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory) /// - ContainedInventory + ContainedInventory, + /// + /// The position of the entity the StatusEffect is targeting. If there are multiple targets, an item is spawned at all of them. + /// + Target } public enum SpawnRotationType { /// - /// Fixed rotation specified using the Rotation attribute. + /// Neutral (0) rotation. Can be rotated further using the Rotation attribute. /// - Fixed, + None, /// /// The rotation of the entity executing the StatusEffect /// + This, + /// + /// The rotation from the position of the spawned entity to the target of the StatusEffect + /// Target, /// /// The rotation of the limb executing the StatusEffect, or the limb the StatusEffect is targeting @@ -270,8 +278,16 @@ namespace Barotrauma Equip = element.GetAttributeBool("equip", false); SpawnPosition = element.GetAttributeEnum("spawnposition", SpawnPositionType.This); - RotationType = element.GetAttributeEnum("rotationtype", RotationRad != 0 ? SpawnRotationType.Fixed : SpawnRotationType.Target); + if (element.GetAttributeString("rotationtype", string.Empty).Equals("Fixed", StringComparison.OrdinalIgnoreCase)) + { + //backwards compatibility, "This" was previously (inaccurately) called "Fixed" + RotationType = SpawnRotationType.This; + } + else + { + RotationType = element.GetAttributeEnum("rotationtype", RotationRad != 0 ? SpawnRotationType.This : SpawnRotationType.Target); + } InheritEventTags = element.GetAttributeBool(nameof(InheritEventTags), false); } } @@ -331,12 +347,23 @@ namespace Barotrauma /// Should the talents that trigger when the character gains skills be triggered by the effect? /// public readonly bool TriggerTalents; + /// + /// Should the amount be multiplied by delta time? Useful if you want to give a skill increase per frame. + /// + public readonly bool UseDeltaTime; + /// + /// Should the amount be inversely proportional to the current skill level? + /// Meaning, the higher the skill level, the less the skill is increased. + /// + public readonly bool Proportional; public GiveSkill(ContentXElement element, string parentDebugName) { SkillIdentifier = element.GetAttributeIdentifier(nameof(SkillIdentifier), Identifier.Empty); Amount = element.GetAttributeFloat(nameof(Amount), 0); TriggerTalents = element.GetAttributeBool(nameof(TriggerTalents), true); + UseDeltaTime = element.GetAttributeBool(nameof(UseDeltaTime), false); + Proportional = element.GetAttributeBool(nameof(Proportional), false); if (SkillIdentifier == Identifier.Empty) { @@ -692,7 +719,7 @@ namespace Barotrauma /// In other words, when enabled, the strength of the affliction(s) caused by this effect is higher on higher-vitality characters. /// Can be used to make characters take the same relative amount of damage regardless of their maximum vitality. /// - private readonly bool? multiplyAfflictionsByMaxVitality; + private readonly bool multiplyAfflictionsByMaxVitality; public IEnumerable SpawnCharacters { @@ -704,7 +731,11 @@ namespace Barotrauma private readonly List talentTriggers; private readonly List giveExperiences; private readonly List giveSkills; - + + private HashSet<(Character targetCharacter, AnimLoadInfo anim)> failedAnimations; + public readonly record struct AnimLoadInfo(AnimationType Type, Either File, float Priority, ImmutableArray ExpectedSpeciesNames); + private readonly List animationsToTrigger; + /// /// How long the effect runs (in seconds). Note that if is true, /// there can be multiple instances of the effect running at a time. @@ -805,6 +836,11 @@ namespace Barotrauma targetTypes |= targetType; } } + if (targetTypes == 0) + { + string errorMessage = $"Potential error in StatusEffect ({parentDebugName}). Target not defined, the effect might not work correctly. Use target=\"This\" if you want the effect to target the entity it's defined in. Setting \"This\" as the target."; + DebugConsole.AddSafeError(errorMessage); + } var targetIdentifiers = element.GetAttributeIdentifierArray(Array.Empty(), "targetnames", "targets", "targetidentifiers", "targettags"); if (targetIdentifiers.Any()) @@ -815,15 +851,8 @@ namespace Barotrauma triggeredEventTargetTag = element.GetAttributeIdentifier("eventtargettag", triggeredEventTargetTag); triggeredEventEntityTag = element.GetAttributeIdentifier("evententitytag", triggeredEventEntityTag); triggeredEventUserTag = element.GetAttributeIdentifier("eventusertag", triggeredEventUserTag); - spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); - - var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality)); - if (multiplyAfflictionsElement != null) - { - multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false); - } - + multiplyAfflictionsByMaxVitality = element.GetAttributeBool(nameof(multiplyAfflictionsByMaxVitality), false); #if CLIENT playSoundOnRequiredItemFailure = element.GetAttributeBool("playsoundonrequireditemfailure", false); #endif @@ -992,11 +1021,13 @@ namespace Barotrauma continue; } } - - Affliction afflictionInstance = afflictionPrefab.Instantiate(subElement.GetAttributeFloat(1.0f, "amount", "strength")); - afflictionInstance.Probability = subElement.GetAttributeFloat(1.0f, "probability"); + + Affliction afflictionInstance = afflictionPrefab.Instantiate(subElement.GetAttributeFloat(1.0f, "amount", nameof(afflictionInstance.Strength))); + // Deserializing the object normally might cause some unexpected side effects. At least it clamps the strength of the affliction, which we don't want here. + // Could probably be solved by using the NonClampedStrength or by bypassing the clamping, but ran out of time and played it safe here. + afflictionInstance.Probability = subElement.GetAttributeFloat(1.0f, nameof(afflictionInstance.Probability)); + afflictionInstance.MultiplyByMaxVitality = subElement.GetAttributeBool(nameof(afflictionInstance.MultiplyByMaxVitality), false); Afflictions.Add(afflictionInstance); - break; case "reduceaffliction": if (subElement.GetAttribute("name") != null) @@ -1083,6 +1114,25 @@ namespace Barotrauma giveSkills ??= new List(); giveSkills.Add(new GiveSkill(subElement, parentDebugName)); break; + case "triggeranimation": + AnimationType animType = subElement.GetAttributeEnum("type", def: AnimationType.NotDefined); + string fileName = subElement.GetAttributeString("filename", def: null) ?? subElement.GetAttributeString("file", def: null); + Either file = fileName != null ? fileName.ToLowerInvariant() : subElement.GetAttributeContentPath("path"); + if (!file.TryGet(out string _)) + { + if (!file.TryGet(out ContentPath _) || (file.TryGet(out ContentPath contentPath) && contentPath.IsNullOrWhiteSpace())) + { + DebugConsole.ThrowError($"Error in a element of {subElement.ParseContentPathFromUri()}: neither path nor filename defined!"); + } + } + else + { + float priority = subElement.GetAttributeFloat("priority", def: 0f); + Identifier[] expectedSpeciesNames = subElement.GetAttributeIdentifierArray("expectedspecies", Array.Empty()); + animationsToTrigger ??= new List(); + animationsToTrigger.Add(new AnimLoadInfo(animType, file, priority, expectedSpeciesNames.ToImmutableArray())); + } + break; } } InitProjSpecific(element, parentDebugName); @@ -1181,6 +1231,8 @@ namespace Barotrauma { foreach (Powered powered in Powered.PoweredList) { + //make sure we didn't already add this item due to it having some other Powered component + if (targets.Contains(powered)) { continue; } Item item = powered.Item; if (!item.Removed && CheckDistance(item) && IsValidTarget(item)) { @@ -1546,6 +1598,8 @@ namespace Barotrauma } if (ShouldWaitForInterval(entity, deltaTime)) { return; } + Item parentItem = entity as Item; + PhysicsBody parentItemBody = parentItem?.body; Hull hull = GetHull(entity); Vector2 position = GetPosition(entity, targets, worldPosition); if (useItemCount > 0) @@ -1610,7 +1664,28 @@ namespace Barotrauma } } } - + if (removeItem) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item item) { Entity.Spawner?.AddItemToRemoveQueue(item); } + } + } + if (removeCharacter) + { + for (int i = 0; i < targets.Count; i++) + { + var target = targets[i]; + if (target is Character character) + { + Entity.Spawner?.AddEntityToRemoveQueue(character); + } + else if (target is Limb limb) + { + Entity.Spawner?.AddEntityToRemoveQueue(limb.character); + } + } + } if (breakLimb || hideLimb) { for (int i = 0; i < targets.Count; i++) @@ -1719,8 +1794,8 @@ namespace Barotrauma RegisterTreatmentResults(user, entity as Item, limb, affliction, result); } } - - foreach (var (affliction, amount) in ReduceAffliction) + + foreach ((Identifier affliction, float amount) in ReduceAffliction) { Limb targetLimb = null; Character targetCharacter = null; @@ -1741,11 +1816,11 @@ namespace Barotrauma float prevVitality = targetCharacter.Vitality; if (targetLimb != null) { - targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, attacker: user, treatmentAction: actionType); } else { - targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, attacker: user, treatmentAction: actionType); } if (!targetCharacter.IsDead) { @@ -1808,6 +1883,8 @@ namespace Barotrauma } } } + + TryTriggerAnimation(target, entity); if (isNotClient) { @@ -1826,13 +1903,23 @@ namespace Barotrauma if (giveSkills != null) { - foreach (GiveSkill giveSkill in giveSkills) + Character targetCharacter = CharacterFromTarget(target); + if (targetCharacter is { Removed: false }) { - Character targetCharacter = CharacterFromTarget(target); - if (targetCharacter != null && !targetCharacter.Removed) + foreach (GiveSkill giveSkill in giveSkills) { Identifier skillIdentifier = giveSkill.SkillIdentifier == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier; - targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount, !giveSkill.TriggerTalents); + float amount = giveSkill.UseDeltaTime ? giveSkill.Amount * deltaTime : giveSkill.Amount; + + if (giveSkill.Proportional) + { + targetCharacter.Info?.ApplySkillGain(skillIdentifier, amount, !giveSkill.TriggerTalents); + } + else + { + targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, amount, !giveSkill.TriggerTalents); + } + Identifier GetRandomSkill() { return targetCharacter.Info?.Job?.GetSkills().GetRandomUnsynced()?.Identifier ?? Identifier.Empty; @@ -1890,10 +1977,9 @@ namespace Barotrauma { foreach (EventPrefab eventPrefab in triggeredEvents) { - Event ev = eventPrefab.CreateInstance(); + Event ev = eventPrefab.CreateInstance(eventManager.RandomSeed); if (ev == null) { continue; } - eventManager.QueuedEvents.Enqueue(ev); - + eventManager.QueuedEvents.Enqueue(ev); if (ev is ScriptedEvent scriptedEvent) { if (!triggeredEventTargetTag.IsEmpty) @@ -1987,11 +2073,11 @@ namespace Barotrauma } } if (i == characterSpawnInfo.Count) // Only perform the below actions if this is the last character being spawned. - { + { if (characterSpawnInfo.TransferControl) { #if CLIENT - if (Character.Controlled == target) + if (Character.Controlled == target) { Character.Controlled = newCharacter; } @@ -2002,7 +2088,7 @@ namespace Barotrauma GameMain.Server.SetClientCharacter(c, newCharacter); } #endif - } + } if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); } } } @@ -2021,13 +2107,17 @@ namespace Barotrauma } } - if (spawnItems != null) + if (spawnItems != null && spawnItems.Count > 0) { if (spawnItemRandomly) { if (spawnItems.Count > 0) { - SpawnItem(spawnItems.GetRandomUnsynced()); + var randomSpawn = spawnItems.GetRandomUnsynced(); + for (int i = 0; i < randomSpawn.Count; i++) + { + ProcessItemSpawnInfo(randomSpawn); + } } } else @@ -2036,269 +2126,33 @@ namespace Barotrauma { for (int i = 0; i < itemSpawnInfo.Count; i++) { - SpawnItem(itemSpawnInfo); + ProcessItemSpawnInfo(itemSpawnInfo); } } } - } - void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo) - { - Item parentItem = entity as Item; - if (user == null && parentItem != null) + void ProcessItemSpawnInfo(ItemSpawnInfo spawnInfo) { - // Set the user for projectiles spawned from status effects (e.g. flak shrapnels) - SetUser(parentItem.GetComponent()?.User); - } - switch (chosenItemSpawnInfo.SpawnPosition) - { - case ItemSpawnInfo.SpawnPositionType.This: - Entity.Spawner?.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => - { - Projectile projectile = newItem.GetComponent(); - if (entity != null) - { - var rope = newItem.GetComponent(); - if (rope != null && sourceBody != null && sourceBody.UserData is Limb sourceLimb) - { - rope.Attach(sourceLimb, newItem); -#if SERVER - newItem.CreateServerEvent(rope); -#endif - } - float spread = Rand.Range(-chosenItemSpawnInfo.AimSpreadRad, chosenItemSpawnInfo.AimSpreadRad); - float rotation = chosenItemSpawnInfo.RotationRad; - Vector2 worldPos; - if (sourceBody != null) - { - worldPos = sourceBody.Position; - if (user?.Submarine != null) - { - worldPos += user.Submarine.Position; - } - } - else - { - worldPos = entity.WorldPosition; - } - switch (chosenItemSpawnInfo.RotationType) - { - case ItemSpawnInfo.SpawnRotationType.Fixed: - if (sourceBody != null) - { - rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.RotationRad); - } - else if (parentItem?.body != null) - { - rotation = parentItem.body.TransformRotation(chosenItemSpawnInfo.RotationRad); - } - break; - case ItemSpawnInfo.SpawnRotationType.Target: - rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); - break; - case ItemSpawnInfo.SpawnRotationType.Limb: - if (sourceBody != null) - { - rotation = sourceBody.TransformedRotation; - } - break; - case ItemSpawnInfo.SpawnRotationType.Collider: - if (parentItem?.body != null) - { - rotation = parentItem.body.Rotation; - } - else if (user != null) - { - rotation = user.AnimController.Collider.Rotation + MathHelper.PiOver2; - } - break; - case ItemSpawnInfo.SpawnRotationType.MainLimb: - if (user != null) - { - rotation = user.AnimController.MainLimb.body.TransformedRotation; - } - break; - case ItemSpawnInfo.SpawnRotationType.Random: - if (projectile != null) - { - DebugConsole.LogError("Random rotation is not supported for Projectiles."); - } - else - { - rotation = Rand.Range(0f, MathHelper.TwoPi, Rand.RandSync.Unsynced); - } - break; - default: - throw new NotImplementedException("Item spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType); - } - if (user != null) - { - rotation += chosenItemSpawnInfo.RotationRad * user.AnimController.Dir; - } - rotation += spread; - if (projectile != null) - { - var sourceEntity = (sourceBody?.UserData as ISpatialEntity) ?? entity; - Vector2 spawnPos = sourceEntity.SimPosition; - projectile.Shoot(user, spawnPos, spawnPos, rotation, - ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); - projectile.Item.Submarine = projectile.LaunchSub = sourceEntity?.Submarine; - } - else if (newItem.body != null) - { - newItem.body.SetTransform(newItem.SimPosition, rotation); - Vector2 impulseDir = new Vector2(MathF.Cos(rotation), MathF.Sin(rotation)); - newItem.body.ApplyLinearImpulse(impulseDir * chosenItemSpawnInfo.Impulse); - } - } - OnItemSpawned(newItem, chosenItemSpawnInfo); - }); - break; - case ItemSpawnInfo.SpawnPositionType.ThisInventory: - { - Inventory inventory = null; - if (entity is Character character && character.Inventory != null) - { - inventory = character.Inventory; - } - else if (entity is Item item) - { - foreach (ItemContainer itemContainer in item.GetComponents()) - { - if (itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) - { - inventory = itemContainer?.Inventory; - break; - } - } - if (!chosenItemSpawnInfo.SpawnIfCantBeContained && inventory == null) - { - return; - } - } - if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) - { - Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item => - { - if (chosenItemSpawnInfo.Equip && entity is Character character && character.Inventory != null) - { - //if the item is both pickable and wearable, try to wear it instead of picking it up - List allowedSlots = - item.GetComponents().Count() > 1 ? - new List(item.GetComponent()?.AllowedSlots ?? item.GetComponent().AllowedSlots) : - new List(item.AllowedSlots); - allowedSlots.Remove(InvSlotType.Any); - character.Inventory.TryPutItem(item, null, allowedSlots); - } - OnItemSpawned(item, chosenItemSpawnInfo); - }); - } - } - break; - case ItemSpawnInfo.SpawnPositionType.SameInventory: - { - Inventory inventory = null; - if (entity is Character character) - { - inventory = character.Inventory; - } - else if (entity is Item item) - { - inventory = item.ParentInventory; - } - if (inventory != null) - { - Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => - { - OnItemSpawned(newItem, chosenItemSpawnInfo); - }); - } - else if (chosenItemSpawnInfo.SpawnIfNotInInventory) - { - Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position, onSpawned: (Item newItem) => - { - OnItemSpawned(newItem, chosenItemSpawnInfo); - }); - } - } - break; - case ItemSpawnInfo.SpawnPositionType.ContainedInventory: - { - Inventory thisInventory = null; - if (entity is Character character) - { - thisInventory = character.Inventory; - } - else if (entity is Item item) - { - var itemContainer = item.GetComponent(); - thisInventory = itemContainer?.Inventory; - if (!chosenItemSpawnInfo.SpawnIfCantBeContained && !itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) - { - return; - } - } - if (thisInventory != null) - { - foreach (Item item in thisInventory.AllItems) - { - Inventory containedInventory = item.GetComponent()?.Inventory; - if (containedInventory != null && (containedInventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) - { - Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => - { - OnItemSpawned(newItem, chosenItemSpawnInfo); - }); - break; - } - } - } - } - break; - } - void OnItemSpawned(Item newItem, ItemSpawnInfo itemSpawnInfo) - { - newItem.Condition = newItem.MaxCondition * itemSpawnInfo.Condition; - if (itemSpawnInfo.InheritEventTags) + if (spawnInfo.SpawnPosition == ItemSpawnInfo.SpawnPositionType.Target) { - foreach (var activeEvent in GameMain.GameSession.EventManager.ActiveEvents) + foreach (var target in targets) { - if (activeEvent is ScriptedEvent scriptedEvent) + if (target is Entity targetEntity) { - scriptedEvent.InheritTags(entity, newItem); + SpawnItem(spawnInfo, entity, sourceBody, position, targetEntity); } } } + else + { + SpawnItem(spawnInfo, entity, sourceBody, position, targetEntity: null); + } } } } ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); - //do this last - the entities spawned by the effect might need the entity for something, so better to remove it last - if (removeItem) - { - for (int i = 0; i < targets.Count; i++) - { - if (targets[i] is Item item) { Entity.Spawner?.AddItemToRemoveQueue(item); } - } - } - if (removeCharacter) - { - for (int i = 0; i < targets.Count; i++) - { - var target = targets[i]; - if (target is Character character) - { - Entity.Spawner?.AddEntityToRemoveQueue(character); - } - else if (target is Limb limb) - { - Entity.Spawner?.AddEntityToRemoveQueue(limb.character); - } - } - } - if (oneShot) { Disabled = true; @@ -2309,17 +2163,283 @@ namespace Barotrauma intervalTimers[entity] = Interval; } - static Character CharacterFromTarget(ISerializableEntity target) + } + private static Character CharacterFromTarget(ISerializableEntity target) + { + Character targetCharacter = target as Character; + if (targetCharacter == null) { - Character targetCharacter = target as Character; - if (targetCharacter == null) + if (target is Limb targetLimb && !targetLimb.Removed) { - if (target is Limb targetLimb && !targetLimb.Removed) + targetCharacter = targetLimb.character; + } + } + return targetCharacter; + } + + void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo, Entity entity, PhysicsBody sourceBody, Vector2 position, Entity targetEntity) + { + Item parentItem = entity as Item; + PhysicsBody parentItemBody = parentItem?.body; + if (user == null && parentItem != null) + { + // Set the user for projectiles spawned from status effects (e.g. flak shrapnels) + SetUser(parentItem.GetComponent()?.User); + } + + if (chosenItemSpawnInfo.SpawnPosition == ItemSpawnInfo.SpawnPositionType.Target && targetEntity != null) + { + entity = targetEntity; + position = entity.WorldPosition; + if (entity is Item it) + { + sourceBody ??= + (entity as Item)?.body ?? + (entity as Character)?.AnimController.Collider; + } + } + + switch (chosenItemSpawnInfo.SpawnPosition) + { + case ItemSpawnInfo.SpawnPositionType.This: + case ItemSpawnInfo.SpawnPositionType.Target: + Entity.Spawner?.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => { - targetCharacter = targetLimb.character; + Projectile projectile = newItem.GetComponent(); + if (entity != null) + { + var rope = newItem.GetComponent(); + if (rope != null && sourceBody != null && sourceBody.UserData is Limb sourceLimb) + { + rope.Attach(sourceLimb, newItem); +#if SERVER + newItem.CreateServerEvent(rope); +#endif + } + float spread = Rand.Range(-chosenItemSpawnInfo.AimSpreadRad, chosenItemSpawnInfo.AimSpreadRad); + float rotation = chosenItemSpawnInfo.RotationRad; + Vector2 worldPos; + if (sourceBody != null) + { + worldPos = sourceBody.Position; + if (user?.Submarine != null) + { + worldPos += user.Submarine.Position; + } + } + else + { + worldPos = entity.WorldPosition; + } + switch (chosenItemSpawnInfo.RotationType) + { + case ItemSpawnInfo.SpawnRotationType.None: + rotation = chosenItemSpawnInfo.RotationRad; + break; + case ItemSpawnInfo.SpawnRotationType.This: + if (sourceBody != null) + { + rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.RotationRad); + } + else if (parentItemBody != null) + { + rotation = parentItemBody.TransformRotation(chosenItemSpawnInfo.RotationRad); + } + break; + case ItemSpawnInfo.SpawnRotationType.Target: + rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); + break; + case ItemSpawnInfo.SpawnRotationType.Limb: + if (sourceBody != null) + { + rotation = sourceBody.TransformedRotation; + } + break; + case ItemSpawnInfo.SpawnRotationType.Collider: + if (parentItemBody != null) + { + rotation = parentItemBody.TransformedRotation; + } + else if (user != null) + { + rotation = user.AnimController.Collider.Rotation + MathHelper.PiOver2; + } + break; + case ItemSpawnInfo.SpawnRotationType.MainLimb: + if (user != null) + { + rotation = user.AnimController.MainLimb.body.TransformedRotation; + } + break; + case ItemSpawnInfo.SpawnRotationType.Random: + if (projectile != null) + { + DebugConsole.LogError("Random rotation is not supported for Projectiles."); + } + else + { + rotation = Rand.Range(0f, MathHelper.TwoPi, Rand.RandSync.Unsynced); + } + break; + default: + throw new NotImplementedException("Item spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType); + } + if (user != null) + { + rotation += chosenItemSpawnInfo.RotationRad * user.AnimController.Dir; + } + rotation += spread; + if (projectile != null) + { + var sourceEntity = (sourceBody?.UserData as ISpatialEntity) ?? entity; + Vector2 spawnPos = sourceEntity.SimPosition; + projectile.Shoot(user, spawnPos, spawnPos, rotation, + ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); + projectile.Item.Submarine = projectile.LaunchSub = sourceEntity?.Submarine; + } + else if (newItem.body != null) + { + newItem.body.SetTransform(newItem.SimPosition, rotation); + Vector2 impulseDir = new Vector2(MathF.Cos(rotation), MathF.Sin(rotation)); + newItem.body.ApplyLinearImpulse(impulseDir * chosenItemSpawnInfo.Impulse); + } + } + OnItemSpawned(newItem, chosenItemSpawnInfo); + }); + break; + case ItemSpawnInfo.SpawnPositionType.ThisInventory: + { + Inventory inventory = null; + if (entity is Character character && character.Inventory != null) + { + inventory = character.Inventory; + } + else if (entity is Item item) + { + foreach (ItemContainer itemContainer in item.GetComponents()) + { + if (itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + { + inventory = itemContainer?.Inventory; + break; + } + } + if (!chosenItemSpawnInfo.SpawnIfCantBeContained && inventory == null) + { + return; + } + } + if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item => + { + if (chosenItemSpawnInfo.Equip && entity is Character character && character.Inventory != null) + { + //if the item is both pickable and wearable, try to wear it instead of picking it up + List allowedSlots = + item.GetComponents().Count() > 1 ? + new List(item.GetComponent()?.AllowedSlots ?? item.GetComponent().AllowedSlots) : + new List(item.AllowedSlots); + allowedSlots.Remove(InvSlotType.Any); + character.Inventory.TryPutItem(item, null, allowedSlots); + } + OnItemSpawned(item, chosenItemSpawnInfo); + }); + } + } + break; + case ItemSpawnInfo.SpawnPositionType.SameInventory: + { + Inventory inventory = null; + if (entity is Character character) + { + inventory = character.Inventory; + } + else if (entity is Item item) + { + inventory = item.ParentInventory; + } + if (inventory != null) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => + { + OnItemSpawned(newItem, chosenItemSpawnInfo); + }); + } + else if (chosenItemSpawnInfo.SpawnIfNotInInventory) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position, onSpawned: (Item newItem) => + { + OnItemSpawned(newItem, chosenItemSpawnInfo); + }); + } + } + break; + case ItemSpawnInfo.SpawnPositionType.ContainedInventory: + { + Inventory thisInventory = null; + if (entity is Character character) + { + thisInventory = character.Inventory; + } + else if (entity is Item item) + { + var itemContainer = item.GetComponent(); + thisInventory = itemContainer?.Inventory; + if (!chosenItemSpawnInfo.SpawnIfCantBeContained && !itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + { + return; + } + } + if (thisInventory != null) + { + foreach (Item item in thisInventory.AllItems) + { + Inventory containedInventory = item.GetComponent()?.Inventory; + if (containedInventory != null && (containedInventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => + { + OnItemSpawned(newItem, chosenItemSpawnInfo); + }); + break; + } + } + } + } + break; + } + void OnItemSpawned(Item newItem, ItemSpawnInfo itemSpawnInfo) + { + newItem.Condition = newItem.MaxCondition * itemSpawnInfo.Condition; + if (itemSpawnInfo.InheritEventTags) + { + foreach (var activeEvent in GameMain.GameSession.EventManager.ActiveEvents) + { + if (activeEvent is ScriptedEvent scriptedEvent) + { + scriptedEvent.InheritTags(entity, newItem); + } + } + } + } + } + + private void TryTriggerAnimation(ISerializableEntity target, Entity entity) + { + if (animationsToTrigger == null) { return; } + // Could probably use a similar pattern in other places above too, but refactoring statuseffects is very volatile. + if ((CharacterFromTarget(target) ?? entity as Character) is Character targetCharacter) + { + foreach (AnimLoadInfo animLoadInfo in animationsToTrigger) + { + if (failedAnimations != null && failedAnimations.Contains((targetCharacter, animLoadInfo))) { continue; } + if (!targetCharacter.AnimController.TryLoadTemporaryAnimation(animLoadInfo, throwErrors: animLoadInfo.ExpectedSpeciesNames.Contains(targetCharacter.SpeciesName))) + { + failedAnimations ??= new HashSet<(Character, AnimLoadInfo)>(); + failedAnimations.Add((targetCharacter, animLoadInfo)); } } - return targetCharacter; } } @@ -2418,7 +2538,7 @@ namespace Barotrauma } } - foreach (var (affliction, amount) in element.Parent.ReduceAffliction) + foreach ((Identifier affliction, float amount) in element.Parent.ReduceAffliction) { Limb targetLimb = null; Character targetCharacter = null; @@ -2439,11 +2559,11 @@ namespace Barotrauma float prevVitality = targetCharacter.Vitality; if (targetLimb != null) { - targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, treatmentAction: actionType, attacker: element.User); } else { - targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType, attacker: element.User); } if (!targetCharacter.IsDead) { @@ -2459,6 +2579,8 @@ namespace Barotrauma } } } + + element.Parent.TryTriggerAnimation(target, element.Entity); } element.Parent.ApplyProjSpecific(deltaTime, @@ -2496,10 +2618,10 @@ namespace Barotrauma return afflictionMultiplier * AfflictionMultiplier; } - private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool? multiplyByMaxVitality) + private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool multiplyByMaxVitality) { float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime); - if (multiplyByMaxVitality ?? affliction.MultiplyByMaxVitality) + if (multiplyByMaxVitality) { afflictionMultiplier *= targetCharacter.MaxVitality / 100f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs index 830c43168..2af009b39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs @@ -51,6 +51,15 @@ public static class Tags public static readonly Identifier ArtifactHolder = "artifactholder".ToIdentifier(); public static readonly Identifier Thalamus = "thalamus".ToIdentifier(); + public static readonly Identifier IgnoreThis = "ignorethis".ToIdentifier(); + public static readonly Identifier UnignoreThis = "unignorethis".ToIdentifier(); + + public static readonly Identifier DeconstructThis = "deconstructthis".ToIdentifier(); + public static readonly Identifier DontDeconstructThis = "dontdeconstructthis".ToIdentifier(); + + public static readonly Identifier Poison = "poison".ToIdentifier(); + public static readonly Identifier Stun = "stun".ToIdentifier(); + public static readonly Identifier Crate = "crate".ToIdentifier(); public static readonly Identifier DontSellItems = "dontsellitems".ToIdentifier(); public static readonly Identifier CargoContainer = "cargocontainer".ToIdentifier(); @@ -67,6 +76,8 @@ public static class Tags public static readonly Identifier StunnerItem = "stunner".ToIdentifier(); public static readonly Identifier MobileRadio = "mobileradio".ToIdentifier(); + public static readonly Identifier Scooter = "scooter".ToIdentifier(); + /// /// Any handcuffs. /// @@ -110,5 +121,8 @@ public static class Tags public static readonly Identifier ElectricalSkill = "electrical".ToIdentifier(); public static readonly Identifier MechanicalSkill = "mechanical".ToIdentifier(); public static readonly Identifier MedicalSkill = "medical".ToIdentifier(); + + public static readonly Identifier SkillLossDeathResistance = "skilllossdeath".ToIdentifier(); + public static readonly Identifier SkillLossRespawnResistance = "skilllossrespawn".ToIdentifier(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index f8b5849f3..61be54ef6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -172,7 +172,7 @@ namespace Barotrauma var allTexts = TextPacks[GameSettings.CurrentConfig.Language] .SelectMany(p => p.Texts.TryGetValue(tag, out var value) ? (IEnumerable)value - : Array.Empty()); + : Array.Empty()).ToList(); var firstOverride = allTexts.FirstOrDefault(t => t.IsOverride); if (firstOverride != default) @@ -190,17 +190,25 @@ namespace Barotrauma var allTexts = TextPacks[GameSettings.CurrentConfig.Language] .SelectMany(p => p.Texts); - var firstOverride = allTexts.SelectMany(kvp => kvp.Value).FirstOrDefault(t => t.IsOverride); - if (firstOverride != default) + foreach (var textList in allTexts) { - 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))); + var firstOverride = textList.Value.FirstOrDefault(t => t.IsOverride); + if (firstOverride != default) + { + //if there's any overrides for this tag, only return the overrides + foreach (var text in textList.Value) + { + if (!text.IsOverride) { continue; } + yield return new KeyValuePair(textList.Key, text.String); + } + } + else + { + foreach (var text in textList.Value) + { + yield return new KeyValuePair(textList.Key, text.String); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs index 99ba1035e..7cecbfa72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs @@ -57,15 +57,14 @@ namespace Barotrauma }; protected override IEnumerable NonActionChildElementNames => nonActionChildElementNames; - public TraitorEvent(TraitorEventPrefab prefab) : base(prefab) + public TraitorEvent(TraitorEventPrefab prefab, int seed) : base(prefab, seed) { this.prefab = prefab; codeWord = string.Empty; } - public override void Init(EventSet? parentSet = null) + protected override void InitEventSpecific(EventSet? parentSet = null) { - base.Init(parentSet); if (traitor == null) { DebugConsole.ThrowError($"Error when initializing event \"{prefab.Identifier}\": traitor not set.\n" + Environment.StackTrace); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 8365930bb..de9e8f723 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -55,6 +55,12 @@ namespace Barotrauma price = location?.GetAdjustedMechanicalCost((int)price) ?? price; + // Adjust by campaign difficulty settings + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + price *= campaign.Settings.ShipyardPriceMultiplier; + } + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characterList.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NetLimitedString.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetLimitedString.cs new file mode 100644 index 000000000..d522174c2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetLimitedString.cs @@ -0,0 +1,22 @@ +#nullable enable + +namespace Barotrauma +{ + internal readonly struct NetLimitedString + { + public readonly string Value; + public const int MaxLength = 255; + + public static readonly NetLimitedString Empty = new(string.Empty); + + public NetLimitedString(string value) + { + Value = value.Length > MaxLength + ? value[..MaxLength] + : value; + } + + public override string ToString() + => Value; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index ccea18bfa..262d7b87a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -639,6 +639,9 @@ namespace Barotrauma if (o is null) { throw new ArgumentNullException(); } } + /// + /// Converts a percentage value in the 0-1 range to a string representation in the format "x %" according to the grammar rules of the selected language + /// public static string GetFormattedPercentage(float v) { return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value; diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 3178f482c..fa9b4e4be 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,247 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.4.4.1 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed welding objective completing automatically in the basic tutorial due to the reduced flooding rates. +- Small fixes to localization issues. +- Bots don't consider opiates valid treatments for burns, because the amount of burns they heal now is so low it just leads to wasting meds. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.4.4.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Updated localizations. +- "Lock default wiring" server setting locks components in circuit boxes too. +- Fixed circuit box labels being resizeable to a negative size, making them impossible to select afterwards. +- Fixed inability to remove circuit box labels in multiplayer. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.4.3.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added new lost cargo missions, in which you must recover cargo from a sunken sub. +- Added six new beacon stations, and updated existing beacon stations. +- Added seven new wrecks. +- Chat messages and NPC dialog is shown in speech bubbles above the character's head. Can be disabled in the game settings. +- Improved the particle effects when the sub hits a level wall, and when cutting/destroying level walls. +- Certain afflictions and talents change the character's walking/running animations (e.g. drunkenness, the "musical talents", vigor, hyperactivity, concussion). +- Removed the randomization of quality from fabricated stackable items. The randomization made them annoying to fabricate, because the items of different quality couldn't be stacked in the fabricator's output slot. +- Motion sensor ranges are visualized when wearing engineer's goggles. +- Made fabricators and deconstructors faster in outposts (it's not fun to force everyone to wait while someone is working a fabricator in an outpost). +- Fire no longer damages husk eggs, fruit and explosives over time. Allowing them to be partially damaged made them really difficult to handle when they happened to be in a stack. Now husk eggs are immune to fire, and the rest get destroyed completely after being in fire for a moment. +- Improvements to the visual effects of lava vents and hydrothermal vents. +- Pets leave behind a corpse when they die. Having them just "pop" often meant you couldn't be sure if the pet had gone missing or died. +- Added a hotkey for dropping the held item. Not bound to any key by default. +- Added a hotkey (by default Alt) that displays a label on all nearby interactable items. The label can be clicked on to interact with the item. Should make it easier to e.g. find loose items on the floor or pick up a specific item when there's many close to each other. +- Tons of changes and improvements to outpost events: the main goal has been to make the things described by the event popups "actually happen", as opposed to just describing them in text. There's now more visual and audio effects in the events, and the things described by the texts tend to actually exist in the game world. There's also been lots of changes to the outcomes of the events: the choices you make should now generally be more meaningful. +- Most outpost events can now be started by another player if the first player who encounters them chooses to ignore them. +- Made the bilge pump circuits non-interactable and the bilge pumps non-wireable in outposts. It was too easy to abuse them to drown the outpost. +- All the pathways from the location you start at in the campaign lead to an outpost. Choosing a new destination at an uninhabited location seems confusing to new players, who haven't yet seen how transitions between levels work. +- Improvements and fixes to harpoons and ropes: the user can now pull towards the target by pressing space (but only when diving), added sounds to reeling and the rope snapping, adjusted the forces and changed how the forces were applied to both the user and the target. +- Added ON_DOCK and ON_UNDOCK outputs to docking ports. +- New automatic docking hatch assembly (much simpler than the old one, now built using a circuit box with labels that explain how it works). +- Added an option to add labels inside circuit boxes (can be used to e.g. explain parts of the circuit). +- Added a menu to the submarine editor that lists all the container tags used in the submarine, explains which tags are available and how they're used, and allows adding them to containers more easily. There's also now a warning on saving if the submarine is missing any common/recommended tags. +- Clown crates now have pressure immunity, making them potentially useful for more than just fooling around. + +Monsters: +- Added Viperling, a venomous variant of Spinelings. +- Reintroduced the legacy monster Mantis. Keep an eye out for anything unusual on the walls when exploring caves! +- Changes to make big monsters more of a threat to characters inside the sub, not just the submarine itself: + - Charybdis can poke its head inside the sub, bite and pull characters out. + - Made monsters better at targeting positions on the hull with a character on the other side (meaning they're more likely to be able to cause shrapnel damage). + - Made the shrapnel particles more noticeable. + - Endworm can poke its mandibles inside the sub and damage characters inside. + - Heavy impacts can launch off very high-velocity shrapnel that can penetrate a couple of inner walls, similar to spineling spikes. + - Added some weaknesses to abyss monsters: Endworm has a weak spot in its mouth, Charybdis flees for a moment if it takes heavy damage to the head or mouth (a feature that initially was there but had been bugged for a while). +- Updated husk's ragdoll, textures and animations. +- Husks can now go unconscious and eventually get back up if not "properly" killed, the same way as huskified humans. +- Adjustments to the loot dropped by Latcher and Charybdis. + +Diving suit changes: +- Several changes to to make it less obvious choice to wear diving suits indoors all the time: + - Hull breaches flood the sub more slowly now, giving you more time to find a suit. + - Reduced walking speed when wearing a suit (with adjustments to the animation to make the suits feel more "tanky", as opposed to just making the character look like it's walking in slow-motion). + - Reduced the damage protection of suits - it was previously so high it encouraged wearing the suits in all situations just for the damage protection they offer. + - Wearing a suit obstructs your vision more now. + - Reduced the crush depths of diving suits to make them match the maximum crush depth of a fully upgraded sub. Allowing players to survive below the sub's crush depth didn't make that much sense: it was practically impossible to recover the sub, so it's better to treat as a "game over" state and kill the players, as opposed to leaving the game in an unrecoverable "soft-locked" state. +- Decreased the armor ratings (damage modifiers) of the (early-game) diving suits. +- Added Explosion Damage resistance to diving suits and protective gear (and clown outfits, ka-honk). +- Made the exosuit more powerful by giving it more damage resistance, a chance to ignore stuns and increasing its speed + +Balance: +- Minor adjustments to the turret balance (most noticeably, made explosive ammo less OP, armor piercing ammo more effective against structures). +- Removed threshers from Cold Caverns. Thresher bites can now cause infected wounds, which can be treated with antibiotic glue, broad-spectrum antibiotics or by applying ethanol or rum to the wound. +- Acid grenades have a slightly longer duration. +- Increased the amount of hyperactivity given by energy drinks. It's still pretty weak, but the previous effect was practically meaningless (making the item more harmful than useful due to the nausea it can cause). +- Dual-wielding ranged weapons reduces reload times and accuracy. + +Medical system: +- Made opiates less of a "solution for everything": they have a much higher risk of causing addiction and overdoses, and morphine heals much more slowly, making it less viable for combat-heavy situations. +- Added "infection" affliction type to add some variety to afflictions. Thresher bites and, on high world hostility campaigns, bleeding wounds and untreated burns have a chance of becoming infected. +- Added "alcohol sensitivity" as a side-effect of antibiotics. This causes drunkenness to build up much faster, so avoid using antibiotics and alcohol together! +- Stabilozine's effects no longer stack: it stops the progress of poisons, but doesn't cure poisonings. +- Added "adrenaline rush" as an effect for adrenaline. "Adrenaline rush" keeps the patient conscious for its duration, removes all active stun when inflicted and applies short-term stun resistance. +- Husk infections can be treated with sufforin and cyanide (but you must be careful to have a cure at hand!). +- Antibiotic glue can be used multiple times. +- Made Pomegrenade Extract a bit more useful: gives the "slow metabolism" buff. +- Ethanol and rum can be poured on limbs to treat infections and burns. +- Removed skill requirements from tonic liquid. +- Alien blood causes organ damage, making it less viable as a risk-free cheap alternative for blood packs. +- Saline can be used to treat infections. +- Resting in bunks heals injuries a little faster now. +- Europabrew can now act as a universal poison cure, but also speeds up husk infection in addition to vulnerability to acid burns. +- Rum can now also be made from pomegrenade. + +Submarines: +- Fixed Azimuth's periscope being too high up, causing the characters to float when using it. +- Fixed hulls being set up strangely in Camel's humps (not extending all the way up to the ceiling, making it possible for there to be holes in the walls without water getting in). +- Various fixes and improvements to the shuttles. +- Fixed too low oxygen output in Typhon's brig, causing characters inside to eventually suffocate without an additional oxygen supply. +- Fixed Azimuth's cargo and engine rooms not draining full due to the hulls extending below the floor and the bilge pumps. +- Fixed sloped wall piece making it difficult to move from Remora to its drone. +- Fixed grenades sometimes going through certain walls (one common spot was Camel's bow). +- Adjusted the hulls in Orca's lower airlock. The small in-between hull with the pump prevented the sub from flooding, because water couldn't flow out from that small hull fast enough to counter the rate of the pump. + +Additional campaign difficulty settings: +- Oxygen tank duration. +- Reactor fuel duration. +- Crew vitality. +- Non-crew NPC vitality. +- Shop purchase prices. +- Shipyard purchase prices (buying new subs and upgrades). +- Severity of injuries from failed repairs. +- Mission income. +- Option to disable the husk infection warning messages. +- Renamed the generic "difficulty" setting as "world hostility", since it only affects things such as monster spawns and environmental hazards. + +Wrecks: +- Added wrecked variants of chaingun, pulse laser, flak cannon, double coilgun and their loaders. +- New wrecks: Barsuk, Camel, Humpback, Typhon 2, Remora, R-29 and Venture. +- Fixes to the wreck spawning logic: linked submarines and non-hulled spaces are taken into account in the placement, preventing them from ending up inside walls. +- Various improvements and fixes to the existing wrecks. +- Added new, higher-res wrecked versions of the shuttle sprites. Marked the old ones as legacy structures. +- Fixed wrecked hatches' broken sprite rendering in front of characters. +- Added console command ‘converttowreck’ to convert submarines to wrecks more easily. + +AI: +- Bots can now be ordered to deconstruct items. There's a separate contextual order for marking items to be deconstructed and a regular order that makes the bots start deconstructing those marked items. +- Improvements to medic AI: they're now better at taking the negative effects of drugs into account, meaning they should be less eager to cause opiate overdoses. +- Made bots better at choosing suits adequate for the current depth. +- Fixed escaped prisoners being unable to seek for weapons in the prisoner transport missions. +- Fixed inability to order bots to turrets that are connected to the periscope via wifi components inside circuit boxes. +- Fixed bots cleaning up active glow sticks and flares. +- Fixed bots sometimes deciding to idle inside docking ports. +- Fixed bots being unable to put two-handed items on their back when trying to use an underwater scooter. +- Fixed medic bots sometimes taking meds from inside their autoinjector headset even if they have suitable meds in a toolbelt or some other container in their inventory. +- Fixed escorted characters (e.g. separatists) attacking you if you steal items from bandits. + +Multiplayer: +- Added player-specific voice chat volume sliders (i.e. if a specific player is very loud in the voice chat, you can reduce their volume or vice versa). +- Fixed inability to remove invisible symbols (= special symbols not included in the fonts the game uses) from your name using the name text box in the server lobby. This seemed to be the cause for the mysterious "name contains symbols disallowed by the server host" errors: if your Steam name contained special symbols the game can't render, you couldn't remove them by editing the name in the server lobby. +- Fixed an exploit that allowed getting free items from stores. +- Replaced Reaper's Tax with optional extra skill loss when respawning mid-round. +- Fixed clients' wallets appearing empty when spawning mid-round. +- Reworked the server lobby layout. The goal was to reorganize the UI to group things in a more logical way and to make things easier to find. +- Adjusted the local voice chat range significantly shorter (comparable to the range of the local text chat now). +- Fixed VOIP breaking after changing the resolution. +- Fixed characters sometimes getting assigned different personality traits between clients and the server. +- Fixed inability to hire more crew if you've reached the maximum crew size, and then fire some to make room for more characters. +- Fixed clients not getting notified in any way when a server has disabled downloading mods directly from the server, causing them to just get stuck in the mod download screen. +- Fixed servers without an active host or anyone else managing the rounds getting stuck if the settings were configured in a specific way: if the respawn transport time was set to infinite (shuttle will spawn once, but will never leave the level and allow a 2nd respawn), the shuttle had spawned and all the players left, and someone new joined the server, they wouldn't be able to end the round because they've never spawned, and could also never respawn. + +Talents: +- Reworked most of the XP-giving talents to make them more meaningful and balanced. +- Fixed "Fireman's Carry" talent not working on stunned characters. +- Fixed tinkerer talent (which allows repairing items above 100%) causing some oddities with repair thresholds: the item would show up as repairable when the condition percentage (relative to the above-normal maximum condition) was below the threshold, but the repair UI wouldn't show up until it was "actually" below the condition where it should become repairable. +- Fixed talent options displaying as locked (gray frame and a lock icon in the corner) even if you've unlocked them if another sub tree is incomplete. +- “Residual Waste" can no longer be exploited for duplicating FPGA circuits. + +Improvements to sub editor's grouping: +- Groups are now called "layers", which reflects their common uses better than "groups". +- Layer options are more easily available in the right-click context menus. +- When a layer is selected, all new entities you place are automatically placed into that layer, so you don't need to manually move every new item you place. +- Added LayerAction as a way for ScriptedEvents to enable or disable layers, useful for modders. + +Fixes: +- Fixed some items sometimes falling through holes on the submarine's floor (and sometimes also getting stuck partially inside the floor). +- Fixed campaign saves getting bricked if you save the game during the brief moment when your crew has died, but the "game over" popup hasn't appeared yet. +- Fixed campaign saves getting bricked if you save the game after you've fired some of your crewmates through the HR manager and the remaining characters have died (or vice versa). +- Fixed (traitor) events that require you to fabricate a specific item complete regardless of what you fabricated. +- Fixed being able to fabricate items from just one ingredient, when the one ingredient matches multiple 'variable' ingredients (ingredients where multiple materials are valid, such as assault rifle magazines which require a munition tip, core and jacket). +- Fixed thalamus organs sometimes spawning in dry hulls, making them die without the player having to do anything. +- Fixed diving suits emitting light when placed in cabinets in the sub editor. +- Fixed waytoascension2 event getting stuck if you choose the "I'll need to think about this" dialog option, end the round, and start a new one. +- Fixed being able to switch submarines using the submarine switch terminals in abandoned outposts or the "dummy outposts" inside normal levels. +- Fixed piezo crystals sometimes spawning close enough to wrecks and beacon stations to zap them. +- Fixed character hover texts (e.g. "[G] grab") not refreshing when you change keybinds. +- Fixed CPR pump animation often causing impact damage to the patient. +- Fixed sonar sometimes not showing parts of the mountain on the ocean floor. +- Fixed beacon stations you've restored during the round not displaying as active on the campaign map until you start a new round. +- Fixed inability to pick up dropped stun batons. +- Wire nodes are placed at the center of the grid cells instead of the corners to get them to align with signal components placed on walls. +- Fixed being able to quickly swap the suit you are currently wearing with a broken suit, by just clicking the broken suit. +- Fixed status effects on certain monster's limbs not working: Charybdis had one that should've made it retreat when it takes heavy damage to the head or mouth, and Fractal Guardian, Moloch and Latcher should've emitted special particles when hit on the weak spots. +- Fixed genetic materials not getting destroyed when a gene splicer gets moved to a duffel bag when a character despawns. +- Fixed language of the units (km/h, m) not changing on the nav terminal when you change the language mid-round. +- Fixed crosshair being mispositioned for a split-second when the character turns around while aiming. +- Fixed leaks sometimes being impossible to repair on "Tail Fin E P2". +- Fixed freezing when you press tab in the sub editor's wiring mode. +- Fixed duct blocks sometimes casting shadows strangely (leaving a see-through gap between the duct block and the adjacent walls). +- Fixed deconstructing a nuclear depth decoy fabricated with the cheap recipe outputting incendium (even though it's not included in the recipe). +- Characters that have thresher genes or who don't need oxygen for some reason (husk infection/symbiosis) don't consume oxygen from tanks. +- Fixed a rare level generation issue that sometimes prevented certain levels from loading, kicking the game back to the main menu or server lobby with the error message "maximum amount of entities exceeded". +- Fixed door's sound muffling effect behaving inconsistently in MP (sometimes muffling sounds even though the door open). +- Fixed certain keys on an AZERTY keyboard not being recognized as keybinds. +- Fixed steep but short wall pieces launching characters downwards too aggressively. +- Fixed buoyancy still affecting docked subs differently than other subs. A sub with docked subs/shuttles would ascend and descend slightly slower than one where the hulls are all part of the same sub. +- Fixed gaps' and waypoints' groups resetting in the sub editor when saving and loading. +- Fixed monsters in the idle (or observing) state moving at too slow speed and changing the direction in which they were heading too rapidly when they wander around. +- Fixed enemy subs' and respawn shuttle's crush depths not being set correctly, sometimes causing them to get crushed in late-campaign levels. +- Fixed items in a character's inventory, who's in the main sub, being counted twice in the "owned item" count displayed in the store interface. +- Fixed purchased items spawning attached to a wall if there's no room in your inventory. +- Fixed characters spawned using console commands not getting ID card tags for the sub they spawn in. + +Modding: +- Fixed inability to publish mods when the language is set to Japanese. +- Added support for defining the ragdoll by a content path, instead of having to define it by a folder. Allows decoupling a ragdoll from a character. +- Added support for (temporarily) overriding character animations using status effects. Allows, for example, wearable items and afflictions to change the character's walking and running animations (see divinggear.xml and afflictions.xml for usage examples). Note that the vanilla animations are configured using the file name, which makes the game search for the animation file from the folder the rest of the character's animations are in. If you want to make your mod add a custom animation to a vanilla character, you need to configure the animation using a file path instead (e.g. path="%ModDir%/CustomWalkAnimation.xml"). +- Fixed variants of character variants not working. +- Added an option to force location types to be owned by a specific faction by adding faction="somefaction" or secondaryfaction="somefaction" to the location type config. +- Fixed inability to drag and drop items into hidden inventories. That can't be done with any vanilla items, but turns out some mods had functionality that relied on this behavior, and we broke it in the latest update. +- Sub editor now warns about rooms with insufficient oxygen output (= if the vents output too little oxygen to support one character). +- Made the joint limit widgets in the character editor (hopefully) a bit more intuitive and easy to use. There's now a gray line that indicates the angle of the limb, and that gray line is what gets clamped between the joint limits. +- Fixed widgets and indicators jittering in the character editor when the character moves. +- The "pitch slide" of turret and ranged weapon charge sounds (used in the vanilla chaingun and rapid fissile accelerator) is now editable using the “ChargeSoundWindupPitchSlide” attribute. +- The deattach speed of repair tools can be edited ("DeattachSpeed" attribute). +- Fixed events that are not allowed at start not triggering if you're in the abyss (or somewhere else a long way away from the destination). +- Fixed PerCave/PerRuin/PerWreck event settings not working correctly in event child sets (the game created the correct number of events, e.g. 3 if there's 3 caves, but each event happened in a random cave). +- Added an option to make TagAction require the target to be in a module with specific tags +- Fixed NPCOperateItemAction being hard-coded to select Controller components, meaning you could only use it to make the character interact with an item that has a Controller component. +- EventManager now works also in the editor test modes. +- Fixed the “interval” attribute of status effects not working when the effect was defined in limbs. +- Added an option to select a specific wreck and a beacon station in the level editor (simplifying testing when you want to test a specific one). +- Fixed room name selection (accessible in the sub editor when editing a hull) being empty if any text file in a mod includes overrides. +- Fixed reputation bars always showing -100 and 100 as the minimum and maximum, even if a custom faction has a different min/max value. +- Fixed crashing when spawning a humanoid character that doesn't have any head sprite configured for it. +- Fixed crashing when placing a door item that's set to be open by default. +- Fixed OnOpen and OnClose action types only working for playing sounds when a door opens or closes, but not for actual status effects. +- Fixed "custom skills" the character's gained during the round (skills levels the character/job originally doesn't have) disappearing between rounds, and only reappearing when the character gains some skill level. +- Fixed MissionActions that are set to choose from a pool of missions always choosing the same mission. +- Fixed inability to take specific kinds of items from a container that requires holding some item. For example, if you needed to hold a crowbar to access a cabinet, and took an item from the cabinet in a way that forces that crowbar item to be unequipped. +- Fixed WeldedSprites not appearing in the sprite editor. +- Fixed connection panel interface's borders overlapping with the connector sprites on certain resolutions if the panel is very tall. +- Fixed OnImpact effects not working on broken items even if AllowWhenBroken is set true. +- Fixed TriggerComponents no longer applying forces or other effects to characters when they ragdoll. +- Made OnSuccess and OnUse effects usable in Repairables (previously only OnFailure effects worked). +- Fixed event editor crashing if you tried to load an event that had action with attributes set to incorrect types of values (e.g. with a CheckMoneyAction with amount set to a string). +- Fixed "Collider" spawn rotation type when spawning items via a status effect not taking the flipping of the parent item into account, causing e.g. projectiles to launch to the right when aiming to the left. +- Fixed tracer particles not showing up when a projectile is spawned using a status effect indoors. +- Fixed crash if a non-attachable item is made attachable mid-round (e.g. via status effects or console commands) and the submarine then saved. +- Fixed monsters always aiming ranged attacks at the position of the target character regardless of which limb they're targeting. Was not noticeable in the vanilla game, because none of the vanilla enemies tried to aim at a specific limb. +- RequireAimToUse is no longer forcibly enabled on ranged weapons (i.e. it's possible to create a ranged weapon you can fire just by left clicking). + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.3.0.4 ------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/hintmanager.xml b/Barotrauma/BarotraumaShared/hintmanager.xml index b7611ed1b..6eb27946a 100644 --- a/Barotrauma/BarotraumaShared/hintmanager.xml +++ b/Barotrauma/BarotraumaShared/hintmanager.xml @@ -79,4 +79,8 @@ + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs b/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs index 128ead59e..b1376d11f 100644 --- a/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs +++ b/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs @@ -36,7 +36,7 @@ public sealed class FabricatorQualityRollTests } } - var result = new Fabricator.QualityResult(startingQuality, plusOneProbability, plusTwoProbability); + var result = new Fabricator.QualityResult(startingQuality, HasRandomQuality: true, plusOneProbability, plusTwoProbability); // iterate to confirm that the percentage chance is correct const int iterations = 100000; diff --git a/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs index ece129035..24ae767ff 100644 --- a/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs +++ b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using Xunit; @@ -14,8 +14,8 @@ public sealed class GenericToolBoxTests { public static Arbitrary IdentifierPairGenerator() { - return Arb.From(from Identifier first in Arb.Generate() - from Identifier second in Arb.Generate().Where(second => second != first) + return Arb.From(from Identifier first in Arb.Generate().Where(first => !first.Value.Contains('~')) + from Identifier second in Arb.Generate().Where(second => second != first && !second.Value.Contains('~')) select new DifferentIdentifierPair(first, second)); } } @@ -28,6 +28,9 @@ public sealed class GenericToolBoxTests public DifferentIdentifierPair(Identifier first, Identifier second) { if (first == second) { throw new InvalidOperationException("Identifiers must be different"); } + //tildes have a special meaning in stat identifiers, don't use them + if (first.Value.Contains('~')) { throw new InvalidOperationException($"{first} is not a valid identifier."); } + if (second.Value.Contains('~')) { throw new InvalidOperationException($"{second} is not a valid identifier."); } First = first; Second = second; diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs index b89762131..1dcc8e4b4 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -108,5 +108,54 @@ namespace Barotrauma result += $"<{string.Join(", ", t.GetGenericArguments().Select(NameWithGenerics))}>"; return result; } + + /// + /// Gets a type by its name, with backwards compatibility for types that have been renamed. + /// + /// + public static Type? GetTypeWithBackwardsCompatibility(string nameSpace, string typeName, bool throwOnError, bool ignoreCase) + { + if (Assembly.GetEntryAssembly() is not { } entryAssembly) { return null; } + var types = entryAssembly + .GetTypes() + .Where(t => NameMatches(t.Namespace, nameSpace, ignoreCase)); + + foreach (Type type in types) + { + if (NameMatches(type.Name, typeName, ignoreCase)) + { + return type; + } + + if (type.GetCustomAttribute() is { } knownAsAttribute) + { + if (NameMatches(knownAsAttribute.PreviousName, typeName, ignoreCase)) + { + return type; + } + } + } + + if (throwOnError) + { + throw new TypeLoadException($"Could not find the type {typeName} in namespace {nameSpace}"); + } + + return null; + + static bool NameMatches(string? name1, string? name2, bool ignoreCase) + => string.Equals(name1, name2, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + + /// + /// The names of generic types include the arity at the end (fancy way of saying the number of parameters, e.g. GUISelectionCarousel would be GUISelectionCarousel`1) + /// This method strips that part out. + /// + public static Identifier GetTypeNameWithoutGenericArity(Type type) + { + string name = type.Name; + int index = name.IndexOf('`'); + return (index == -1 ? name : name.Substring(0, index)).ToIdentifier(); + } } } \ No newline at end of file diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/TypePreviouslyKnownAs.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/TypePreviouslyKnownAs.cs new file mode 100644 index 000000000..f94514105 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/TypePreviouslyKnownAs.cs @@ -0,0 +1,24 @@ +using System; + +namespace Barotrauma +{ + /// + /// This attribute is used to indicate that a class was previously known by a different name. + /// This is used for backwards compatibility when we have types that are loaded from XML using reflection. + /// + /// Only works in cases where we use to load the type. + /// + /// If you wish to use this, you will need to replace the call to Type.GetType() in the load method with + /// ReflectionUtils.GetTypeWithBackwardsCompatibility(). + /// + [AttributeUsage(AttributeTargets.Class)] + public class TypePreviouslyKnownAs : Attribute + { + public string PreviousName { get; } + + public TypePreviouslyKnownAs(string previousName) + { + PreviousName = previousName; + } + } +} \ No newline at end of file diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs index c5baac2cd..530a6b5de 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs @@ -151,6 +151,14 @@ namespace Microsoft.Xna.Framework.Input _map.Add(1073742097, Keys.BrowserRefresh); _map.Add(1073742098, Keys.BrowserFavorites); _map.Add(1073742106, Keys.Sleep); + // Map keys on an Azerty layout to the corresponding keys on a US layout + _map.Add(178, Keys.OemTilde); // ² + _map.Add(41, Keys.OemMinus); // ) + _map.Add(36, Keys.Add); // $ + _map.Add(249, Keys.OemQuotes); // ù + _map.Add(42, Keys.OemPipe); // * + _map.Add(58, Keys.OemPeriod); // : + _map.Add(33, Keys.OemQuestion); // ! } public static Keys ToXna(int key)